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 }