forked from toolshed/abra
		
	
		
			
				
	
	
		
			280 lines
		
	
	
		
			7.0 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			280 lines
		
	
	
		
			7.0 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
| 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/i18n"
 | |
| 	"coopcloud.tech/abra/pkg/log"
 | |
| 	"github.com/spf13/cobra"
 | |
| )
 | |
| 
 | |
| var AppCmdCommand = &cobra.Command{
 | |
| 	// translators: `app command` command
 | |
| 	Use:     i18n.G("command <domain> [service | --local] <cmd> [[args] [flags] | [flags] -- [args]]"),
 | |
| 	Aliases: []string{i18n.G("cmd")},
 | |
| 	Short:   i18n.G("Run app commands"),
 | |
| 	Long: i18n.G(`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: i18n.G(`  # pass <cmd> args/flags without "--"
 | |
|   abra app cmd 1312.net app my_cmd_arg foo --user bar
 | |
| 
 | |
|   # pass <cmd> 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(i18n.G("requires at least 2 arguments with --local/-l"))
 | |
| 			}
 | |
| 
 | |
| 			if slices.Contains(os.Args, "--") {
 | |
| 				if cmd.ArgsLenAtDash() > 2 {
 | |
| 					return errors.New(i18n.G("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 <recipe> 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(i18n.G("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(i18n.G("cannot use --local & --user together"))
 | |
| 		}
 | |
| 
 | |
| 		hasCmdArgs, parsedCmdArgs := parseCmdArgs(args, local)
 | |
| 
 | |
| 		if _, err := os.Stat(app.Recipe.AbraShPath); err != nil {
 | |
| 			if os.IsNotExist(err) {
 | |
| 				log.Fatal(i18n.G("%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.Debug(i18n.G("--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.Debug(i18n.G("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(i18n.G("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.Debug(i18n.G("%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.Fatal(i18n.G("no service %s for %s?", targetServiceName, app.Name))
 | |
| 		}
 | |
| 
 | |
| 		log.Debug(i18n.G("running command %s within the context of %s_%s", cmdName, app.StackName(), targetServiceName))
 | |
| 
 | |
| 		if hasCmdArgs {
 | |
| 			log.Debug(i18n.G("parsed following command arguments: %s", parsedCmdArgs))
 | |
| 		} else {
 | |
| 			log.Debug(i18n.G("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{
 | |
| 	// translators: `app list` command
 | |
| 	Use:     i18n.G("list <domain> [flags]"),
 | |
| 	Aliases: []string{i18n.G("ls")},
 | |
| 	Short:   i18n.G("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,
 | |
| 		i18n.G("local"),
 | |
| 		i18n.G("l"),
 | |
| 		false,
 | |
| 		i18n.G("run command locally"),
 | |
| 	)
 | |
| 
 | |
| 	AppCmdCommand.Flags().StringVarP(
 | |
| 		&remoteUser,
 | |
| 		i18n.G("user"),
 | |
| 		i18n.G("u"),
 | |
| 		"",
 | |
| 		i18n.G("request remote user"),
 | |
| 	)
 | |
| 
 | |
| 	AppCmdCommand.Flags().BoolVarP(
 | |
| 		&disableTTY,
 | |
| 		i18n.G("tty"),
 | |
| 		i18n.G("T"),
 | |
| 		false,
 | |
| 		i18n.G("disable remote TTY"),
 | |
| 	)
 | |
| 
 | |
| 	AppCmdCommand.Flags().BoolVarP(
 | |
| 		&internal.Chaos,
 | |
| 		i18n.G("chaos"),
 | |
| 		i18n.G("C"),
 | |
| 		false,
 | |
| 		i18n.G("ignore uncommitted recipes changes"),
 | |
| 	)
 | |
| }
 |