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
}