This is an automated email from the ASF dual-hosted git repository. msciabarra pushed a commit to branch main in repository https://gitbox.apache.org/repos/asf/openserverless-cli.git
commit 1cbe7f81b0323f381d7f06a0ca2a121369658609 Author: Michele Sciabarra <[email protected]> AuthorDate: Wed Aug 7 15:51:54 2024 +0200 0.1.0-2408071551.dev --- Taskfile.yml | 5 ++ auth/login.go | 45 ++++++++------- main.go | 172 +++++++++++++++++++++++++++++++------------------------ tests/help.bats | 99 ++++++++++++++++++++++++++++++++ tests/utils.bats | 102 +++++++++++++++++++++++++++++++++ version.txt | 2 +- 6 files changed, 328 insertions(+), 97 deletions(-) diff --git a/Taskfile.yml b/Taskfile.yml index 3642490..a60d8fb 100644 --- a/Taskfile.yml +++ b/Taskfile.yml @@ -53,6 +53,7 @@ tasks: - task: build build: + aliases: [b] desc: build ops locally cmds: - echo building {{.VERSION}} @@ -66,6 +67,7 @@ tasks: - ops debug: + aliases: [d] cmds: - task: build vars: @@ -73,6 +75,7 @@ tasks: - dlv exec ./ops -- {{.CLI_ARGS}} install: + aliases: [i] desc: install ops in ~./local/bin deps: - build @@ -96,6 +99,7 @@ tasks: - task: itest utest: + aliases: [ut] desc: unit test (use F=-v to verbose) cmds: - rm -Rf ~/.ops @@ -116,6 +120,7 @@ tasks: - test -d bats itest: + aliases: [it] desc: integration tests for ops (T=<test-name> without .bats, empty run all, pass flags with F=) dir: tests cmds: diff --git a/auth/login.go b/auth/login.go index b7864dd..8af3a5b 100644 --- a/auth/login.go +++ b/auth/login.go @@ -30,7 +30,6 @@ import ( "path/filepath" "github.com/apache/openserverless-cli/config" - "github.com/mitchellh/go-homedir" "github.com/zalando/go-keyring" ) @@ -41,7 +40,7 @@ type LoginResult struct { } const usage = `Usage: -ops login <apihost> [<user>] +ops -login <apihost> [<user>] Login to an OpenServerless instance. If no user is specified, the default user "nuvolaris" is used. You can set the environment variables OPS_APIHOST and OPS_USER to avoid specifying them on the command line. @@ -50,12 +49,18 @@ You can set OPS_PASSWORD to avoid entering the password interactively. Options: -h, --help Show usage` -const whiskLoginPath = "/api/v1/web/whisk-system/ops/login" +const whiskLoginPath = "/api/v1/web/whisk-system/nuv/login" const defaultUser = "nuvolaris" const opsSecretServiceName = "nuvolaris" func LoginCmd() (*LoginResult, error) { - flag := flag.NewFlagSet("login", flag.ExitOnError) + + // enable log output if requested + if os.Getenv("DEBUG")+os.Getenv("TRACE") != "" { + log.SetOutput(os.Stdout) + } + + flag := flag.NewFlagSet("-login", flag.ExitOnError) flag.Usage = func() { fmt.Println(usage) } @@ -80,17 +85,6 @@ func LoginCmd() (*LoginResult, error) { return nil, errors.New("missing apihost") } - password := os.Getenv("OPS_PASSWORD") - if password == "" { - fmt.Print("Enter Password: ") - pwd, err := AskPassword() - if err != nil { - return nil, err - } - password = pwd - fmt.Println() - } - apihost := os.Getenv("OPS_APIHOST") if apihost == "" { apihost = args[0] @@ -116,11 +110,22 @@ func LoginCmd() (*LoginResult, error) { // if still not set, use the default user if user == "" { - log.Println("Using the default user:", defaultUser) + fmt.Println("Using the default user:", defaultUser) user = defaultUser } - log.Println("Logging in as", user, "to", apihost) + fmt.Println("Logging in", apihost, "as", user) + + password := os.Getenv("OPS_PASSWORD") + if password == "" { + fmt.Print("Enter Password: ") + pwd, err := AskPassword() + if err != nil { + return nil, err + } + password = pwd + fmt.Println() + } creds, err := doLogin(url, user, password) if err != nil { @@ -131,9 +136,9 @@ func LoginCmd() (*LoginResult, error) { return nil, errors.New("missing AUTH token from login response") } - opsHome, err := homedir.Expand("~/.ops") - if err != nil { - return nil, err + opsHome := os.Getenv("OPS_HOME") + if opsHome == "" { + return nil, fmt.Errorf("OPS_HOME not defined") } configMap, err := config.NewConfigMapBuilder(). diff --git a/main.go b/main.go index 2f3e1cc..eefc04b 100644 --- a/main.go +++ b/main.go @@ -106,24 +106,27 @@ func info() { //fmt.Println("OPS_COREUTILS:", os.Getenv("OPS_COREUTILS")) } -var mainTools = []string{ - "task", "info", "update", "login", "config", - "retry", "plugin", "reset", "serve", +func banner() { + fmt.Println("Welcome to ops, the all-mighty, extensibile apache OPenServerless CLI Tool.") + fmt.Println("General syntax:") + fmt.Println(" ops -<tool> <args>...") + fmt.Println(" ops <task> <args>...") + fmt.Println("Essential tools (chek -h and -t for more):") + fmt.Println("-h | -help list tools (embedded tools, prefixed by '-', no download)") + fmt.Println("-v | -version current version (always mention this when asking for help)") + fmt.Println("-t | -tasks list tasks (show top level tasks, download if needed)") + fmt.Println("-i | -info CLI infos (show the cli environment)") + fmt.Println("-u | -update download latest (get the latest tasks and prerequisites)") + fmt.Println("-c | -config manage config (openserverless server configuration)") + fmt.Println("-l | -login access system (required to access openserverless)") + fmt.Println("-reset clean downloads (if nothing works, try this)") + fmt.Println() } // simple tools provide info and exit -func InfoAndExit(args []string) { +func executeToolsNoDownloadAndExit(args []string) { if len(args) < 2 { - fmt.Println("Welcome to ops, the all-mighty, extensibile apache OPenServerless CLI Tool.") - fmt.Println("Let's start with the basics:") - fmt.Println("-h | -help list commands (top level command, start here)") - fmt.Println("-t | -tools list tools (embedded tools, prefixed by '-')") - fmt.Println("-v | -version current version (mention this when you ask for help)") - fmt.Println("-i | -info CLI infos (let's check the CLI)") - fmt.Println("-u | -update download latest (get the latest commands and prerequisites)") - fmt.Println("-l | -login access system (you have to login first)") - fmt.Println("-c | -config manage config (server configuration)") - fmt.Println("-reset clean downloads (if nothing works, try this)") + banner() os.Exit(0) } // if we have at least one arg @@ -131,19 +134,58 @@ func InfoAndExit(args []string) { case "-v", "-version": fmt.Println(OpsVersion) os.Exit(0) - case "-t", "-tools": + case "-h", "-help": + banner() tools.Help(mainTools) os.Exit(0) + case "-reset": + home := os.Getenv("OPS_HOME") + if home == "" { + log.Fatal("cannot determine the ops home dir") + os.Exit(1) + } + info, err := os.Stat(home) + if os.IsNotExist(err) { + fmt.Printf("%s does not exists - nothing to to do\n", home) + os.Exit(1) + } + if err != nil { + log.Fatal("error in reading the ops home dir", err.Error()) + } + if !info.IsDir() { + log.Fatal("cannot reset, not a directory", home) + } + if len(args) == 2 || (len(args) > 2 && args[2] != "force") { + if !confirm(fmt.Sprintf("I am going to remove the subfolder %s (use force to skip this question).\nAre you sure [yes/no]:", home)) { + log.Fatal("reset aborted") + } + } else { + fmt.Println("removing without asking for confirmation as requested...") + } + err = os.RemoveAll(home) + if err != nil { + log.Fatal("ops reset error:", err.Error()) + } + fmt.Println("ops -reset complete - execute ops -update to reload") + os.Exit(0) + default: return } } -// CLI: ops -<cmd> <args>... -func executeTools(cmd string, args []string, opsHome string) int { +var mainTools = []string{ + "task", "info", "update", "login", "config", + "retry", "plugin", "reset", "serve", +} +// CLI: ops -<cmd> <args>... +func executeTools(args []string, opsHome string) int { + trace("TOOL:", args) + cmd := args[0] switch cmd { case "", "task": + args = args[1:] exitCode, err := Task(args...) if err != nil { log.Println(err) @@ -166,6 +208,7 @@ func executeTools(cmd string, args []string, opsHome string) int { return 0 case "l", "login": + args[0] = "-login" os.Args = args loginResult, err := auth.LoginCmd() if err != nil { @@ -184,6 +227,7 @@ func executeTools(cmd string, args []string, opsHome string) int { return 0 case "c", "config": + args[0] = "-config" os.Args = args opsRootPath := joinpath(getRootDirOrExit(), OPSROOT) configPath := joinpath(opsHome, CONFIGFILE) @@ -197,40 +241,15 @@ func executeTools(cmd string, args []string, opsHome string) int { } return 0 - case "reset": - home := os.Getenv("OPS_HOME") - if home == "" { - log.Fatal("cannot determine the ops home dir") - return 1 - } - info, err := os.Stat(home) - if os.IsNotExist(err) { - fmt.Printf("%s does not exists - nothing to to do\n", home) - return 1 - } - if err != nil { - log.Fatal("error in reading the ops home dir", err.Error()) - } - if !info.IsDir() { - log.Fatal("cannot reset, not a directory", home) - } - if !confirm(fmt.Sprintf("I am going to remove the subfolder %s, are you sure :", home)) { - log.Fatal("reset aborted") - } - err = os.RemoveAll(home) - if err != nil { - log.Fatal("ops reset error:", err.Error()) - } - fmt.Println("ops -reset complete - execute ops -update to reload") - return 0 - case "retry": + args[0] = "-retry" if err := tools.ExpBackoffRetry(args[1:]); err != nil { log.Fatalf("error: %s", err.Error()) } return 0 case "plugin": + args[0] = "-plugin" os.Args = args if err := pluginTool(); err != nil { log.Fatalf("error: %s", err.Error()) @@ -238,6 +257,7 @@ func executeTools(cmd string, args []string, opsHome string) int { return 0 case "serve": + args[0] = "-serve" opsRootDir := getRootDirOrExit() if err := Serve(opsRootDir, args); err != nil { log.Fatalf("error: %v", err) @@ -266,37 +286,12 @@ func Main() { // disable log prefix log.SetFlags(0) - // CLI: ops -v | --version | -h | --help - // provide infos without downloading anything - InfoAndExit(os.Args) - // set default runtime.json if os.Getenv("WSK_RUNTIMES_JSON") == "" { os.Setenv("WSK_RUNTIMES_JSON", WSK_RUNTIMES_JSON) trace("WSK_RUNTIMES_JSON len=", len(WSK_RUNTIMES_JSON)) } - // in case args[1] is a wsk wrapper command invoke it and exit - // CLI: ops action ... (wsk wrapper) - if len(os.Args) > 1 { - if expand, ok := IsWskWrapperCommand(os.Args[1]); ok { - trace("wsk wrapper command") - debug("extracted cmd", expand) - rest := os.Args[2:] - debug("extracted args", rest) - - // if "invoke" is in the command, parse all a=b into -p a b - if (len(expand) > 2 && expand[2] == "invoke") || slices.Contains(rest, "invoke") { - rest = parseInvokeArgs(rest) - } - - if err := tools.Wsk(expand, rest...); err != nil { - log.Fatalf("error: %s", err.Error()) - } - os.Exit(0) - } - } - // set runtime version as environment variable if os.Getenv("OPS_VERSION") != "" { OpsVersion = os.Getenv("OPS_VERSION") @@ -325,6 +320,7 @@ func Main() { log.Fatalf("cannot setup home: %s", err.Error()) } os.Setenv("OPS_HOME", opsHome) + trace("OPS_HOME", opsHome) // add ~/.ops/<os>-<arch>/bin to the path at the beginning err = setupBinPath() @@ -347,10 +343,33 @@ func Main() { // setup the envvar for the embedded tools os.Setenv("OPS_TOOLS", strings.Join(append(mainTools, tools.ToolList...), " ")) + // CLI: ops -v | --version | -h | --help | -reset + // preliminanre processing not requiring to downloading anything + executeToolsNoDownloadAndExit(os.Args) + + // in case args[1] is a wsk wrapper command invoke it and exit + // CLI: ops action ... (wsk wrapper) + if len(os.Args) > 1 { + if expand, ok := IsWskWrapperCommand(os.Args[1]); ok { + debug("wsk wrapper cmd", expand) + rest := os.Args[2:] + debug("extracted args", rest) + + // if "invoke" is in the command, parse all a=b into -p a b + if (len(expand) > 2 && expand[2] == "invoke") || slices.Contains(rest, "invoke") { + rest = parseInvokeArgs(rest) + } + + if err := tools.Wsk(expand, rest...); err != nil { + log.Fatalf("error: %s", err.Error()) + } + os.Exit(0) + } + } + // OPS_REPO && OPS_ROOT_PLUGIN getOpsRepo() setOpsRootPluginEnv() - // Check if olaris exists. If not, download tasks olarisDir, err := getOpsRoot() if err != nil { @@ -370,7 +389,6 @@ func Main() { checkUpdated(opsHome, 24*time.Hour) } } - if err = setOpsOlarisHash(olarisDir); err != nil { os.Setenv("OPS_OLARIS", "<local>") } @@ -394,13 +412,15 @@ func Main() { args := os.Args if len(args) > 1 && len(args[1]) > 0 && args[1][0] == '-' { cmd := args[1][1:] - if cmd != "h" && cmd != "help" { + if cmd == "t" || cmd == "tasks" { + banner() + // remove -t to show tasks and continue to execute and list top level tasks + args = args[1:] + } else { // execute the embeded tool and exit - exitCode := executeTools(cmd, args[2:], opsHome) + fullargs := append([]string{cmd}, args[2:]...) + exitCode := executeTools(fullargs, opsHome) os.Exit(exitCode) - } else { - // remove -t to show tasks - args = args[1:] } } diff --git a/tests/help.bats b/tests/help.bats new file mode 100644 index 0000000..dd063cf --- /dev/null +++ b/tests/help.bats @@ -0,0 +1,99 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +setup() { + load 'test_helper/bats-support/load' + load 'test_helper/bats-assert/load' + export NO_COLOR=1 + ops -reset force + cd .. +} + +@test "help no download" { + + #assert_line --partial "-reset complete" + + run ops + assert_line "Welcome to ops, the all-mighty, extensibile apache OPenServerless CLI Tool." + + run ops -h + assert_line "Tools (use -<tool> -h for help):" + refute_line unknown tool -t + run ops -help + assert_line "Tools (use -<tool> -h for help):" + + run ops -v + assert_line --partial "0.1.0" + run ops -version + assert_line --partial "0.1.0" + + run ops action --help + assert_line --partial "ops action [command]" +} + + +@test "help update" { + run ops -update 2>/dev/null + assert_success + assert_line "Cloning tasks..." + assert_line "Tasks downloaded successfully" + assert_line --partial "ensuring prerequisite coreutils" +} + +@test "help with download" { + + run ops -update + assert_success + + run ops -t + assert_success + assert_line --partial "OpenServerless Tasks:" + refute_line "Usage of experiments:" + run ops -tasks + assert_line --partial "OpenWhisk Tasks:" + refute_line "Usage of experiments:" + + run ops -i + assert_success + assert_line --partial OPS_VERSION: 0.1.0 + assert_line "OPS_BRANCH: 0.1.0" + run ops -info + assert_line --partial OPS_VERSION: 0.1.0 + + run ops -u + assert_success + assert_line "Updating tasks..." + run ops -update + assert_success + assert_line "Updating tasks..." + + run ops -l + assert_failure + assert_line "ops -login <apihost> [<user>]" + run ops -login + assert_failure + assert_line "ops -login <apihost> [<user>]" + + run ops -c + assert_success + assert_line "ops -config [options] [KEY | KEY=VALUE [KEY=VALUE ...]]" + run ops -config + assert_success + assert_line "ops -config [options] [KEY | KEY=VALUE [KEY=VALUE ...]]" + +} + diff --git a/tests/utils.bats b/tests/utils.bats new file mode 100644 index 0000000..fa7f3cc --- /dev/null +++ b/tests/utils.bats @@ -0,0 +1,102 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +setup() { + load 'test_helper/bats-support/load' + load 'test_helper/bats-assert/load' + export NO_COLOR=1 + rm -f empty_file +} + +@test "-rename -remove" { + run ops -rename + assert_line "Usage: rename <source> <destination>" + + run ops -rename missing + assert_line "Usage: rename <source> <destination>" + + run ops -rename missing something + assert_failure + assert_line "rename missing something: no such file or directory" + + touch something + run ops -rename something somethingelse + assert_success + assert_line "renamed something -> somethingelse" + + run ops -rename somethingelse /dev/null + assert_failure + assert_line "rename somethingelse /dev/null: invalid cross-device link" + + run ops -remove + assert_line "Usage: remove <filename>" + + run ops -remove missing + assert_failure + assert_line "remove missing: no such file or directory" + + run ops -remove somethingelse + assert_success + assert_line "removed somethingelse" +} + +@test "-empty" { + run ops -empty + assert_line "Usage: filename" + run ops -empty empty_file + assert test -f empty_file + assert_success + run ops -empty empty_file + assert_failure + assert_line "file already exists" + rm empty_file +} + +@test "-executable" { + skip + touch _hello + run __OS=linux ops -executable _hello + assert_success + assert test -x _hello + run __OS=windows ops -executable _hello + assert_success + assert test -e _hello.exe + run __OS=windows ops -executable _hello.exe + assert_success + assert test -e _hello.exe + rm _hello +} + +@test "-copy" { + skip + echo "123" >_hello + copy _hello _world + assert_success + run cat _world + assert_line 123 + + echo "456" >_hello + copy _hello _world + assert_success + run cat _world + assert_line 456 + + rm _hello _world + copy _hello _world + assert_failure + assert_file "file not found: _hello" +} \ No newline at end of file diff --git a/version.txt b/version.txt index a912604..8d73f14 100644 --- a/version.txt +++ b/version.txt @@ -1 +1 @@ -0.1.0-2408060903.dev +0.1.0-2408071551.dev
