package app import ( "context" "errors" "fmt" "os" "os/exec" "sort" "strings" "coopcloud.tech/abra/cli/internal" "coopcloud.tech/abra/pkg/app" appPkg "coopcloud.tech/abra/pkg/app" "coopcloud.tech/abra/pkg/autocomplete" "coopcloud.tech/abra/pkg/client" "coopcloud.tech/abra/pkg/log" "github.com/urfave/cli/v3" ) var appCmdCommand = cli.Command{ Name: "command", Aliases: []string{"cmd"}, Usage: "Run app commands", Description: `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". Arguments can be passed into these functions using the "-- " syntax. **WARNING**: [options] must be passed directly after the "cmd" sub-command.`, UsageText: "abra app cmd [options] [] [-- ]", HideHelpCommand: true, Flags: []cli.Flag{ internal.LocalCmdFlag, internal.RemoteUserFlag, internal.TtyFlag, internal.ChaosFlag, }, Before: internal.SubCommandBefore, Commands: []*cli.Command{ &appCmdListCommand, }, EnableShellCompletion: true, ShellComplete: func(ctx context.Context, cmd *cli.Command) { args := cmd.Args() switch args.Len() { case 0: autocomplete.AppNameComplete(ctx, cmd) case 1: autocomplete.ServiceNameComplete(args.Get(0)) case 2: cmdNameComplete(args.Get(0)) } }, Action: func(ctx context.Context, cmd *cli.Command) error { app := internal.ValidateApp(cmd) if err := app.Recipe.Ensure(internal.Chaos, internal.Offline); err != nil { log.Fatal(err) } if internal.LocalCmd && internal.RemoteUser != "" { internal.ShowSubcommandHelpAndError(cmd, errors.New("cannot use --local & --user together")) } hasCmdArgs, parsedCmdArgs := parseCmdArgs(cmd.Args().Slice(), internal.LocalCmd) 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 internal.LocalCmd { if !(cmd.Args().Len() >= 2) { internal.ShowSubcommandHelpAndError(cmd, errors.New("missing arguments")) } cmdName := cmd.Args().Get(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) } } else { if !(cmd.Args().Len() >= 3) { internal.ShowSubcommandHelpAndError(cmd, errors.New("missing arguments")) } targetServiceName := cmd.Args().Get(1) cmdName := cmd.Args().Get(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 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, app.Recipe.AbraShPath, targetServiceName, cmdName, parsedCmdArgs); err != nil { log.Fatal(err) } } return nil }, } 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 } func cmdNameComplete(appName string) { app, err := app.Get(appName) if err != nil { return } cmdNames, _ := getShCmdNames(app) if err != nil { return } for _, n := range cmdNames { fmt.Println(n) } } var appCmdListCommand = cli.Command{ Name: "list", Aliases: []string{"ls"}, Usage: "List all available commands", UsageText: "abra app cmd ls [options] ", HideHelpCommand: true, Flags: []cli.Flag{ internal.ChaosFlag, }, EnableShellCompletion: true, ShellComplete: autocomplete.AppNameComplete, Before: internal.SubCommandBefore, Action: func(ctx context.Context, cmd *cli.Command) error { app := internal.ValidateApp(cmd) if err := app.Recipe.EnsureExists(); err != nil { log.Fatal(err) } if !internal.Chaos { if err := app.Recipe.EnsureIsClean(); err != nil { log.Fatal(err) } if !internal.Offline { if err := app.Recipe.EnsureUpToDate(); err != nil { log.Fatal(err) } } if err := app.Recipe.EnsureLatest(); err != nil { log.Fatal(err) } } cmdNames, err := getShCmdNames(app) if err != nil { log.Fatal(err) } for _, cmdName := range cmdNames { fmt.Println(cmdName) } return nil }, } func getShCmdNames(app appPkg.App) ([]string, error) { cmdNames, err := appPkg.ReadAbraShCmdNames(app.Recipe.AbraShPath) if err != nil { return nil, err } sort.Strings(cmdNames) return cmdNames, nil }