github.com/fastly/cli@v1.7.2-0.20240304164155-9d0f1d77c3bf/pkg/argparser/cmd.go (about)

     1  package argparser
     2  
     3  import (
     4  	"fmt"
     5  	"io"
     6  
     7  	"github.com/fastly/go-fastly/v9/fastly"
     8  	"github.com/fastly/kingpin"
     9  
    10  	"github.com/fastly/cli/pkg/api"
    11  	"github.com/fastly/cli/pkg/env"
    12  	fsterr "github.com/fastly/cli/pkg/errors"
    13  	"github.com/fastly/cli/pkg/global"
    14  	"github.com/fastly/cli/pkg/manifest"
    15  	"github.com/fastly/cli/pkg/text"
    16  )
    17  
    18  // Command is an interface that abstracts over all of the concrete command
    19  // structs. The Name method lets us select which command should be run, and the
    20  // Exec method invokes whatever business logic the command should do.
    21  type Command interface {
    22  	Name() string
    23  	Exec(in io.Reader, out io.Writer) error
    24  }
    25  
    26  // Select chooses the command matching name, if it exists.
    27  func Select(name string, commands []Command) (Command, bool) {
    28  	for _, command := range commands {
    29  		if command.Name() == name {
    30  			return command, true
    31  		}
    32  	}
    33  	return nil, false
    34  }
    35  
    36  // Registerer abstracts over a kingpin.App and kingpin.CmdClause. We pass it to
    37  // each concrete command struct's constructor as the "parent" into which the
    38  // command should install itself.
    39  type Registerer interface {
    40  	Command(name, help string) *kingpin.CmdClause
    41  }
    42  
    43  // Globals are flags and other stuff that's useful to every command. Globals are
    44  // passed to each concrete command's constructor as a pointer, and are populated
    45  // after a call to Parse. A concrete command's Exec method can use any of the
    46  // information in the globals.
    47  type Globals struct {
    48  	Token   string
    49  	Verbose bool
    50  	Client  api.Interface
    51  }
    52  
    53  // Base is stuff that should be included in every concrete command.
    54  type Base struct {
    55  	CmdClause *kingpin.CmdClause
    56  	Globals   *global.Data
    57  }
    58  
    59  // Name implements the Command interface, and returns the FullCommand from the
    60  // kingpin.Command that's used to select which command to actually run.
    61  func (b Base) Name() string {
    62  	return b.CmdClause.FullCommand()
    63  }
    64  
    65  // Optional models an optional type that consumers can use to assert whether the
    66  // inner value has been set and is therefore valid for use.
    67  type Optional struct {
    68  	WasSet bool
    69  }
    70  
    71  // Set implements kingpin.Action and is used as callback to set that the optional
    72  // inner value is valid.
    73  func (o *Optional) Set(_ *kingpin.ParseElement, _ *kingpin.ParseContext) error {
    74  	o.WasSet = true
    75  	return nil
    76  }
    77  
    78  // OptionalString models an optional string flag value.
    79  type OptionalString struct {
    80  	Optional
    81  	Value string
    82  }
    83  
    84  // OptionalStringSlice models an optional string slice flag value.
    85  type OptionalStringSlice struct {
    86  	Optional
    87  	Value []string
    88  }
    89  
    90  // OptionalBool models an optional boolean flag value.
    91  type OptionalBool struct {
    92  	Optional
    93  	Value bool
    94  }
    95  
    96  // OptionalInt models an optional int flag value.
    97  type OptionalInt struct {
    98  	Optional
    99  	Value int
   100  }
   101  
   102  // ServiceDetailsOpts provides data and behaviours required by the
   103  // ServiceDetails function.
   104  type ServiceDetailsOpts struct {
   105  	AllowActiveLocked  bool
   106  	AutoCloneFlag      OptionalAutoClone
   107  	APIClient          api.Interface
   108  	Manifest           manifest.Data
   109  	Out                io.Writer
   110  	ServiceNameFlag    OptionalServiceNameID
   111  	ServiceVersionFlag OptionalServiceVersion
   112  	VerboseMode        bool
   113  	ErrLog             fsterr.LogInterface
   114  }
   115  
   116  // ServiceDetails returns the Service ID and Service Version.
   117  func ServiceDetails(opts ServiceDetailsOpts) (serviceID string, serviceVersion *fastly.Version, err error) {
   118  	serviceID, source, flag, err := ServiceID(opts.ServiceNameFlag, opts.Manifest, opts.APIClient, opts.ErrLog)
   119  	if err != nil {
   120  		return serviceID, serviceVersion, err
   121  	}
   122  	if opts.VerboseMode {
   123  		DisplayServiceID(serviceID, flag, source, opts.Out)
   124  	}
   125  
   126  	v, err := opts.ServiceVersionFlag.Parse(serviceID, opts.APIClient)
   127  	if err != nil {
   128  		return serviceID, serviceVersion, err
   129  	}
   130  
   131  	if opts.AutoCloneFlag.WasSet {
   132  		currentVersion := v
   133  		v, err = opts.AutoCloneFlag.Parse(currentVersion, serviceID, opts.VerboseMode, opts.Out, opts.APIClient)
   134  		if err != nil {
   135  			return serviceID, currentVersion, err
   136  		}
   137  	} else if !opts.AllowActiveLocked && (fastly.ToValue(v.Active) || fastly.ToValue(v.Locked)) {
   138  		err = fsterr.RemediationError{
   139  			Inner:       fmt.Errorf("service version %d is not editable", fastly.ToValue(v.Number)),
   140  			Remediation: fsterr.AutoCloneRemediation,
   141  		}
   142  		return serviceID, v, err
   143  	}
   144  
   145  	return serviceID, v, nil
   146  }
   147  
   148  // ServiceID returns the Service ID and the source of that information.
   149  //
   150  // NOTE: If Service ID not provided then check if Service Name provided and use
   151  // that information to acquire the Service ID.
   152  func ServiceID(serviceName OptionalServiceNameID, data manifest.Data, client api.Interface, li fsterr.LogInterface) (serviceID string, source manifest.Source, flag string, err error) {
   153  	flag = "--service-id"
   154  	serviceID, source = data.ServiceID()
   155  
   156  	if source == manifest.SourceUndefined {
   157  		if !serviceName.WasSet {
   158  			err = fsterr.ErrNoServiceID
   159  			if li != nil {
   160  				li.Add(err)
   161  			}
   162  			return serviceID, source, flag, err
   163  		}
   164  
   165  		flag = "--service-name"
   166  		serviceID, err = serviceName.Parse(client)
   167  		if err != nil {
   168  			if li != nil {
   169  				li.Add(err)
   170  			}
   171  		} else {
   172  			source = manifest.SourceFlag
   173  		}
   174  	}
   175  
   176  	return serviceID, source, flag, err
   177  }
   178  
   179  // DisplayServiceID acquires the Service ID (if provided) and displays both it
   180  // and its source location.
   181  func DisplayServiceID(sid, flag string, s manifest.Source, out io.Writer) {
   182  	var via string
   183  	switch s {
   184  	case manifest.SourceFlag:
   185  		via = fmt.Sprintf(" (via %s)", flag)
   186  	case manifest.SourceFile:
   187  		via = fmt.Sprintf(" (via %s)", manifest.Filename)
   188  	case manifest.SourceEnv:
   189  		via = fmt.Sprintf(" (via %s)", env.ServiceID)
   190  	case manifest.SourceUndefined:
   191  		via = " (not provided)"
   192  	}
   193  	text.Output(out, "Service ID%s: %s", via, sid)
   194  	text.Break(out)
   195  }
   196  
   197  // ArgsIsHelpJSON determines whether the supplied command arguments are exactly
   198  // `help --format=json` or `help --format json`.
   199  func ArgsIsHelpJSON(args []string) bool {
   200  	switch len(args) {
   201  	case 2:
   202  		if args[0] == "help" && args[1] == "--format=json" {
   203  			return true
   204  		}
   205  	case 3:
   206  		if args[0] == "help" && args[1] == "--format" && args[2] == "json" {
   207  			return true
   208  		}
   209  	}
   210  	return false
   211  }
   212  
   213  // IsHelpOnly indicates if the user called `fastly help [...]`.
   214  func IsHelpOnly(args []string) bool {
   215  	return len(args) > 0 && args[0] == "help"
   216  }
   217  
   218  // IsHelpFlagOnly indicates if the user called `fastly --help [...]`.
   219  func IsHelpFlagOnly(args []string) bool {
   220  	return len(args) > 0 && args[0] == "--help"
   221  }
   222  
   223  // IsVerboseAndQuiet indicates if the user called `fastly --verbose --quiet`.
   224  // These flags are mutually exclusive.
   225  func IsVerboseAndQuiet(args []string) bool {
   226  	matches := map[string]bool{}
   227  	for _, a := range args {
   228  		if a == "--verbose" || a == "-v" {
   229  			matches["--verbose"] = true
   230  		}
   231  		if a == "--quiet" || a == "-q" {
   232  			matches["--quiet"] = true
   233  		}
   234  	}
   235  	return len(matches) > 1
   236  }
   237  
   238  // IsGlobalFlagsOnly indicates if the user called the binary with any
   239  // permutation order of the globally defined flags.
   240  //
   241  // NOTE: Some global flags accept a value while others do not. The following
   242  // algorithm takes this into account by mapping the flag to an expected value.
   243  // For example, --verbose doesn't accept a value so is set to zero.
   244  //
   245  // EXAMPLES:
   246  //
   247  // The following would return false as a command was specified:
   248  //
   249  // args: [--verbose -v --endpoint ... --token ... -t ... --endpoint ...  version] 11
   250  // total: 10
   251  //
   252  // The following would return true as only global flags were specified:
   253  //
   254  // args: [--verbose -v --endpoint ... --token ... -t ... --endpoint ...] 10
   255  // total: 10
   256  //
   257  // IMPORTANT: Kingpin doesn't support global flags.
   258  // We hack a solution in ../app/run.go (`configureKingpin` function).
   259  func IsGlobalFlagsOnly(args []string) bool {
   260  	// Global flags are defined in ../app/run.go
   261  	// False positive https://github.com/semgrep/semgrep/issues/8593
   262  	// nosemgrep: trailofbits.go.iterate-over-empty-map.iterate-over-empty-map
   263  	globals := map[string]int{
   264  		"--accept-defaults": 0,
   265  		"-d":                0,
   266  		"--account":         1,
   267  		"--api":             1,
   268  		"--auto-yes":        0,
   269  		"-y":                0,
   270  		"--debug-mode":      0,
   271  		"--enable-sso":      0,
   272  		"--help":            0,
   273  		"--non-interactive": 0,
   274  		"-i":                0,
   275  		"--profile":         1,
   276  		"-o":                1,
   277  		"--quiet":           0,
   278  		"-q":                0,
   279  		"--token":           1,
   280  		"-t":                1,
   281  		"--verbose":         0,
   282  		"-v":                0,
   283  	}
   284  	var total int
   285  	for _, a := range args {
   286  		for k := range globals {
   287  			if a == k {
   288  				total++
   289  				total += globals[k]
   290  			}
   291  		}
   292  	}
   293  	return len(args) == total
   294  }