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  }