github.com/criteo/command-launcher@v0.0.0-20230407142452-fb616f546e98/internal/command/default-command.go (about)

     1  package command
     2  
     3  import (
     4  	"fmt"
     5  	"html/template"
     6  	"os"
     7  	"os/exec"
     8  	"path/filepath"
     9  	"runtime"
    10  	"strings"
    11  
    12  	"github.com/criteo/command-launcher/internal/helper"
    13  	log "github.com/sirupsen/logrus"
    14  )
    15  
    16  const (
    17  	CACHE_DIR_PATTERN  = "#CACHE#"
    18  	OS_PATTERN         = "#OS#"
    19  	ARCH_PATTERN       = "#ARCH#"
    20  	BINARY_PATTERN     = "#BINARY#"
    21  	SCRIPT_PATTERN     = "#SCRIPT#"
    22  	EXT_PATTERN        = "#EXT#"
    23  	SCRIPT_EXT_PATTERN = "#SCRIPT_EXT#"
    24  )
    25  
    26  /*
    27  DefaultCommand implements the command.Command interface
    28  
    29  There are two types of cdt command:
    30  1. group command
    31  2. executable command
    32  
    33  A group command doesn't do any thing but contain other executable commands. An executable
    34  command must be under a group command, the default one is the cdt root (group = "")
    35  
    36  for example, command: cdt hotfix create
    37  
    38  hotfix is a group command, and create is a command under the "hotfix" group command
    39  
    40  Another example: cdt ls, here ls is an executable command under the root "" group command
    41  
    42  Note: nested group command is not supported! It is not a good practice to have to much level
    43  of nested commands like: cdt workspace create moab.
    44  
    45  The group field of group command is ignored.
    46  
    47  An additional "category" field is reserved in case we have too much first level commands,
    48  we can use it to category them in the cdt help output.
    49  */
    50  type DefaultCommand struct {
    51  	CmdID                 string
    52  	CmdPackageName        string
    53  	CmdRepositoryID       string
    54  	CmdRuntimeGroup       string
    55  	CmdRuntimeName        string
    56  	CmdName               string         `json:"name" yaml:"name"`
    57  	CmdCategory           string         `json:"category" yaml:"category"`
    58  	CmdType               string         `json:"type" yaml:"type"`
    59  	CmdGroup              string         `json:"group" yaml:"group"`
    60  	CmdArgsUsage          string         `json:"argsUsage" yaml:"argsUsage"` // optional, set this field will custom the one line usage
    61  	CmdExamples           []ExampleEntry `json:"examples" yaml:"examples"`
    62  	CmdShortDescription   string         `json:"short" yaml:"short"`
    63  	CmdLongDescription    string         `json:"long" yaml:"long"`
    64  	CmdExecutable         string         `json:"executable" yaml:"executable"`
    65  	CmdArguments          []string       `json:"args" yaml:"args"`
    66  	CmdDocFile            string         `json:"docFile" yaml:"docFile"`
    67  	CmdDocLink            string         `json:"docLink" yaml:"docLink"`
    68  	CmdValidArgs          []string       `json:"validArgs" yaml:"validArgs"`         // the valid argument options
    69  	CmdValidArgsCmd       []string       `json:"validArgsCmd" yaml:"validArgsCmd"`   // the command to call to get the args for autocompletion
    70  	CmdRequiredFlags      []string       `json:"requiredFlags" yaml:"requiredFlags"` // the required flags -- deprecated in 1.9.0, see flags, exclusiveFlags, and groupFlags
    71  	CmdFlags              []Flag         `json:"flags" yaml:"flags"`
    72  	CmdExclusiveFlags     [][]string     `json:"exclusiveFlags" yaml:"exclusiveFlags"`
    73  	CmdGroupFlags         [][]string     `json:"groupFlags" yaml:"groupFlags"`
    74  	CmdFlagValuesCmd      []string       `json:"flagValuesCmd" yaml:"flagValuesCmd"` // the command to call flag values for autocompletion
    75  	CmdCheckFlags         bool           `json:"checkFlags" yaml:"checkFlags"`       // whether parse the flags and check them before execution
    76  	CmdRequestedResources []string       `json:"requestedResources" yaml:"requestedResources"`
    77  
    78  	PkgDir string `json:"pkgDir"`
    79  }
    80  
    81  func NewDefaultCommandFromCopy(cmd Command, pkgDir string) *DefaultCommand {
    82  	return &DefaultCommand{
    83  		CmdID:           cmd.ID(),
    84  		CmdPackageName:  cmd.PackageName(),
    85  		CmdRepositoryID: cmd.RepositoryID(),
    86  		CmdRuntimeGroup: cmd.RuntimeGroup(),
    87  		CmdRuntimeName:  cmd.RuntimeName(),
    88  
    89  		CmdName:               cmd.Name(),
    90  		CmdCategory:           cmd.Category(),
    91  		CmdType:               cmd.Type(),
    92  		CmdGroup:              cmd.Group(),
    93  		CmdArgsUsage:          cmd.ArgsUsage(),
    94  		CmdExamples:           cmd.Examples(),
    95  		CmdShortDescription:   cmd.ShortDescription(),
    96  		CmdLongDescription:    cmd.LongDescription(),
    97  		CmdExecutable:         cmd.Executable(),
    98  		CmdArguments:          cmd.Arguments(),
    99  		CmdDocFile:            cmd.DocFile(),
   100  		CmdDocLink:            cmd.DocLink(),
   101  		CmdValidArgs:          cmd.ValidArgs(),
   102  		CmdValidArgsCmd:       cmd.ValidArgsCmd(),
   103  		CmdRequiredFlags:      cmd.RequiredFlags(),
   104  		CmdFlags:              cmd.Flags(),
   105  		CmdExclusiveFlags:     cmd.ExclusiveFlags(),
   106  		CmdGroupFlags:         cmd.GroupFlags(),
   107  		CmdFlagValuesCmd:      cmd.FlagValuesCmd(),
   108  		CmdCheckFlags:         cmd.CheckFlags(),
   109  		CmdRequestedResources: cmd.RequestedResources(),
   110  		PkgDir:                pkgDir,
   111  	}
   112  }
   113  
   114  func CmdID(repo, pkg, group, name string) string {
   115  	return fmt.Sprintf("%s>%s>%s>%s", repo, pkg, group, name)
   116  }
   117  func CmdReverseID(repo, pkg, group, name string) string {
   118  	return fmt.Sprintf("%s@%s@%s@%s", name, group, pkg, repo)
   119  }
   120  
   121  func (cmd *DefaultCommand) Execute(envVars []string, args ...string) (int, error) {
   122  	arguments := append(cmd.CmdArguments, args...)
   123  	cmd.interpolateArray(&arguments)
   124  	command := cmd.interpolateCmd()
   125  
   126  	log.Debug("Command line: ", command, " ", arguments)
   127  
   128  	ctx := exec.Command(command, arguments...)
   129  	// inject additional environments
   130  	env := append(os.Environ(), envVars...)
   131  	ctx.Env = env
   132  
   133  	ctx.Stdout = os.Stdout
   134  	ctx.Stderr = os.Stderr
   135  	ctx.Stdin = os.Stdin
   136  
   137  	log.Debug("Command start executing")
   138  	if err := ctx.Run(); err != nil {
   139  		log.Debug("Command execution err: ", err)
   140  		if exitError, ok := err.(*exec.ExitError); ok {
   141  			log.Debug("Exit code: ", exitError.ExitCode())
   142  			return exitError.ExitCode(), err
   143  		} else {
   144  			exitcode := ctx.ProcessState.ExitCode()
   145  			return exitcode, err
   146  		}
   147  	}
   148  
   149  	exitcode := ctx.ProcessState.ExitCode()
   150  	log.Debug("Command executed successfully with exit code: ", exitcode)
   151  	return exitcode, nil
   152  }
   153  
   154  func (cmd *DefaultCommand) ExecuteWithOutput(envVars []string, args ...string) (int, string, error) {
   155  	wd, err := os.Getwd()
   156  	if err != nil {
   157  		return 1, "", err
   158  	}
   159  	arguments := append(cmd.CmdArguments, args...)
   160  	cmd.interpolateArray(&arguments)
   161  	command := cmd.interpolateCmd()
   162  
   163  	env := append(os.Environ(), envVars...)
   164  
   165  	log.Debug("Execute command line with output: ", command, " ", arguments)
   166  
   167  	return helper.CallExternalWithOutput(env, wd, command, arguments...)
   168  }
   169  
   170  func (cmd *DefaultCommand) ExecuteValidArgsCmd(envVars []string, args ...string) (int, string, error) {
   171  	return cmd.executeArrayCmd(envVars, cmd.CmdValidArgsCmd, args...)
   172  }
   173  
   174  func (cmd *DefaultCommand) ExecuteFlagValuesCmd(envVars []string, flagCmd []string, args ...string) (int, string, error) {
   175  	return cmd.executeArrayCmd(envVars, flagCmd, args...)
   176  }
   177  
   178  func (cmd *DefaultCommand) executeArrayCmd(envVars []string, cmdArray []string, args ...string) (int, string, error) {
   179  	wd, err := os.Getwd()
   180  	if err != nil {
   181  		return 1, "", err
   182  	}
   183  	validCmd := ""
   184  	validArgs := []string{}
   185  	if cmdArray != nil {
   186  		argsLen := len(cmdArray)
   187  		if argsLen >= 2 {
   188  			validCmd = cmdArray[0]
   189  			validArgs = cmdArray[1:argsLen]
   190  		} else if argsLen >= 1 {
   191  			validCmd = cmdArray[0]
   192  		}
   193  	}
   194  	if validCmd == "" {
   195  		return 0, "", nil
   196  	}
   197  	cmd.interpolateArray(&validArgs)
   198  	// Should we interpolate the argumments too???
   199  	return helper.CallExternalWithOutput(envVars, wd, cmd.interpolate(validCmd), append(validArgs, args...)...)
   200  }
   201  
   202  func (cmd *DefaultCommand) ID() string {
   203  	return CmdID(cmd.CmdRepositoryID, cmd.CmdPackageName, cmd.CmdGroup, cmd.CmdName)
   204  }
   205  
   206  func (cmd *DefaultCommand) PackageName() string {
   207  	return cmd.CmdPackageName
   208  }
   209  
   210  func (cmd *DefaultCommand) RepositoryID() string {
   211  	return cmd.CmdRepositoryID
   212  }
   213  
   214  func (cmd *DefaultCommand) RuntimeGroup() string {
   215  	if cmd.CmdRuntimeGroup == "" {
   216  		return cmd.CmdGroup
   217  	}
   218  	return cmd.CmdRuntimeGroup
   219  }
   220  
   221  func (cmd *DefaultCommand) RuntimeName() string {
   222  	if cmd.CmdRuntimeName == "" {
   223  		return cmd.CmdName
   224  	}
   225  	return cmd.CmdRuntimeName
   226  }
   227  
   228  // Full group name in form of group name @ [empty] @ package @ repo
   229  // Read as a group command named [name] in root group (empty) from package [package] managed by repo [repo]
   230  func (cmd *DefaultCommand) FullGroup() string {
   231  	return CmdReverseID(cmd.CmdRepositoryID, cmd.CmdPackageName, "", cmd.CmdGroup)
   232  }
   233  
   234  // Full command name in form of name @ group @ package @ repo
   235  // Read as a command named [name] in group [group] from package [package] managed by repo [repo]
   236  func (cmd *DefaultCommand) FullName() string {
   237  	return CmdReverseID(cmd.CmdRepositoryID, cmd.CmdPackageName, cmd.CmdGroup, cmd.CmdName)
   238  }
   239  
   240  func (cmd *DefaultCommand) Name() string {
   241  	return cmd.CmdName
   242  }
   243  
   244  func (cmd *DefaultCommand) Type() string {
   245  	if cmd.CmdType != "group" &&
   246  		cmd.CmdType != "executable" &&
   247  		cmd.CmdType != "system" {
   248  		// for invalid cmd type, set it to group to make it do nothing
   249  		return "group"
   250  	}
   251  	return cmd.CmdType
   252  }
   253  
   254  func (cmd *DefaultCommand) Category() string {
   255  	return cmd.CmdCategory
   256  }
   257  
   258  func (cmd *DefaultCommand) Group() string {
   259  	return cmd.CmdGroup
   260  }
   261  
   262  // custom the usage message for the arguments format
   263  // this is useful to name your arguments and show argument orders
   264  // this will replace the one-line usage message in help
   265  // NOTE: there is no need to provide the command name in the usage
   266  // it will be added by command launcher automatically
   267  func (cmd *DefaultCommand) ArgsUsage() string {
   268  	return cmd.CmdArgsUsage
   269  }
   270  
   271  func (cmd *DefaultCommand) Examples() []ExampleEntry {
   272  	if cmd.CmdExamples == nil {
   273  		return []ExampleEntry{}
   274  	}
   275  	return cmd.CmdExamples
   276  }
   277  
   278  func (cmd *DefaultCommand) LongDescription() string {
   279  	return cmd.CmdLongDescription
   280  }
   281  
   282  func (cmd *DefaultCommand) ShortDescription() string {
   283  	return cmd.CmdShortDescription
   284  }
   285  
   286  func (cmd *DefaultCommand) Executable() string {
   287  	return cmd.CmdExecutable
   288  }
   289  
   290  func (cmd *DefaultCommand) Arguments() []string {
   291  	if cmd.CmdArguments == nil {
   292  		return []string{}
   293  	}
   294  	return cmd.CmdArguments
   295  }
   296  
   297  func (cmd *DefaultCommand) DocFile() string {
   298  	return cmd.interpolate(cmd.CmdDocFile)
   299  }
   300  
   301  func (cmd *DefaultCommand) DocLink() string {
   302  	return cmd.CmdDocLink
   303  }
   304  
   305  func (cmd *DefaultCommand) RequestedResources() []string {
   306  	if cmd.CmdRequestedResources == nil {
   307  		return []string{}
   308  	}
   309  	return cmd.CmdRequestedResources
   310  }
   311  
   312  func (cmd *DefaultCommand) ValidArgs() []string {
   313  	if cmd.CmdValidArgs != nil && len(cmd.CmdValidArgs) > 0 {
   314  		return cmd.CmdValidArgs
   315  	}
   316  	return []string{}
   317  }
   318  
   319  func (cmd *DefaultCommand) ValidArgsCmd() []string {
   320  	if cmd.CmdValidArgsCmd != nil && len(cmd.CmdValidArgsCmd) > 0 {
   321  		return cmd.CmdValidArgsCmd
   322  	}
   323  	return []string{}
   324  }
   325  
   326  func (cmd *DefaultCommand) RequiredFlags() []string {
   327  	if cmd.CmdRequiredFlags != nil && len(cmd.CmdRequiredFlags) > 0 {
   328  		return cmd.CmdRequiredFlags
   329  	}
   330  	return []string{}
   331  }
   332  
   333  func (cmd *DefaultCommand) Flags() []Flag {
   334  	if cmd.CmdFlags != nil && len(cmd.CmdFlags) > 0 {
   335  		return cmd.CmdFlags
   336  	}
   337  	return []Flag{}
   338  }
   339  
   340  func (cmd *DefaultCommand) ExclusiveFlags() [][]string {
   341  	if cmd.CmdExclusiveFlags == nil {
   342  		return [][]string{}
   343  	}
   344  	return cmd.CmdExclusiveFlags
   345  }
   346  
   347  func (cmd *DefaultCommand) GroupFlags() [][]string {
   348  	if cmd.CmdGroupFlags == nil {
   349  		return [][]string{}
   350  	}
   351  	return cmd.CmdGroupFlags
   352  }
   353  
   354  func (cmd *DefaultCommand) FlagValuesCmd() []string {
   355  	if cmd.CmdFlagValuesCmd != nil && len(cmd.CmdFlagValuesCmd) > 0 {
   356  		return cmd.CmdFlagValuesCmd
   357  	}
   358  	return []string{}
   359  }
   360  
   361  func (cmd *DefaultCommand) CheckFlags() bool {
   362  	return cmd.CmdCheckFlags
   363  }
   364  
   365  func (cmd *DefaultCommand) PackageDir() string {
   366  	return cmd.PkgDir
   367  }
   368  
   369  func (cmd *DefaultCommand) SetPackageDir(pkgDir string) {
   370  	cmd.PkgDir = pkgDir
   371  }
   372  
   373  func (cmd *DefaultCommand) SetNamespace(repoId string, pkgName string) {
   374  	cmd.CmdRepositoryID = repoId
   375  	cmd.CmdPackageName = pkgName
   376  	cmd.CmdID = fmt.Sprintf("%s:%s:%s:%s", repoId, pkgName, cmd.Group(), cmd.Name())
   377  }
   378  
   379  func (cmd *DefaultCommand) SetRuntimeGroup(name string) {
   380  	cmd.CmdRuntimeGroup = name
   381  }
   382  
   383  func (cmd *DefaultCommand) SetRuntimeName(name string) {
   384  	cmd.CmdRuntimeName = name
   385  }
   386  
   387  func (cmd *DefaultCommand) copyArray(src []string) []string {
   388  	if len(src) == 0 {
   389  		return []string{}
   390  	}
   391  	return append([]string{}, src...)
   392  }
   393  
   394  func (cmd *DefaultCommand) copyExamples() []ExampleEntry {
   395  	ret := []ExampleEntry{}
   396  	if cmd.CmdExamples == nil || len(cmd.CmdExamples) == 0 {
   397  		return ret
   398  	}
   399  	for _, v := range cmd.CmdExamples {
   400  		ret = append(ret, v.Clone())
   401  	}
   402  	return ret
   403  }
   404  
   405  func (cmd *DefaultCommand) interpolateArray(values *[]string) {
   406  	for i := range *values {
   407  		(*values)[i] = cmd.interpolate((*values)[i])
   408  	}
   409  }
   410  
   411  func (cmd *DefaultCommand) interpolateCmd() string {
   412  	return cmd.interpolate(cmd.CmdExecutable)
   413  }
   414  
   415  func (cmd *DefaultCommand) binary(os string) string {
   416  	if os == "windows" {
   417  		return fmt.Sprintf("%s.exe", cmd.CmdName)
   418  	}
   419  	return cmd.CmdName
   420  }
   421  
   422  func (cmd *DefaultCommand) extension(os string) string {
   423  	if os == "windows" {
   424  		return ".exe"
   425  	}
   426  	return ""
   427  }
   428  
   429  func (cmd *DefaultCommand) script(os string) string {
   430  	return fmt.Sprintf("%s%s", cmd.CmdName, cmd.script_ext(os))
   431  }
   432  
   433  func (cmd *DefaultCommand) script_ext(os string) string {
   434  	if os == "windows" {
   435  		return ".bat"
   436  	}
   437  	return ""
   438  }
   439  
   440  func (cmd *DefaultCommand) interpolate(text string) string {
   441  	return cmd.doInterpolate(runtime.GOOS, runtime.GOARCH, text)
   442  }
   443  
   444  func (cmd *DefaultCommand) doInterpolate(os string, arch string, text string) string {
   445  	output := strings.ReplaceAll(text, CACHE_DIR_PATTERN, filepath.ToSlash(cmd.PkgDir))
   446  	output = strings.ReplaceAll(output, OS_PATTERN, os)
   447  	output = strings.ReplaceAll(output, ARCH_PATTERN, arch)
   448  	output = strings.ReplaceAll(output, BINARY_PATTERN, cmd.binary(os))
   449  	output = strings.ReplaceAll(output, EXT_PATTERN, cmd.extension(os))
   450  	output = strings.ReplaceAll(output, SCRIPT_PATTERN, cmd.script(os))
   451  	output = strings.ReplaceAll(output, SCRIPT_EXT_PATTERN, cmd.script_ext(os))
   452  	output = cmd.render(output)
   453  	return output
   454  }
   455  
   456  // Support golang built-in text/template engine
   457  type TemplateContext struct {
   458  	Os              string
   459  	Arch            string
   460  	Cache           string
   461  	Root            string
   462  	PackageDir      string
   463  	Binary          string
   464  	Script          string
   465  	Extension       string
   466  	ScriptExtension string
   467  }
   468  
   469  func (cmd *DefaultCommand) render(text string) string {
   470  	ctx := TemplateContext{
   471  		Os:              runtime.GOOS,
   472  		Arch:            runtime.GOARCH,
   473  		Cache:           filepath.ToSlash(cmd.PkgDir),
   474  		Root:            filepath.ToSlash(cmd.PkgDir),
   475  		PackageDir:      filepath.ToSlash(cmd.PkgDir),
   476  		Binary:          cmd.binary(runtime.GOOS),
   477  		Script:          cmd.script(runtime.GOOS),
   478  		Extension:       cmd.extension(runtime.GOOS),
   479  		ScriptExtension: cmd.script_ext(runtime.GOOS),
   480  	}
   481  
   482  	t, err := template.New("command-template").Parse(text)
   483  	if err != nil {
   484  		return text
   485  	}
   486  
   487  	builder := strings.Builder{}
   488  	err = t.Execute(&builder, ctx)
   489  	if err != nil {
   490  		return text
   491  	}
   492  
   493  	return builder.String()
   494  }