forked from toolshed/abra
		
	
		
			
				
	
	
		
			282 lines
		
	
	
		
			7.1 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			282 lines
		
	
	
		
			7.1 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")},
 | 
						|
	// translators: Short description for `app cmd` command
 | 
						|
	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 cmd list` command
 | 
						|
	Use:     i18n.G("list <domain> [flags]"),
 | 
						|
	Aliases: []string{i18n.G("ls")},
 | 
						|
	// translators: Short description for `app cmd list` command
 | 
						|
	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"),
 | 
						|
	)
 | 
						|
}
 |