github.com/drycc/workflow-cli@v1.5.3-0.20240322092846-d4ee25983af9/drycc.go (about) 1 package main 2 3 import ( 4 "fmt" 5 "io" 6 "os" 7 "os/exec" 8 "strings" 9 "syscall" 10 11 docopt "github.com/docopt/docopt-go" 12 "github.com/drycc/workflow-cli/cli" 13 "github.com/drycc/workflow-cli/cmd" 14 "github.com/drycc/workflow-cli/parser" 15 ) 16 17 const extensionPrefix = "drycc-" 18 19 // main exits with the return value of Command(os.Args[1:]), deferring all logic to 20 // a func we can test. 21 func main() { 22 os.Exit(Command(os.Args[1:], os.Stdout, os.Stderr, os.Stdin)) 23 } 24 25 // Command routes drycc commands to their proper parser. 26 func Command(argv []string, wOut io.Writer, wErr io.Writer, wIn io.Reader) int { 27 usage := ` 28 The Drycc command-line client issues API calls to a Drycc controller. 29 30 Usage: drycc <command> [<args>...] 31 32 Options: 33 -h --help 34 display help information 35 -v --version 36 display client version 37 -c --config=<config> 38 path to configuration file. Equivalent to 39 setting $DRYCC_PROFILE. Defaults to ~/.drycc/config.json. 40 If value is not a filepath, will assume location ~/.drycc/client.json 41 42 Auth commands, use 'drycc help auth' to learn more: 43 44 register register a new user with a controller 45 login login to a controller 46 logout logout from the current controller 47 48 Subcommands, use 'drycc help [subcommand]' to learn more: 49 50 apps manage applications used to provide services 51 autoscale manage autoscale for applications 52 builds manage builds created using 'git push' 53 canary manage canary deploy for applications 54 certs manage SSL endpoints for an app 55 config manage environment variables that define app config 56 domains manage and assign domain names to your applications 57 git manage git for applications 58 healthchecks manage healthchecks for applications 59 keys manage ssh keys used for 'git push' deployments 60 labels manage labels of application 61 limits manage resource limits for your application 62 perms manage permissions for applications 63 ps manage processes inside an app container 64 registry manage private registry information for your application 65 releases manage releases of an application 66 routing manage routability of an application 67 tags manage tags for application containers 68 tls manage TLS settings for applications 69 users manage users 70 version display client version 71 services manage services for your applications 72 routes manage routes for your applications 73 gateways manage gateways for your applications 74 timeouts manage pods termination grace period 75 volumes manage volumes for your applications 76 resources manage resources for your applications 77 78 Shortcut commands, use 'drycc shortcuts' to see all: 79 80 create create a new application 81 destroy destroy an application 82 info view information about the current app 83 logs view aggregated log info for the app 84 open open a URL to the app in a browser 85 pull imports an image and deploys as a new release 86 run run a command in an ephemeral app container 87 scale scale processes by type (web=2, worker=1) 88 89 Use 'git push drycc main' to deploy to an application. 90 ` 91 // Reorganize some command line flags and commands. 92 command, argv := parseArgs(argv) 93 // Give docopt an optional final false arg so it doesn't call os.Exit(). 94 _, err := docopt.ParseArgs(usage, []string{command}, "") 95 96 if err != nil { 97 fmt.Fprintln(wErr, err) 98 return 1 99 } 100 101 if len(argv) == 0 { 102 fmt.Fprintln(wErr, "Usage: drycc <command> [<args>...]") 103 return 1 104 } 105 106 configFlag := getConfigFlag(argv) 107 // Don't pass down config flag to parser because it isn't defined there. 108 argv = removeConfigFlag(argv) 109 cmdr := cmd.DryccCmd{ConfigFile: configFlag, WOut: wOut, WErr: wErr, WIn: wIn} 110 111 // Dispatch the command, passing the argv through so subcommands can 112 // re-parse it according to their usage strings. 113 switch command { 114 case "apps": 115 err = parser.Apps(argv, &cmdr) 116 case "auth": 117 err = parser.Auth(argv, &cmdr) 118 case "autoscale": 119 err = parser.Autoscale(argv, &cmdr) 120 case "builds": 121 err = parser.Builds(argv, &cmdr) 122 case "canary": 123 err = parser.Canary(argv, &cmdr) 124 case "certs": 125 err = parser.Certs(argv, &cmdr) 126 case "config": 127 err = parser.Config(argv, &cmdr) 128 case "domains": 129 err = parser.Domains(argv, &cmdr) 130 case "services": 131 err = parser.Services(argv, &cmdr) 132 case "gateways": 133 err = parser.Gateways(argv, &cmdr) 134 case "routes": 135 err = parser.Routes(argv, &cmdr) 136 case "git": 137 err = parser.Git(argv, &cmdr) 138 case "healthchecks": 139 err = parser.Healthchecks(argv, &cmdr) 140 case "help": 141 fmt.Fprint(os.Stdout, usage) 142 return 0 143 case "keys": 144 err = parser.Keys(argv, &cmdr) 145 case "labels": 146 err = parser.Labels(argv, &cmdr) 147 case "limits": 148 err = parser.Limits(argv, &cmdr) 149 case "timeouts": 150 err = parser.Timeouts(argv, &cmdr) 151 case "perms": 152 err = parser.Perms(argv, &cmdr) 153 case "ps": 154 err = parser.Ps(argv, &cmdr) 155 case "registry": 156 err = parser.Registry(argv, &cmdr) 157 case "releases": 158 err = parser.Releases(argv, &cmdr) 159 case "routing": 160 err = parser.Routing(argv, &cmdr) 161 case "shortcuts": 162 err = parser.Shortcuts(argv, &cmdr) 163 case "tags": 164 err = parser.Tags(argv, &cmdr) 165 case "tls": 166 err = parser.TLS(argv, &cmdr) 167 case "users": 168 err = parser.Users(argv, &cmdr) 169 case "version": 170 err = parser.Version(argv, &cmdr) 171 case "volumes": 172 err = parser.Volumes(argv, &cmdr) 173 case "resources": 174 err = parser.Resources(argv, &cmdr) 175 default: 176 env := os.Environ() 177 178 binary, err := exec.LookPath(extensionPrefix + command) 179 if err != nil { 180 parser.PrintUsage(&cmdr) 181 return 1 182 } 183 184 cmdArgv := prepareCmdArgs(command, argv) 185 186 err = syscall.Exec(binary, cmdArgv, env) 187 if err != nil { 188 parser.PrintUsage(&cmdr) 189 return 1 190 } 191 } 192 if err != nil { 193 fmt.Fprintf(wErr, "Error: %v\n", err) 194 return 1 195 } 196 return 0 197 } 198 199 func removeConfigFlag(argv []string) []string { 200 var kept []string 201 for i, arg := range argv { 202 // -- /bin/sh -c --config condition 203 if arg == "--" { 204 kept = append(kept, argv[i:]...) 205 break 206 } 207 208 if arg == "-c" || strings.HasPrefix(arg, "--config=") { 209 continue 210 // If the previous option is -c, remove the argument as well 211 } else if i != 0 && argv[i-1] == "-c" { 212 continue 213 } 214 215 kept = append(kept, arg) 216 } 217 218 return kept 219 } 220 221 func getConfigFlag(argv []string) string { 222 for i, arg := range argv { 223 // -- /bin/sh -c/ --config= condition 224 if arg == "--" { 225 return "" 226 } 227 if strings.HasPrefix(arg, "--config=") { 228 return strings.TrimPrefix(arg, "--config=") 229 } else if i != 0 && argv[i-1] == "-c" { 230 return arg 231 } 232 } 233 234 return "" 235 } 236 237 // parseArgs returns the provided args with "--help" as the last arg if need be, 238 // expands shortcuts and formats commands to be properly routed. 239 func parseArgs(argv []string) (string, []string) { 240 if len(argv) == 1 { 241 if argv[0] == "--help" || argv[0] == "-h" { 242 // rearrange "drycc --help" as "drycc help" 243 argv[0] = "help" 244 } else if argv[0] == "--version" || argv[0] == "-v" { 245 // rearrange "drycc --version" as "drycc version" 246 argv[0] = "version" 247 } 248 } 249 250 if len(argv) > 1 { 251 // Rearrange "drycc help <command>" to "drycc <command> --help". 252 if argv[0] == "help" || argv[0] == "--help" || argv[0] == "-h" { 253 argv = append(argv[1:], "--help") 254 } 255 } 256 257 if len(argv) > 0 { 258 argv[0] = replaceShortcut(argv[0]) 259 260 index := strings.Index(argv[0], ":") 261 262 if index != -1 { 263 command := argv[0] 264 return command[:index], argv 265 } 266 267 return argv[0], argv 268 } 269 270 return "", argv 271 } 272 273 // split original command and pass its first element in arguments 274 func prepareCmdArgs(command string, argv []string) []string { 275 cmdArgv := []string{extensionPrefix + command} 276 cmdSplit := strings.Split(argv[0], command+":") 277 278 if len(cmdSplit) > 1 { 279 cmdArgv = append(cmdArgv, cmdSplit[1]) 280 } 281 282 return append(cmdArgv, argv[1:]...) 283 } 284 285 func replaceShortcut(command string) string { 286 expandedCommand := cli.Shortcuts[command] 287 if expandedCommand == "" { 288 return command 289 } 290 291 return expandedCommand 292 }