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 }