main.go (433 lines of code) (raw):

// 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. package openserverless import ( "errors" "fmt" "log" "os" "os/exec" "path/filepath" "strings" "time" "github.com/apache/openserverless-cli/auth" "github.com/apache/openserverless-cli/config" "github.com/apache/openserverless-cli/tools" "golang.org/x/exp/slices" "github.com/mitchellh/go-homedir" _ "embed" ) func setupCmd(me string) (string, error) { if os.Getenv("OPS_CMD") != "" { return os.Getenv("OPS_CMD"), nil } // look in path me, err := exec.LookPath(me) if err != nil { return "", err } trace("found", me) // resolve links fileInfo, err := os.Lstat(me) if err != nil { return "", err } if fileInfo.Mode()&os.ModeSymlink != 0 { me, err = filepath.EvalSymlinks(me) if err != nil { return "", err } trace("resolving link to", me) } // get the absolute path me, err = filepath.Abs(me) if err != nil { return "", err } //nolint:errcheck os.Setenv("OPS_CMD", me) trace("OPS_CMD:", me) return me, nil } func setupBinPath() error { // initialize tools (used by the shell to find myself) bindir, err := EnsureBindir() if err == nil { os.Setenv("PATH", fmt.Sprintf("%s%c%s", bindir, os.PathListSeparator, os.Getenv("PATH"))) debugf("PATH=%s", os.Getenv("PATH")) } return err } func setOpsRootPluginEnv() { if os.Getenv("OPS_ROOT_PLUGIN") == "" { //nolint:errcheck os.Setenv("OPS_ROOT_PLUGIN", os.Getenv("OPS_PWD")) } trace("set OPS_ROOT_PLUGIN", os.Getenv("OPS_ROOT_PLUGIN")) } func info() { fmt.Println("OPS & OPS_CMD:", os.Getenv("OPS_CMD")) fmt.Println("OPS_VERSION:", os.Getenv("OPS_VERSION")) fmt.Println("OPS_BRANCH:", os.Getenv("OPS_BRANCH")) fmt.Println("OPS_BIN:", os.Getenv("OPS_BIN")) fmt.Println("OPS_TMP:", os.Getenv("OPS_TMP")) fmt.Println("OPS_HOME:", os.Getenv("OPS_HOME")) fmt.Println("OPS_ROOT:", os.Getenv("OPS_ROOT")) fmt.Println("OPS_REPO:", os.Getenv("OPS_REPO")) fmt.Println("OPS_PWD:", os.Getenv("OPS_PWD")) fmt.Println("OPS_OLARIS:", os.Getenv("OPS_OLARIS")) fmt.Println("OPS_ROOT_PLUGIN:", os.Getenv("OPS_ROOT_PLUGIN")) //fmt.Println("OPS_TOOLS:", os.Getenv("OPS_TOOLS")) //fmt.Println("OPS_COREUTILS:", os.Getenv("OPS_COREUTILS")) } 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 executeToolsNoDownloadAndExit(args []string) { if len(args) < 2 { banner() os.Exit(0) } // if we have at least one arg switch args[1] { case "-v", "-version": fmt.Println(OpsVersion) os.Exit(0) 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 } } 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) } return exitCode case "i", "info": info() return 0 case "u", "update": // ok no up, nor down, let's download it dir, err := pullTasks(true, true) if err != nil { log.Fatalf("error: %v", err) } if err := setOpsOlarisHash(dir); err != nil { log.Fatal("unable to set OPS_OLARIS...", err.Error()) } return 0 case "l", "login": args[0] = "-login" os.Args = args loginResult, err := auth.LoginCmd() if err != nil { log.Fatalf("error: %s", err.Error()) } if loginResult == nil { return 1 } fmt.Println("Successfully logged in as " + loginResult.Login + ".") if err := wskPropertySet(loginResult.ApiHost, loginResult.Auth); err != nil { log.Fatalf("error: %s", err.Error()) } fmt.Println("OpenServerless host and auth set successfully. You are now ready to use ops!") return 0 case "c", "config": args[0] = "-config" os.Args = args opsRootPath := joinpath(getRootDirOrExit(), OPSROOT) configPath := joinpath(opsHome, CONFIGFILE) configMap, err := buildConfigMap(opsRootPath, configPath) if err != nil { log.Fatalf("error: %s", err.Error()) } if err := config.ConfigTool(*configMap); err != nil { log.Fatalf("error: %s", err.Error()) } return 0 case "retry": args[0] = "-retry" if err := tools.ExpBackoffRetry(args); 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()) } return 0 case "serve": args[0] = "-serve" opsRootDir := getRootDirOrExit() if err := Serve(opsRootDir, args); err != nil { log.Fatalf("error: %v", err) } return 0 default: // check if it is an embedded to and invoke it if tools.IsTool(cmd) { code, err := tools.RunTool(cmd, args[1:]) if err != nil { log.Print(err.Error()) } return code } // no embeded tool found warn("unknown tool", "-"+cmd) return 1 } //return 0 // unreachable - or it should be! } func Main() { var err error // disable log prefix log.SetFlags(0) // 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)) } // set runtime version as environment variable if os.Getenv("OPS_VERSION") != "" { OpsVersion = os.Getenv("OPS_VERSION") } else { OpsVersion = strings.TrimSpace(OpsVersion) os.Setenv("OPS_VERSION", OpsVersion) } // setup OPS_CMD // CLI: ops ... me := os.Args[0] if strings.Contains("ops ops.exe", filepath.Base(me)) { _, err = setupCmd(me) if err != nil { log.Fatalf("cannot setup cmd: %s", err.Error()) } } os.Setenv("OPS", me) // setup home opsHome := os.Getenv("OPS_HOME") if opsHome == "" { opsHome, err = homedir.Expand("~/.ops") } if err != nil { 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() if err != nil { log.Fatalf("cannot setup PATH: %s", err.Error()) } // ensure there is ~/.ops/tmp err = setupTmp() if err != nil { log.Fatalf("cannot setup OPS_TMP: %s", err.Error()) } // setup the OPS_PWD variable err = setOpsPwdEnv() if err != nil { log.Fatalf("cannot setup OPS_PWD: %s", err.Error()) } // setup the envvar for the embedded tools os.Setenv("OPS_TOOLS", strings.Join(tools.MergeToolsList(mainTools), " ")) // 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 { olarisDir := joinpath(joinpath(opsHome, getOpsBranch()), "olaris") if !isDir(olarisDir) { log.Println("Welcome to ops! Setting up...") olarisDir, err = pullTasks(true, true) if err != nil { log.Fatalf("cannot locate or download OPS_ROOT: %s", err.Error()) } // if just updated, do not repeat if len(os.Args) > 1 && os.Args[1] == "-update" { os.Exit(0) } } else { // check if olaris was recently updated checkUpdated(opsHome, 24*time.Hour) } } if err = setOpsOlarisHash(olarisDir); err != nil { os.Setenv("OPS_OLARIS", "<local>") } // set the enviroment variables from the config opsRootDir := getRootDirOrExit() debug("opsRootDir", opsRootDir) err = setAllConfigEnvVars(opsRootDir, opsHome) if err != nil { log.Fatalf("cannot apply env vars from configs: %s", err.Error()) } // preflight checks - we need at least ssh curl to proceed if err := preflightChecks(); err != nil { log.Fatalf("failed preflight check: %s", err.Error()) } // first argument with prefix "-" is considered an embedded tool // using "-" or "--" or "-task" invokes the embedded task // CLI: ops -<tool> (embedded tool) args := os.Args if len(args) > 1 && len(args[1]) > 0 && args[1][0] == '-' { cmd := args[1][1:] 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 fullargs := append([]string{cmd}, args[2:]...) exitCode := executeTools(fullargs, opsHome) os.Exit(exitCode) } } if err := runOps(opsRootDir, args); err != nil { log.Fatalf("task execution error: %s", err.Error()) } } // parse all a=b into -p a b func parseInvokeArgs(rest []string) []string { trace("parsing invoke args") args := []string{} for _, arg := range rest { if strings.Contains(arg, "=") { kv := strings.Split(arg, "=") p := []string{"-p", kv[0], kv[1]} args = append(args, p...) } else { args = append(args, arg) } } debug("parsed invoke args", args) return args } // getRootDirOrExit returns the olaris dir or exits (Fatal) if not found func getRootDirOrExit() string { dir, err := getOpsRoot() if err != nil { log.Fatalf("error: %s", err.Error()) } return dir } func setAllConfigEnvVars(opsRootDir string, configDir string) error { trace("setting all config env vars") configMap, err := buildConfigMap(joinpath(opsRootDir, OPSROOT), joinpath(configDir, CONFIGFILE)) if err != nil { return err } kv := configMap.Flatten() for k, v := range kv { if err := os.Setenv(k, v); err != nil { return err } debug("env var set", k, v) } return nil } func wskPropertySet(apihost, auth string) error { args := []string{"property", "set", "--apihost", apihost, "--auth", auth} cmd := append([]string{"wsk"}, args...) if err := tools.Wsk(cmd); err != nil { return err } return nil } func runOps(baseDir string, args []string) error { err := Ops(baseDir, args[1:]) if err == nil { return nil } // If the task is not found, // fallback to plugins var taskNotFoundErr *TaskNotFoundErr if errors.As(err, &taskNotFoundErr) { trace("task not found, looking for plugin:", args[1]) plgDir, err := findTaskInPlugins(args[1]) if err != nil { return taskNotFoundErr } debug("Found plugin", plgDir) if err := Ops(plgDir, args[2:]); err != nil { log.Fatalf("error: %s", err.Error()) } return nil } return err } func setOpsPwdEnv() error { if os.Getenv("OPS_PWD") == "" { dir, err := os.Getwd() if err != nil { return err } //nolint:errcheck os.Setenv("OPS_PWD", dir) } trace("set OPS_PWD", os.Getenv("OPS_PWD")) return nil } func buildConfigMap(opsRootPath string, configPath string) (*config.ConfigMap, error) { plgOpsRootMap, err := GetOpsRootPlugins() if err != nil { return nil, err } configMap, err := config.NewConfigMapBuilder(). WithOpsRoot(opsRootPath). WithConfigJson(configPath). WithPluginOpsRoots(plgOpsRootMap). Build() if err != nil { return nil, err } return &configMap, nil } var wskWrapperCommands = map[string][]string{ "action": {"wsk", "action"}, "activation": {"wsk", "activation"}, "invoke": {"wsk", "action", "invoke", "-r"}, "logs": {"wsk", "activation", "logs"}, "package": {"wsk", "package"}, "result": {"wsk", "activation", "result"}, "rule": {"wsk", "rule"}, "trigger": {"wsk", "trigger"}, "url": {"wsk", "action", "get", "--url"}, } func IsWskWrapperCommand(name string) ([]string, bool) { cmd, ok := wskWrapperCommands[name] return cmd, ok }