github.com/jenspinney/cli@v6.42.1-0.20190207184520-7450c600020e+incompatible/command/v6/push_command.go (about)

     1  package v6
     2  
     3  import (
     4  	"os"
     5  	"path/filepath"
     6  
     7  	"code.cloudfoundry.org/cli/actor/pushaction"
     8  	"code.cloudfoundry.org/cli/actor/sharedaction"
     9  	"code.cloudfoundry.org/cli/actor/v2action"
    10  	"code.cloudfoundry.org/cli/actor/v2v3action"
    11  	"code.cloudfoundry.org/cli/actor/v3action"
    12  	"code.cloudfoundry.org/cli/command"
    13  	"code.cloudfoundry.org/cli/command/flag"
    14  	"code.cloudfoundry.org/cli/command/translatableerror"
    15  	"code.cloudfoundry.org/cli/command/v6/shared"
    16  	sharedV3 "code.cloudfoundry.org/cli/command/v6/shared"
    17  	"code.cloudfoundry.org/cli/util/configv3"
    18  	"code.cloudfoundry.org/cli/util/manifest"
    19  	"code.cloudfoundry.org/cli/util/progressbar"
    20  	"github.com/cloudfoundry/bosh-cli/director/template"
    21  	"github.com/cloudfoundry/noaa/consumer"
    22  	log "github.com/sirupsen/logrus"
    23  )
    24  
    25  //go:generate counterfeiter . ProgressBar
    26  
    27  type ProgressBar interface {
    28  	pushaction.ProgressBar
    29  	Complete()
    30  	Ready()
    31  }
    32  
    33  //go:generate counterfeiter . V2PushActor
    34  
    35  type V2PushActor interface {
    36  	Apply(config pushaction.ApplicationConfig, progressBar pushaction.ProgressBar) (<-chan pushaction.ApplicationConfig, <-chan pushaction.Event, <-chan pushaction.Warnings, <-chan error)
    37  	CloudControllerV2APIVersion() string
    38  	CloudControllerV3APIVersion() string
    39  	ConvertToApplicationConfigs(orgGUID string, spaceGUID string, noStart bool, apps []manifest.Application) ([]pushaction.ApplicationConfig, pushaction.Warnings, error)
    40  	MergeAndValidateSettingsAndManifests(cmdSettings pushaction.CommandLineSettings, apps []manifest.Application) ([]manifest.Application, error)
    41  	ReadManifest(pathToManifest string, pathsToVarsFiles []string, vars []template.VarKV) ([]manifest.Application, pushaction.Warnings, error)
    42  }
    43  
    44  type PushCommand struct {
    45  	OptionalArgs        flag.OptionalAppName                    `positional-args:"yes"`
    46  	Buildpacks          []string                                `short:"b" description:"Custom buildpack by name (e.g. my-buildpack) or Git URL (e.g. 'https://github.com/cloudfoundry/java-buildpack.git') or Git URL with a branch or tag (e.g. 'https://github.com/cloudfoundry/java-buildpack.git#v3.3.0' for 'v3.3.0' tag). To use built-in buildpacks only, specify 'default' or 'null'"`
    47  	Command             flag.Command                            `short:"c" description:"Startup command, set to null to reset to default start command"`
    48  	Domain              string                                  `short:"d" description:"Specify a custom domain (e.g. private-domain.example.com, apps.internal.com) to use instead of the default domain"`
    49  	DockerImage         flag.DockerImage                        `long:"docker-image" short:"o" description:"Docker-image to be used (e.g. user/docker-image-name)"`
    50  	DockerUsername      string                                  `long:"docker-username" description:"Repository username; used with password from environment variable CF_DOCKER_PASSWORD"`
    51  	DropletPath         flag.PathWithExistenceCheck             `long:"droplet" description:"Path to a tgz file with a pre-staged app"`
    52  	PathToManifest      flag.PathWithExistenceCheck             `short:"f" description:"Path to manifest"`
    53  	HealthCheckType     flag.HealthCheckTypeWithDeprecatedValue `long:"health-check-type" short:"u" description:"Application health check type (Default: 'port', 'none' accepted for 'process', 'http' implies endpoint '/')"`
    54  	Hostname            string                                  `long:"hostname" short:"n" description:"Hostname (e.g. my-subdomain)"`
    55  	Instances           flag.Instances                          `short:"i" description:"Number of instances"`
    56  	DiskQuota           flag.Megabytes                          `short:"k" description:"Disk limit (e.g. 256M, 1024M, 1G)"`
    57  	Memory              flag.Megabytes                          `short:"m" description:"Memory limit (e.g. 256M, 1024M, 1G)"`
    58  	NoHostname          bool                                    `long:"no-hostname" description:"Map the root domain to this app"`
    59  	NoManifest          bool                                    `long:"no-manifest" description:"Ignore manifest file"`
    60  	NoRoute             bool                                    `long:"no-route" description:"Do not map a route to this app and remove routes from previous pushes of this app"`
    61  	NoStart             bool                                    `long:"no-start" description:"Do not start an app after pushing"`
    62  	AppPath             flag.PathWithExistenceCheck             `short:"p" description:"Path to app directory or to a zip file of the contents of the app directory"`
    63  	RandomRoute         bool                                    `long:"random-route" description:"Create a random route for this app"`
    64  	RoutePath           flag.RoutePath                          `long:"route-path" description:"Path for the route"`
    65  	StackName           string                                  `short:"s" description:"Stack to use (a stack is a pre-built file system, including an operating system, that can run apps)"`
    66  	VarsFilePaths       []flag.PathWithExistenceCheck           `long:"vars-file" description:"Path to a variable substitution file for manifest; can specify multiple times"`
    67  	Vars                []template.VarKV                        `long:"var" description:"Variable key value pair for variable substitution, (e.g., name=app1); can specify multiple times"`
    68  	HealthCheckTimeout  uint64                                  `short:"t" description:"Time (in seconds) allowed to elapse between starting up an app and the first healthy response from the app"`
    69  	envCFStagingTimeout interface{}                             `environmentName:"CF_STAGING_TIMEOUT" environmentDescription:"Max wait time for buildpack staging, in minutes" environmentDefault:"15"`
    70  	envCFStartupTimeout interface{}                             `environmentName:"CF_STARTUP_TIMEOUT" environmentDescription:"Max wait time for app instance startup, in minutes" environmentDefault:"5"`
    71  	dockerPassword      interface{}                             `environmentName:"CF_DOCKER_PASSWORD" environmentDescription:"Password used for private docker repository"`
    72  
    73  	usage           interface{} `usage:"CF_NAME push APP_NAME [-b BUILDPACK_NAME] [-c COMMAND] [-f MANIFEST_PATH | --no-manifest] [--no-start]\n   [-i NUM_INSTANCES] [-k DISK] [-m MEMORY] [-p PATH] [-s STACK] [-t HEALTH_TIMEOUT] [-u (process | port | http)]\n   [--no-route | --random-route | --hostname HOST | --no-hostname] [-d DOMAIN] [--route-path ROUTE_PATH] [--var KEY=VALUE]... [--vars-file VARS_FILE_PATH]...\n\n   CF_NAME push APP_NAME --docker-image [REGISTRY_HOST:PORT/]IMAGE[:TAG] [--docker-username USERNAME]\n   [-c COMMAND] [-f MANIFEST_PATH | --no-manifest] [--no-start]\n   [-i NUM_INSTANCES] [-k DISK] [-m MEMORY] [-t HEALTH_TIMEOUT] [-u (process | port | http)]\n   [--no-route | --random-route | --hostname HOST | --no-hostname] [-d DOMAIN] [--route-path ROUTE_PATH] [--var KEY=VALUE]... [--vars-file VARS_FILE_PATH]...\n\n   CF_NAME push APP_NAME --droplet DROPLET_PATH\n   [-c COMMAND] [-f MANIFEST_PATH | --no-manifest] [--no-start]\n   [-i NUM_INSTANCES] [-k DISK] [-m MEMORY] [-t HEALTH_TIMEOUT] [-u (process | port | http)]\n   [--no-route | --random-route | --hostname HOST | --no-hostname] [-d DOMAIN] [--route-path ROUTE_PATH] [--var KEY=VALUE]... [--vars-file VARS_FILE_PATH]...\n\n   CF_NAME push -f MANIFEST_WITH_MULTIPLE_APPS_PATH [APP_NAME] [--no-start]"`
    74  	relatedCommands interface{} `related_commands:"apps, create-app-manifest, logs, ssh, start"`
    75  
    76  	UI                      command.UI
    77  	Config                  command.Config
    78  	SharedActor             command.SharedActor
    79  	Actor                   V2PushActor
    80  	ApplicationSummaryActor shared.ApplicationSummaryActor
    81  	ProgressBar             ProgressBar
    82  
    83  	RestartActor RestartActor
    84  	NOAAClient   *consumer.Consumer
    85  }
    86  
    87  func (cmd *PushCommand) Setup(config command.Config, ui command.UI) error {
    88  	cmd.UI = ui
    89  	cmd.Config = config
    90  	sharedActor := sharedaction.NewActor(config)
    91  	cmd.SharedActor = sharedActor
    92  
    93  	ccClient, uaaClient, err := shared.NewClients(config, ui, true)
    94  	if err != nil {
    95  		return err
    96  	}
    97  
    98  	ccClientV3, _, err := sharedV3.NewV3BasedClients(config, ui, true, "")
    99  	if err != nil {
   100  		return err
   101  	}
   102  
   103  	v2Actor := v2action.NewActor(ccClient, uaaClient, config)
   104  	v3Actor := v3action.NewActor(ccClientV3, config, sharedActor, nil)
   105  
   106  	cmd.RestartActor = v2Actor
   107  	cmd.Actor = pushaction.NewActor(v2Actor, v3Actor, sharedActor)
   108  
   109  	cmd.ApplicationSummaryActor = v2v3action.NewActor(v2Actor, v3Actor)
   110  
   111  	cmd.NOAAClient = shared.NewNOAAClient(ccClient.DopplerEndpoint(), config, uaaClient, ui)
   112  
   113  	cmd.ProgressBar = progressbar.NewProgressBar()
   114  	return nil
   115  }
   116  
   117  func (cmd PushCommand) Execute(args []string) error {
   118  	err := cmd.SharedActor.CheckTarget(true, true)
   119  	if err != nil {
   120  		return err
   121  	}
   122  
   123  	user, err := cmd.Config.CurrentUser()
   124  	if err != nil {
   125  		return err
   126  	}
   127  
   128  	log.Info("collating flags")
   129  	cliSettings, err := cmd.GetCommandLineSettings()
   130  	if err != nil {
   131  		log.Errorln("reading flags:", err)
   132  		return err
   133  	}
   134  
   135  	log.Info("checking manifest")
   136  	rawApps, err := cmd.findAndReadManifestWithFlavorText(cliSettings)
   137  	if err != nil {
   138  		log.Errorln("reading manifest:", err)
   139  		return err
   140  	}
   141  
   142  	log.Info("merging manifest and command flags")
   143  	manifestApplications, err := cmd.Actor.MergeAndValidateSettingsAndManifests(cliSettings, rawApps)
   144  	if err != nil {
   145  		log.Errorln("merging manifest:", err)
   146  		return err
   147  	}
   148  
   149  	cmd.UI.DisplayText("Getting app info...")
   150  
   151  	log.Info("converting manifests to ApplicationConfigs")
   152  	appConfigs, warnings, err := cmd.Actor.ConvertToApplicationConfigs(
   153  		cmd.Config.TargetedOrganization().GUID,
   154  		cmd.Config.TargetedSpace().GUID,
   155  		cmd.NoStart,
   156  		manifestApplications,
   157  	)
   158  	cmd.UI.DisplayWarnings(warnings)
   159  	if err != nil {
   160  		log.Errorln("converting manifest:", err)
   161  		return err
   162  	}
   163  
   164  	for _, appConfig := range appConfigs {
   165  		if appConfig.CreatingApplication() {
   166  			cmd.UI.DisplayText("Creating app with these attributes...")
   167  		} else {
   168  			cmd.UI.DisplayText("Updating app with these attributes...")
   169  		}
   170  		log.Infoln("starting create/update:", appConfig.DesiredApplication.Name)
   171  		changes := shared.GetApplicationChanges(appConfig)
   172  		err := cmd.UI.DisplayChangesForPush(changes)
   173  		if err != nil {
   174  			log.Errorln("display changes:", err)
   175  			return err
   176  		}
   177  		cmd.UI.DisplayNewline()
   178  	}
   179  
   180  	for appNumber, appConfig := range appConfigs {
   181  		if appConfig.CreatingApplication() {
   182  			cmd.UI.DisplayTextWithFlavor("Creating app {{.AppName}}...", map[string]interface{}{
   183  				"AppName": appConfig.DesiredApplication.Name,
   184  			})
   185  		} else {
   186  			cmd.UI.DisplayTextWithFlavor("Updating app {{.AppName}}...", map[string]interface{}{
   187  				"AppName": appConfig.DesiredApplication.Name,
   188  			})
   189  		}
   190  
   191  		configStream, eventStream, warningsStream, errorStream := cmd.Actor.Apply(appConfig, cmd.ProgressBar)
   192  		updatedConfig, err := cmd.processApplyStreams(user, appConfig, configStream, eventStream, warningsStream, errorStream)
   193  		if err != nil {
   194  			log.Errorln("process apply stream:", err)
   195  			return err
   196  		}
   197  
   198  		if !cmd.NoStart {
   199  			messages, logErrs, appState, apiWarnings, errs := cmd.RestartActor.RestartApplication(updatedConfig.CurrentApplication.Application, cmd.NOAAClient)
   200  			err = shared.PollStart(cmd.UI, cmd.Config, messages, logErrs, appState, apiWarnings, errs)
   201  			if err != nil {
   202  				return err
   203  			}
   204  		}
   205  
   206  		cmd.UI.DisplayNewline()
   207  
   208  		log.WithField("v3_api_version", cmd.ApplicationSummaryActor.CloudControllerV3APIVersion()).Debug("using v3 for app display")
   209  		appSummary, warnings, err := cmd.ApplicationSummaryActor.GetApplicationSummaryByNameAndSpace(appConfig.DesiredApplication.Name, cmd.Config.TargetedSpace().GUID, true)
   210  		cmd.UI.DisplayWarnings(warnings)
   211  		if err != nil {
   212  			return err
   213  		}
   214  		shared.NewAppSummaryDisplayer2(cmd.UI).AppDisplay(appSummary, true)
   215  
   216  		if appNumber+1 <= len(appConfigs) {
   217  			cmd.UI.DisplayNewline()
   218  		}
   219  	}
   220  
   221  	return nil
   222  }
   223  
   224  // GetCommandLineSettings generates a push CommandLineSettings object from the
   225  // command's command line flags. It also validates those settings, preventing
   226  // contradictory flags.
   227  func (cmd PushCommand) GetCommandLineSettings() (pushaction.CommandLineSettings, error) {
   228  	err := cmd.validateArgs()
   229  	if err != nil {
   230  		return pushaction.CommandLineSettings{}, err
   231  	}
   232  
   233  	pwd, err := os.Getwd()
   234  	if err != nil {
   235  		return pushaction.CommandLineSettings{}, err
   236  	}
   237  
   238  	dockerPassword := cmd.Config.DockerPassword()
   239  	if dockerPassword != "" {
   240  		cmd.UI.DisplayText("Using docker repository password from environment variable CF_DOCKER_PASSWORD.")
   241  	} else if cmd.DockerUsername != "" {
   242  		cmd.UI.DisplayText("Environment variable CF_DOCKER_PASSWORD not set.")
   243  		dockerPassword, err = cmd.UI.DisplayPasswordPrompt("Docker password")
   244  		if err != nil {
   245  			return pushaction.CommandLineSettings{}, err
   246  		}
   247  	}
   248  
   249  	config := pushaction.CommandLineSettings{
   250  		Buildpacks:           cmd.Buildpacks,             // -b
   251  		Command:              cmd.Command.FilteredString, // -c
   252  		CurrentDirectory:     pwd,
   253  		DefaultRouteDomain:   cmd.Domain,               // -d
   254  		DefaultRouteHostname: cmd.Hostname,             // -n/--hostname
   255  		DiskQuota:            cmd.DiskQuota.Value,      // -k
   256  		DockerImage:          cmd.DockerImage.Path,     // -o
   257  		DockerPassword:       dockerPassword,           // ENV - CF_DOCKER_PASSWORD
   258  		DockerUsername:       cmd.DockerUsername,       // --docker-username
   259  		DropletPath:          string(cmd.DropletPath),  // --droplet
   260  		HealthCheckTimeout:   cmd.HealthCheckTimeout,   // -t
   261  		HealthCheckType:      cmd.HealthCheckType.Type, // -u/--health-check-type
   262  		Instances:            cmd.Instances.NullInt,    // -i
   263  		Memory:               cmd.Memory.Value,         // -m
   264  		Name:                 cmd.OptionalArgs.AppName, // arg
   265  		NoHostname:           cmd.NoHostname,           // --no-hostname
   266  		NoRoute:              cmd.NoRoute,              // --no-route
   267  		ProvidedAppPath:      string(cmd.AppPath),      // -p
   268  		RandomRoute:          cmd.RandomRoute,          // --random-route
   269  		RoutePath:            cmd.RoutePath.Path,       // --route-path
   270  		StackName:            cmd.StackName,            // -s
   271  	}
   272  
   273  	log.Debugln("Command Line Settings:", config)
   274  	return config, nil
   275  }
   276  
   277  func (cmd PushCommand) findAndReadManifestWithFlavorText(settings pushaction.CommandLineSettings) ([]manifest.Application, error) {
   278  	var (
   279  		pathToManifest string
   280  	)
   281  	switch {
   282  	case cmd.NoManifest:
   283  		log.Debug("skipping reading of manifest")
   284  	case cmd.PathToManifest != "":
   285  		log.WithField("file", cmd.PathToManifest).Debug("using specified manifest file")
   286  		pathToManifest = string(cmd.PathToManifest)
   287  
   288  		fileInfo, err := os.Stat(pathToManifest)
   289  		if err != nil {
   290  			return nil, err
   291  		}
   292  
   293  		if fileInfo.IsDir() {
   294  			manifestPaths := []string{
   295  				filepath.Join(pathToManifest, "manifest.yml"),
   296  				filepath.Join(pathToManifest, "manifest.yaml"),
   297  			}
   298  			for _, manifestPath := range manifestPaths {
   299  				if _, err = os.Stat(manifestPath); err == nil {
   300  					pathToManifest = manifestPath
   301  					break
   302  				}
   303  			}
   304  		}
   305  
   306  		if err != nil {
   307  			return nil, translatableerror.ManifestFileNotFoundInDirectoryError{
   308  				PathToManifest: pathToManifest,
   309  			}
   310  		}
   311  	default:
   312  		log.Debug("searching for manifest file")
   313  		pathToManifest = filepath.Join(settings.CurrentDirectory, "manifest.yml")
   314  		if _, err := os.Stat(pathToManifest); os.IsNotExist(err) {
   315  			log.WithField("pathToManifest", pathToManifest).Debug("could not find")
   316  
   317  			// While this is unlikely to be used, it is kept for backwards
   318  			// compatibility.
   319  			pathToManifest = filepath.Join(settings.CurrentDirectory, "manifest.yaml")
   320  			if _, err := os.Stat(pathToManifest); os.IsNotExist(err) {
   321  				log.WithField("pathToManifest", pathToManifest).Debug("could not find")
   322  				pathToManifest = ""
   323  			}
   324  		}
   325  	}
   326  
   327  	user, err := cmd.Config.CurrentUser()
   328  	if err != nil {
   329  		return nil, err
   330  	}
   331  
   332  	if pathToManifest == "" {
   333  		cmd.UI.DisplayTextWithFlavor("Pushing app {{.AppName}} to org {{.OrgName}} / space {{.SpaceName}} as {{.Username}}...", map[string]interface{}{
   334  			"AppName":   settings.Name,
   335  			"OrgName":   cmd.Config.TargetedOrganization().Name,
   336  			"SpaceName": cmd.Config.TargetedSpace().Name,
   337  			"Username":  user.Name,
   338  		})
   339  		return nil, nil
   340  	}
   341  
   342  	var pathsToVarsFiles []string
   343  	for _, path := range cmd.VarsFilePaths {
   344  		pathsToVarsFiles = append(pathsToVarsFiles, string(path))
   345  	}
   346  
   347  	cmd.UI.DisplayTextWithFlavor("Pushing from manifest to org {{.OrgName}} / space {{.SpaceName}} as {{.Username}}...", map[string]interface{}{
   348  		"OrgName":   cmd.Config.TargetedOrganization().Name,
   349  		"SpaceName": cmd.Config.TargetedSpace().Name,
   350  		"Username":  user.Name,
   351  	})
   352  	log.WithField("pathToManifest", pathToManifest).Info("reading manifest")
   353  	cmd.UI.DisplayText("Using manifest file {{.Path}}", map[string]interface{}{
   354  		"Path": pathToManifest,
   355  	})
   356  
   357  	apps, warnings, err := cmd.Actor.ReadManifest(pathToManifest, pathsToVarsFiles, cmd.Vars)
   358  	cmd.UI.DisplayWarnings(warnings)
   359  
   360  	return apps, err
   361  }
   362  
   363  func (cmd PushCommand) processApplyStreams(
   364  	user configv3.User,
   365  	appConfig pushaction.ApplicationConfig,
   366  	configStream <-chan pushaction.ApplicationConfig,
   367  	eventStream <-chan pushaction.Event,
   368  	warningsStream <-chan pushaction.Warnings,
   369  	errorStream <-chan error,
   370  ) (pushaction.ApplicationConfig, error) {
   371  	var configClosed, eventClosed, warningsClosed, complete bool
   372  	var updatedConfig pushaction.ApplicationConfig
   373  
   374  	for {
   375  		select {
   376  		case config, ok := <-configStream:
   377  			if !ok {
   378  				log.Debug("processing config stream closed")
   379  				configClosed = true
   380  				break
   381  			}
   382  			updatedConfig = config
   383  			log.Debugf("updated config received: %#v", updatedConfig)
   384  		case event, ok := <-eventStream:
   385  			if !ok {
   386  				log.Debug("processing event stream closed")
   387  				eventClosed = true
   388  				break
   389  			}
   390  			complete = cmd.processEvent(user, appConfig, event)
   391  		case warnings, ok := <-warningsStream:
   392  			if !ok {
   393  				log.Debug("processing warnings stream closed")
   394  				warningsClosed = true
   395  				break
   396  			}
   397  			cmd.UI.DisplayWarnings(warnings)
   398  		case err, ok := <-errorStream:
   399  			if !ok {
   400  				log.Debug("processing error stream closed")
   401  				warningsClosed = true
   402  				break
   403  			}
   404  			return pushaction.ApplicationConfig{}, err
   405  		}
   406  
   407  		if configClosed && eventClosed && warningsClosed && complete {
   408  			log.Debug("breaking apply display loop")
   409  			break
   410  		}
   411  	}
   412  
   413  	return updatedConfig, nil
   414  }
   415  
   416  func (cmd PushCommand) processEvent(user configv3.User, appConfig pushaction.ApplicationConfig, event pushaction.Event) bool {
   417  	log.Infoln("received apply event:", event)
   418  
   419  	switch event {
   420  	case pushaction.CreatingAndMappingRoutes:
   421  		cmd.UI.DisplayText("Mapping routes...")
   422  	case pushaction.UnmappingRoutes:
   423  		cmd.UI.DisplayText("Unmapping routes...")
   424  	case pushaction.ConfiguringServices:
   425  		cmd.UI.DisplayText("Binding services...")
   426  	case pushaction.ResourceMatching:
   427  		cmd.UI.DisplayText("Comparing local files to remote cache...")
   428  	case pushaction.CreatingArchive:
   429  		cmd.UI.DisplayText("Packaging files to upload...")
   430  	case pushaction.UploadingApplication:
   431  		cmd.UI.DisplayText("All files found in remote cache; nothing to upload.")
   432  		cmd.UI.DisplayText("Waiting for API to complete processing files...")
   433  	case pushaction.UploadingDroplet:
   434  		cmd.UI.DisplayText("Uploading droplet...")
   435  		log.Debug("starting progress bar")
   436  		cmd.ProgressBar.Ready()
   437  	case pushaction.UploadingApplicationWithArchive:
   438  		cmd.UI.DisplayText("Uploading files...")
   439  		log.Debug("starting progress bar")
   440  		cmd.ProgressBar.Ready()
   441  	case pushaction.RetryUpload:
   442  		cmd.UI.DisplayText("Retrying upload due to an error...")
   443  	case pushaction.UploadWithArchiveComplete:
   444  		cmd.ProgressBar.Complete()
   445  		cmd.UI.DisplayNewline()
   446  		cmd.UI.DisplayText("Waiting for API to complete processing files...")
   447  	case pushaction.UploadDropletComplete:
   448  		cmd.ProgressBar.Complete()
   449  		cmd.UI.DisplayNewline()
   450  		cmd.UI.DisplayText("Waiting for API to complete processing droplet...")
   451  	case pushaction.Complete:
   452  		return true
   453  	default:
   454  		log.WithField("event", event).Debug("ignoring event")
   455  	}
   456  	return false
   457  }
   458  
   459  func (cmd PushCommand) validateArgs() error {
   460  	switch {
   461  	case cmd.DropletPath != "" && cmd.AppPath != "":
   462  		return translatableerror.ArgumentCombinationError{
   463  			Args: []string{"--droplet", "-p"},
   464  		}
   465  	case cmd.DropletPath != "" && cmd.DockerImage.Path != "":
   466  		return translatableerror.ArgumentCombinationError{
   467  			Args: []string{"--droplet", "--docker-image", "-o"},
   468  		}
   469  	// ArgumentCombinationError trumps RequiredArgsError in this case (when
   470  	// DockerImage unspecified)
   471  	case cmd.DropletPath != "" && cmd.DockerUsername != "":
   472  		return translatableerror.ArgumentCombinationError{
   473  			Args: []string{"--droplet", "--docker-username", "-p"},
   474  		}
   475  	case cmd.DockerImage.Path != "" && cmd.AppPath != "":
   476  		return translatableerror.ArgumentCombinationError{
   477  			Args: []string{"--docker-image, -o", "-p"},
   478  		}
   479  	case cmd.DockerImage.Path != "" && cmd.Buildpacks != nil:
   480  		return translatableerror.ArgumentCombinationError{
   481  			Args: []string{"-b", "--docker-image, -o"},
   482  		}
   483  	case cmd.DockerUsername != "" && cmd.DockerImage.Path == "":
   484  		return translatableerror.RequiredFlagsError{
   485  			Arg1: "--docker-image, -o",
   486  			Arg2: "--docker-username",
   487  		}
   488  	case cmd.Domain != "" && cmd.NoRoute:
   489  		return translatableerror.ArgumentCombinationError{
   490  			Args: []string{"-d", "--no-route"},
   491  		}
   492  	case cmd.Hostname != "" && cmd.NoHostname:
   493  		return translatableerror.ArgumentCombinationError{
   494  			Args: []string{"--hostname", "-n", "--no-hostname"},
   495  		}
   496  	case cmd.Hostname != "" && cmd.NoRoute:
   497  		return translatableerror.ArgumentCombinationError{
   498  			Args: []string{"--hostname", "-n", "--no-route"},
   499  		}
   500  	case cmd.NoHostname && cmd.NoRoute:
   501  		return translatableerror.ArgumentCombinationError{
   502  			Args: []string{"--no-hostname", "--no-route"},
   503  		}
   504  	case cmd.PathToManifest != "" && cmd.NoManifest:
   505  		return translatableerror.ArgumentCombinationError{
   506  			Args: []string{"-f", "--no-manifest"},
   507  		}
   508  	case cmd.RandomRoute && cmd.Hostname != "":
   509  		return translatableerror.ArgumentCombinationError{
   510  			Args: []string{"--hostname", "-n", "--random-route"},
   511  		}
   512  	case cmd.RandomRoute && cmd.NoHostname:
   513  		return translatableerror.ArgumentCombinationError{
   514  			Args: []string{"--no-hostname", "--random-route"},
   515  		}
   516  	case cmd.RandomRoute && cmd.NoRoute:
   517  		return translatableerror.ArgumentCombinationError{
   518  			Args: []string{"--no-route", "--random-route"},
   519  		}
   520  	case cmd.RandomRoute && cmd.RoutePath.Path != "":
   521  		return translatableerror.ArgumentCombinationError{
   522  			Args: []string{"--random-route", "--route-path"},
   523  		}
   524  	case cmd.RoutePath.Path != "" && cmd.NoRoute:
   525  		return translatableerror.ArgumentCombinationError{
   526  			Args: []string{"--route-path", "--no-route"},
   527  		}
   528  	}
   529  
   530  	return nil
   531  }