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

     1  package argparser
     2  
     3  import (
     4  	"encoding/json"
     5  	"errors"
     6  	"fmt"
     7  	"io"
     8  	"os"
     9  	"path/filepath"
    10  	"regexp"
    11  	"sort"
    12  	"strconv"
    13  	"strings"
    14  
    15  	"github.com/fastly/go-fastly/v9/fastly"
    16  	"github.com/fastly/kingpin"
    17  
    18  	"github.com/fastly/cli/pkg/api"
    19  	"github.com/fastly/cli/pkg/env"
    20  	fsterr "github.com/fastly/cli/pkg/errors"
    21  	"github.com/fastly/cli/pkg/text"
    22  )
    23  
    24  var (
    25  	completionRegExp       = regexp.MustCompile("completion-bash$")
    26  	completionScriptRegExp = regexp.MustCompile("completion-script-(?:bash|zsh)$")
    27  )
    28  
    29  // StringFlagOpts enables easy configuration of a flag.
    30  type StringFlagOpts struct {
    31  	Action      kingpin.Action
    32  	Description string
    33  	Dst         *string
    34  	Name        string
    35  	Required    bool
    36  	Short       rune
    37  }
    38  
    39  // RegisterFlag defines a flag.
    40  func (b Base) RegisterFlag(opts StringFlagOpts) {
    41  	clause := b.CmdClause.Flag(opts.Name, opts.Description)
    42  	if opts.Short > 0 {
    43  		clause = clause.Short(opts.Short)
    44  	}
    45  	if opts.Required {
    46  		clause = clause.Required()
    47  	}
    48  	if opts.Action != nil {
    49  		clause = clause.Action(opts.Action)
    50  	}
    51  	clause.StringVar(opts.Dst)
    52  }
    53  
    54  // BoolFlagOpts enables easy configuration of a flag.
    55  type BoolFlagOpts struct {
    56  	Action      kingpin.Action
    57  	Description string
    58  	Dst         *bool
    59  	Name        string
    60  	Required    bool
    61  	Short       rune
    62  }
    63  
    64  // RegisterFlagBool defines a boolean flag.
    65  //
    66  // TODO: Use generics support in go 1.18 to remove the need for multiple functions.
    67  func (b Base) RegisterFlagBool(opts BoolFlagOpts) {
    68  	clause := b.CmdClause.Flag(opts.Name, opts.Description)
    69  	if opts.Short > 0 {
    70  		clause = clause.Short(opts.Short)
    71  	}
    72  	if opts.Required {
    73  		clause = clause.Required()
    74  	}
    75  	if opts.Action != nil {
    76  		clause = clause.Action(opts.Action)
    77  	}
    78  	clause.BoolVar(opts.Dst)
    79  }
    80  
    81  // IntFlagOpts enables easy configuration of a flag.
    82  type IntFlagOpts struct {
    83  	Action      kingpin.Action
    84  	Default     int
    85  	Description string
    86  	Dst         *int
    87  	Name        string
    88  	Required    bool
    89  	Short       rune
    90  }
    91  
    92  // RegisterFlagInt defines an integer flag.
    93  func (b Base) RegisterFlagInt(opts IntFlagOpts) {
    94  	clause := b.CmdClause.Flag(opts.Name, opts.Description)
    95  	if opts.Short > 0 {
    96  		clause = clause.Short(opts.Short)
    97  	}
    98  	if opts.Required {
    99  		clause = clause.Required()
   100  	}
   101  	if opts.Action != nil {
   102  		clause = clause.Action(opts.Action)
   103  	}
   104  	if opts.Default != 0 {
   105  		clause = clause.Default(strconv.Itoa(opts.Default))
   106  	}
   107  	clause.IntVar(opts.Dst)
   108  }
   109  
   110  // OptionalServiceVersion represents a Fastly service version.
   111  type OptionalServiceVersion struct {
   112  	OptionalString
   113  }
   114  
   115  // Parse returns a service version based on the given user input.
   116  func (sv *OptionalServiceVersion) Parse(sid string, client api.Interface) (*fastly.Version, error) {
   117  	vs, err := client.ListVersions(&fastly.ListVersionsInput{
   118  		ServiceID: sid,
   119  	})
   120  	if err != nil {
   121  		return nil, fmt.Errorf("error listing service versions: %w", err)
   122  	}
   123  	if len(vs) == 0 {
   124  		return nil, errors.New("error listing service versions: no versions available")
   125  	}
   126  
   127  	// Sort versions into descending order.
   128  	sort.Slice(vs, func(i, j int) bool {
   129  		return fastly.ToValue(vs[i].Number) > fastly.ToValue(vs[j].Number)
   130  	})
   131  
   132  	var v *fastly.Version
   133  
   134  	switch strings.ToLower(sv.Value) {
   135  	case "latest":
   136  		return vs[0], nil
   137  	case "active":
   138  		v, err = GetActiveVersion(vs)
   139  	case "": // no --version flag provided
   140  		v, err = GetActiveVersion(vs)
   141  		if err != nil {
   142  			return vs[0], nil //lint:ignore nilerr if no active version, return latest version
   143  		}
   144  	default:
   145  		v, err = GetSpecifiedVersion(vs, sv.Value)
   146  	}
   147  	if err != nil {
   148  		return nil, err
   149  	}
   150  
   151  	return v, nil
   152  }
   153  
   154  // OptionalServiceNameID represents a mapping between a Fastly service name and
   155  // its ID.
   156  type OptionalServiceNameID struct {
   157  	OptionalString
   158  }
   159  
   160  // Parse returns a service ID based off the given service name.
   161  func (sv *OptionalServiceNameID) Parse(client api.Interface) (serviceID string, err error) {
   162  	paginator := client.GetServices(&fastly.GetServicesInput{})
   163  	var services []*fastly.Service
   164  	for paginator.HasNext() {
   165  		data, err := paginator.GetNext()
   166  		if err != nil {
   167  			return serviceID, fmt.Errorf("error listing services: %w", err)
   168  		}
   169  		services = append(services, data...)
   170  	}
   171  	for _, s := range services {
   172  		if fastly.ToValue(s.Name) == sv.Value {
   173  			return fastly.ToValue(s.ServiceID), nil
   174  		}
   175  	}
   176  	return serviceID, errors.New("error matching service name with available services")
   177  }
   178  
   179  // OptionalCustomerID represents a Fastly customer ID.
   180  type OptionalCustomerID struct {
   181  	OptionalString
   182  }
   183  
   184  // Parse returns a customer ID either from a flag or from a user defined
   185  // environment variable (see pkg/env/env.go).
   186  //
   187  // NOTE: Will fallback to FASTLY_CUSTOMER_ID environment variable if no flag value set.
   188  func (sv *OptionalCustomerID) Parse() error {
   189  	if sv.Value == "" {
   190  		if e := os.Getenv(env.CustomerID); e != "" {
   191  			sv.Value = e
   192  			return nil
   193  		}
   194  		return fsterr.ErrNoCustomerID
   195  	}
   196  	return nil
   197  }
   198  
   199  // AutoCloneFlagOpts enables easy configuration of the --autoclone flag defined
   200  // via the RegisterAutoCloneFlag constructor.
   201  type AutoCloneFlagOpts struct {
   202  	Action kingpin.Action
   203  	Dst    *bool
   204  }
   205  
   206  // RegisterAutoCloneFlag defines a --autoclone flag that will cause a clone of the
   207  // identified service version if it's found to be active or locked.
   208  func (b Base) RegisterAutoCloneFlag(opts AutoCloneFlagOpts) {
   209  	b.CmdClause.Flag("autoclone", "If the selected service version is not editable, clone it and use the clone.").Action(opts.Action).BoolVar(opts.Dst)
   210  }
   211  
   212  // OptionalAutoClone defines a method set for abstracting the logic required to
   213  // identify if a given service version needs to be cloned.
   214  type OptionalAutoClone struct {
   215  	OptionalBool
   216  }
   217  
   218  // Parse returns a service version.
   219  //
   220  // The returned version is either the same as the input argument `v` or it's a
   221  // cloned version if the input argument was either active or locked.
   222  func (ac *OptionalAutoClone) Parse(v *fastly.Version, sid string, verbose bool, out io.Writer, client api.Interface) (*fastly.Version, error) {
   223  	// if user didn't provide --autoclone flag
   224  	if !ac.Value && (fastly.ToValue(v.Active) || fastly.ToValue(v.Locked)) {
   225  		return nil, fsterr.RemediationError{
   226  			Inner:       fmt.Errorf("service version %d is not editable", fastly.ToValue(v.Number)),
   227  			Remediation: fsterr.AutoCloneRemediation,
   228  		}
   229  	}
   230  	if ac.Value && (v.Active != nil && *v.Active || v.Locked != nil && *v.Locked) {
   231  		version, err := client.CloneVersion(&fastly.CloneVersionInput{
   232  			ServiceID:      sid,
   233  			ServiceVersion: fastly.ToValue(v.Number),
   234  		})
   235  		if err != nil {
   236  			return nil, fmt.Errorf("error cloning service version: %w", err)
   237  		}
   238  		if verbose {
   239  			msg := "Service version %d is not editable, so it was automatically cloned because --autoclone is enabled. Now operating on version %d.\n\n"
   240  			format := fmt.Sprintf(msg, fastly.ToValue(v.Number), fastly.ToValue(version.Number))
   241  			text.Info(out, format)
   242  		}
   243  		return version, nil
   244  	}
   245  
   246  	// Treat the function as a no-op if the version is editable.
   247  	return v, nil
   248  }
   249  
   250  // GetActiveVersion returns the active service version.
   251  func GetActiveVersion(vs []*fastly.Version) (*fastly.Version, error) {
   252  	for _, v := range vs {
   253  		if fastly.ToValue(v.Active) {
   254  			return v, nil
   255  		}
   256  	}
   257  	return nil, fmt.Errorf("no active service version found")
   258  }
   259  
   260  // GetSpecifiedVersion returns the specified service version.
   261  func GetSpecifiedVersion(vs []*fastly.Version, version string) (*fastly.Version, error) {
   262  	i, err := strconv.Atoi(version)
   263  	if err != nil {
   264  		return nil, err
   265  	}
   266  
   267  	for _, v := range vs {
   268  		if fastly.ToValue(v.Number) == i {
   269  			return v, nil
   270  		}
   271  	}
   272  
   273  	return nil, fmt.Errorf("specified service version not found: %s", version)
   274  }
   275  
   276  // Content determines if the given flag value is a file path, and if so read
   277  // the contents from disk, otherwise presume the given value is the content.
   278  func Content(flagval string) string {
   279  	content := flagval
   280  	if path, err := filepath.Abs(flagval); err == nil {
   281  		if _, err := os.Stat(path); err == nil {
   282  			if data, err := os.ReadFile(path); err == nil /* #nosec */ {
   283  				content = string(data)
   284  			}
   285  		}
   286  	}
   287  	return content
   288  }
   289  
   290  // IntToBool converts a binary 0|1 to a boolean.
   291  func IntToBool(i int) bool {
   292  	return i > 0
   293  }
   294  
   295  // ContextHasHelpFlag asserts whether a given kingpin.ParseContext contains a
   296  // `help` flag.
   297  func ContextHasHelpFlag(ctx *kingpin.ParseContext) bool {
   298  	_, ok := ctx.Elements.FlagMap()["help"]
   299  	return ok
   300  }
   301  
   302  // IsCompletionScript determines whether the supplied command arguments are for
   303  // shell completion output that is then eval()'ed by the user's shell.
   304  func IsCompletionScript(args []string) bool {
   305  	var found bool
   306  	for _, arg := range args {
   307  		if completionScriptRegExp.MatchString(arg) {
   308  			found = true
   309  		}
   310  	}
   311  	return found
   312  }
   313  
   314  // IsCompletion determines whether the supplied command arguments are for
   315  // shell completion (i.e. --completion-bash) that should produce output that
   316  // the user's shell can utilise for handling autocomplete behaviour.
   317  func IsCompletion(args []string) bool {
   318  	var found bool
   319  	for _, arg := range args {
   320  		if completionRegExp.MatchString(arg) {
   321  			found = true
   322  		}
   323  	}
   324  	return found
   325  }
   326  
   327  // JSONOutput is a helper for adding a `--json` flag and encoding
   328  // values to JSON. It can be embedded into command structs.
   329  type JSONOutput struct {
   330  	Enabled bool // Set via flag.
   331  }
   332  
   333  // JSONFlag creates a flag for enabling JSON output.
   334  func (j *JSONOutput) JSONFlag() BoolFlagOpts {
   335  	return BoolFlagOpts{
   336  		Name:        FlagJSONName,
   337  		Description: FlagJSONDesc,
   338  		Dst:         &j.Enabled,
   339  		Short:       'j',
   340  	}
   341  }
   342  
   343  // WriteJSON checks whether the enabled flag is set or not. If set,
   344  // then the given value is written as JSON to out. Otherwise, false is returned.
   345  func (j *JSONOutput) WriteJSON(out io.Writer, value any) (bool, error) {
   346  	if !j.Enabled {
   347  		return false, nil
   348  	}
   349  
   350  	enc := json.NewEncoder(out)
   351  	enc.SetIndent("", "  ")
   352  	return true, enc.Encode(value)
   353  }