package app import ( "errors" "fmt" "os" "os/exec" "slices" "sort" "strings" "coopcloud.tech/abra/cli/internal" appPkg "coopcloud.tech/abra/pkg/app" "coopcloud.tech/abra/pkg/autocomplete" "coopcloud.tech/abra/pkg/client" "coopcloud.tech/abra/pkg/log" "github.com/spf13/cobra" ) var AppCmdCommand = &cobra.Command{ Use: "command [service | --local] [[args] [flags] | [flags] -- [args]]", Aliases: []string{"cmd"}, Short: "Run app commands", Long: `Run an app specific command. These commands are bash functions, defined in the abra.sh of the recipe itself. They can be run within the context of a service (e.g. app) or locally on your work station by passing "--local/-l". N.B. If using the "--" style to pass arguments, flags (e.g. "--local/-l") must be passed *before* the "--". It is possible to pass arguments without the "--" as long as no dashes are present (i.e. "foo" works without "--", "-foo" does not).`, Example: ` # pass args/flags without "--" abra app cmd 1312.net app my_cmd_arg foo --user bar # pass args/flags with "--" abra app cmd 1312.net app my_cmd_args --user bar -- foo -vvv # drop the [service] arg if using "--local/-l" abra app cmd 1312.net my_cmd --local`, Args: func(cmd *cobra.Command, args []string) error { if local { if !(len(args) >= 2) { return errors.New("requires at least 2 arguments with --local/-l") } if slices.Contains(os.Args, "--") { if cmd.ArgsLenAtDash() > 2 { return errors.New("accepts at most 2 args with --local/-l") } } // NOTE(d1): it is unclear how to correctly validate this case // // abra app cmd 1312.net app test_cmd_args foo --local // FATAL doesn't have a app function // // "app" should not be there, but there is no reliable way to detect arg // count when the user can pass an arbitrary amount of recipe command // arguments return nil } if !(len(args) >= 3) { return errors.New("requires at least 3 arguments") } return nil }, ValidArgsFunction: func( cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { switch l := len(args); l { case 0: return autocomplete.AppNameComplete() case 1: if !local { return autocomplete.ServiceNameComplete(args[0]) } return autocomplete.CommandNameComplete(args[0]) case 2: if !local { return autocomplete.CommandNameComplete(args[0]) } return nil, cobra.ShellCompDirectiveDefault default: return nil, cobra.ShellCompDirectiveError } }, Run: func(cmd *cobra.Command, args []string) { app := internal.ValidateApp(args) if err := app.Recipe.Ensure(internal.GetEnsureContext()); err != nil { log.Fatal(err) } if local && remoteUser != "" { log.Fatal("cannot use --local & --user together") } hasCmdArgs, parsedCmdArgs := parseCmdArgs(args, local) if _, err := os.Stat(app.Recipe.AbraShPath); err != nil { if os.IsNotExist(err) { log.Fatalf("%s does not exist for %s?", app.Recipe.AbraShPath, app.Name) } log.Fatal(err) } if local { cmdName := args[1] if err := internal.EnsureCommand(app.Recipe.AbraShPath, app.Recipe.Name, cmdName); err != nil { log.Fatal(err) } log.Debugf("--local detected, running %s on local work station", cmdName) var exportEnv string for k, v := range app.Env { exportEnv = exportEnv + fmt.Sprintf("%s='%s'; ", k, v) } var sourceAndExec string if hasCmdArgs { log.Debugf("parsed following command arguments: %s", parsedCmdArgs) sourceAndExec = fmt.Sprintf("TARGET=local; APP_NAME=%s; STACK_NAME=%s; %s . %s; %s %s", app.Name, app.StackName(), exportEnv, app.Recipe.AbraShPath, cmdName, parsedCmdArgs) } else { log.Debug("did not detect any command arguments") sourceAndExec = fmt.Sprintf("TARGET=local; APP_NAME=%s; STACK_NAME=%s; %s . %s; %s", app.Name, app.StackName(), exportEnv, app.Recipe.AbraShPath, cmdName) } shell := "/bin/bash" if _, err := os.Stat(shell); errors.Is(err, os.ErrNotExist) { log.Debugf("%s does not exist locally, use /bin/sh as fallback", shell) shell = "/bin/sh" } cmd := exec.Command(shell, "-c", sourceAndExec) if err := internal.RunCmd(cmd); err != nil { log.Fatal(err) } return } cmdName := args[2] if err := internal.EnsureCommand(app.Recipe.AbraShPath, app.Recipe.Name, cmdName); err != nil { log.Fatal(err) } serviceNames, err := appPkg.GetAppServiceNames(app.Name) if err != nil { log.Fatal(err) } matchingServiceName := false targetServiceName := args[1] for _, serviceName := range serviceNames { if serviceName == targetServiceName { matchingServiceName = true } } if !matchingServiceName { log.Fatalf("no service %s for %s?", targetServiceName, app.Name) } log.Debugf("running command %s within the context of %s_%s", cmdName, app.StackName(), targetServiceName) if hasCmdArgs { log.Debugf("parsed following command arguments: %s", parsedCmdArgs) } else { log.Debug("did not detect any command arguments") } cl, err := client.New(app.Server) if err != nil { log.Fatal(err) } if err := internal.RunCmdRemote( cl, app, disableTTY, app.Recipe.AbraShPath, targetServiceName, cmdName, parsedCmdArgs, remoteUser); err != nil { log.Fatal(err) } }, } var AppCmdListCommand = &cobra.Command{ Use: "list [flags]", Aliases: []string{"ls"}, Short: "List all available commands", Args: cobra.MinimumNArgs(1), Run: func(cmd *cobra.Command, args []string) { app := internal.ValidateApp(args) if err := app.Recipe.Ensure(internal.GetEnsureContext()); err != nil { log.Fatal(err) } cmdNames, err := appPkg.ReadAbraShCmdNames(app.Recipe.AbraShPath) if err != nil { log.Fatal(err) } sort.Strings(cmdNames) for _, cmdName := range cmdNames { fmt.Println(cmdName) } }, } func parseCmdArgs(args []string, isLocal bool) (bool, string) { var ( parsedCmdArgs string hasCmdArgs bool ) if isLocal { if len(args) > 2 { return true, fmt.Sprintf("%s ", strings.Join(args[2:], " ")) } } else { if len(args) > 3 { return true, fmt.Sprintf("%s ", strings.Join(args[3:], " ")) } } return hasCmdArgs, parsedCmdArgs } var ( local bool remoteUser string disableTTY bool ) func init() { AppCmdCommand.Flags().BoolVarP( &local, "local", "l", false, "run command locally", ) AppCmdCommand.Flags().StringVarP( &remoteUser, "user", "u", "", "request remote user", ) AppCmdCommand.Flags().BoolVarP( &disableTTY, "tty", "T", false, "disable remote TTY", ) AppCmdCommand.Flags().BoolVarP( &internal.Chaos, "chaos", "C", false, "ignore uncommitted recipes changes", ) }