github.com/liamawhite/cli-with-i18n@v6.32.1-0.20171122084555-dede0a5c3448+incompatible/command/v2/v2_push_command.go (about)

     1  package v2
     2  
     3  import (
     4  	"os"
     5  	"path/filepath"
     6  
     7  	"github.com/cloudfoundry/noaa/consumer"
     8  	"github.com/liamawhite/cli-with-i18n/actor/pushaction"
     9  	"github.com/liamawhite/cli-with-i18n/actor/sharedaction"
    10  	"github.com/liamawhite/cli-with-i18n/actor/v2action"
    11  	oldcmd "github.com/liamawhite/cli-with-i18n/cf/cmd"
    12  	"github.com/liamawhite/cli-with-i18n/command"
    13  	"github.com/liamawhite/cli-with-i18n/command/flag"
    14  	"github.com/liamawhite/cli-with-i18n/command/translatableerror"
    15  	"github.com/liamawhite/cli-with-i18n/command/v2/shared"
    16  	"github.com/liamawhite/cli-with-i18n/util/configv3"
    17  	"github.com/liamawhite/cli-with-i18n/util/manifest"
    18  	"github.com/liamawhite/cli-with-i18n/util/progressbar"
    19  	log "github.com/Sirupsen/logrus"
    20  )
    21  
    22  //go:generate counterfeiter . ProgressBar
    23  
    24  type ProgressBar interface {
    25  	pushaction.ProgressBar
    26  	Complete()
    27  	Ready()
    28  }
    29  
    30  //go:generate counterfeiter . V2PushActor
    31  
    32  type V2PushActor interface {
    33  	Apply(config pushaction.ApplicationConfig, progressBar pushaction.ProgressBar) (<-chan pushaction.ApplicationConfig, <-chan pushaction.Event, <-chan pushaction.Warnings, <-chan error)
    34  	ConvertToApplicationConfigs(orgGUID string, spaceGUID string, noStart bool, apps []manifest.Application) ([]pushaction.ApplicationConfig, pushaction.Warnings, error)
    35  	MergeAndValidateSettingsAndManifests(cmdSettings pushaction.CommandLineSettings, apps []manifest.Application) ([]manifest.Application, error)
    36  	ReadManifest(pathToManifest string) ([]manifest.Application, error)
    37  }
    38  
    39  type V2PushCommand struct {
    40  	OptionalArgs flag.OptionalAppName `positional-args:"yes"`
    41  	Buildpack    flag.Buildpack       `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'"`
    42  	Command      flag.Command         `short:"c" description:"Startup command, set to null to reset to default start command"`
    43  	// Domain               string                      `short:"d" description:"Domain (e.g. example.com)"`
    44  	DockerImage     flag.DockerImage            `long:"docker-image" short:"o" description:"Docker-image to be used (e.g. user/docker-image-name)"`
    45  	DockerUsername  string                      `long:"docker-username" description:"Repository username; used with password from environment variable CF_DOCKER_PASSWORD"`
    46  	PathToManifest  flag.PathWithExistenceCheck `short:"f" description:"Path to manifest"`
    47  	HealthCheckType flag.HealthCheckType        `long:"health-check-type" short:"u" description:"Application health check type (Default: 'port', 'none' accepted for 'process', 'http' implies endpoint '/')"`
    48  	// Hostname             string                      `long:"hostname" short:"n" description:"Hostname (e.g. my-subdomain)"`
    49  	Instances flag.Instances `short:"i" description:"Number of instances"`
    50  	DiskQuota flag.Megabytes `short:"k" description:"Disk limit (e.g. 256M, 1024M, 1G)"`
    51  	Memory    flag.Megabytes `short:"m" description:"Memory limit (e.g. 256M, 1024M, 1G)"`
    52  	// NoHostname           bool                        `long:"no-hostname" description:"Map the root domain to this app"`
    53  	NoManifest bool `long:"no-manifest" description:"Ignore manifest file"`
    54  	// NoRoute              bool                        `long:"no-route" description:"Do not map a route to this app and remove routes from previous pushes of this app"`
    55  	NoStart bool                        `long:"no-start" description:"Do not start an app after pushing"`
    56  	AppPath flag.PathWithExistenceCheck `short:"p" description:"Path to app directory or to a zip file of the contents of the app directory"`
    57  	// RandomRoute          bool                        `long:"random-route" description:"Create a random route for this app"`
    58  	// RoutePath            string                      `long:"route-path" description:"Path for the route"`
    59  	StackName           string      `short:"s" description:"Stack to use (a stack is a pre-built file system, including an operating system, that can run apps)"`
    60  	HealthCheckTimeout  int         `short:"t" description:"Time (in seconds) allowed to elapse between starting up an app and the first healthy response from the app"`
    61  	envCFStagingTimeout interface{} `environmentName:"CF_STAGING_TIMEOUT" environmentDescription:"Max wait time for buildpack staging, in minutes" environmentDefault:"15"`
    62  	envCFStartupTimeout interface{} `environmentName:"CF_STARTUP_TIMEOUT" environmentDescription:"Max wait time for app instance startup, in minutes" environmentDefault:"5"`
    63  	dockerPassword      interface{} `environmentName:"CF_DOCKER_PASSWORD" environmentDescription:"Password used for private docker repository"`
    64  
    65  	usage           interface{} `usage:"cf v2-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]\n\n   cf v2-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]\n\n   cf v2-push -f MANIFEST_WITH_MULTIPLE_APPS_PATH [APP_NAME] [--no-start]"`
    66  	relatedCommands interface{} `related_commands:"apps, create-app-manifest, logs, ssh, start"`
    67  
    68  	UI          command.UI
    69  	Config      command.Config
    70  	SharedActor command.SharedActor
    71  	Actor       V2PushActor
    72  	ProgressBar ProgressBar
    73  
    74  	RestartActor RestartActor
    75  	NOAAClient   *consumer.Consumer
    76  }
    77  
    78  func (cmd *V2PushCommand) Setup(config command.Config, ui command.UI) error {
    79  	cmd.UI = ui
    80  	cmd.Config = config
    81  	sharedActor := sharedaction.NewActor(config)
    82  
    83  	ccClient, uaaClient, err := shared.NewClients(config, ui, true)
    84  	if err != nil {
    85  		return err
    86  	}
    87  	v2Actor := v2action.NewActor(ccClient, uaaClient, config)
    88  	cmd.RestartActor = v2Actor
    89  	cmd.Actor = pushaction.NewActor(v2Actor, sharedActor)
    90  	cmd.SharedActor = sharedActor
    91  	cmd.NOAAClient = shared.NewNOAAClient(ccClient.DopplerEndpoint(), config, uaaClient, ui)
    92  
    93  	cmd.ProgressBar = progressbar.NewProgressBar()
    94  	return nil
    95  }
    96  
    97  func (cmd V2PushCommand) Execute(args []string) error {
    98  	cmd.UI.DisplayWarning(command.ExperimentalWarning)
    99  
   100  	err := cmd.SharedActor.CheckTarget(cmd.Config, true, true)
   101  	if err != nil {
   102  		return shared.HandleError(err)
   103  	}
   104  
   105  	user, err := cmd.Config.CurrentUser()
   106  	if err != nil {
   107  		return shared.HandleError(err)
   108  	}
   109  
   110  	log.Info("collating flags")
   111  	cliSettings, err := cmd.GetCommandLineSettings()
   112  	if err != nil {
   113  		log.Errorln("reading flags:", err)
   114  		return shared.HandleError(err)
   115  	}
   116  
   117  	log.Info("checking manifest")
   118  	rawApps, err := cmd.findAndReadManifest(cliSettings)
   119  	if _, ok := err.(manifest.UnsupportedFieldsError); ok {
   120  		// !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
   121  		// The following section is not tested as it calls into the old code.
   122  		// !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
   123  		cmd.UI.DisplayWarning("*** Global attributes/inheritance in app manifest are not supported in v2-push, delegating to old push ***")
   124  		var args []string
   125  		for _, arg := range os.Args {
   126  			if arg == "v2-push" {
   127  				args = append(args, "push")
   128  			} else {
   129  				args = append(args, arg)
   130  			}
   131  		}
   132  
   133  		oldcmd.Main(os.Getenv("CF_TRACE"), args)
   134  		return nil
   135  	} else if err != nil {
   136  		log.Errorln("reading manifest:", err)
   137  		return shared.HandleError(err)
   138  	}
   139  
   140  	log.Info("merging manifest and command flags")
   141  	manifestApplications, err := cmd.Actor.MergeAndValidateSettingsAndManifests(cliSettings, rawApps)
   142  	if err != nil {
   143  		log.Errorln("merging manifest:", err)
   144  		return shared.HandleError(err)
   145  	}
   146  
   147  	cmd.UI.DisplayText("Getting app info...")
   148  
   149  	log.Info("converting manifests to ApplicationConfigs")
   150  	appConfigs, warnings, err := cmd.Actor.ConvertToApplicationConfigs(
   151  		cmd.Config.TargetedOrganization().GUID,
   152  		cmd.Config.TargetedSpace().GUID,
   153  		cmd.NoStart,
   154  		manifestApplications,
   155  	)
   156  	cmd.UI.DisplayWarnings(warnings)
   157  	if err != nil {
   158  		log.Errorln("converting manifest:", err)
   159  		return shared.HandleError(err)
   160  	}
   161  
   162  	for _, appConfig := range appConfigs {
   163  		if appConfig.CreatingApplication() {
   164  			cmd.UI.DisplayText("Creating app with these attributes...")
   165  		} else {
   166  			cmd.UI.DisplayText("Updating app with these attributes...")
   167  		}
   168  		log.Infoln("starting create/update:", appConfig.DesiredApplication.Name)
   169  		changes := shared.GetApplicationChanges(appConfig)
   170  		err := cmd.UI.DisplayChangesForPush(changes)
   171  		if err != nil {
   172  			log.Errorln("display changes:", err)
   173  			return shared.HandleError(err)
   174  		}
   175  		cmd.UI.DisplayNewline()
   176  	}
   177  
   178  	for appNumber, appConfig := range appConfigs {
   179  		if appConfig.CreatingApplication() {
   180  			cmd.UI.DisplayTextWithFlavor("Creating app {{.AppName}}...", map[string]interface{}{
   181  				"AppName": appConfig.DesiredApplication.Name,
   182  			})
   183  		} else {
   184  			cmd.UI.DisplayTextWithFlavor("Updating app {{.AppName}}...", map[string]interface{}{
   185  				"AppName": appConfig.DesiredApplication.Name,
   186  			})
   187  		}
   188  
   189  		configStream, eventStream, warningsStream, errorStream := cmd.Actor.Apply(appConfig, cmd.ProgressBar)
   190  		updatedConfig, err := cmd.processApplyStreams(user, appConfig, configStream, eventStream, warningsStream, errorStream)
   191  		if err != nil {
   192  			log.Errorln("process apply stream:", err)
   193  			return shared.HandleError(err)
   194  		}
   195  
   196  		if !cmd.NoStart {
   197  			messages, logErrs, appState, apiWarnings, errs := cmd.RestartActor.RestartApplication(updatedConfig.CurrentApplication.Application, cmd.NOAAClient, cmd.Config)
   198  			err = shared.PollStart(cmd.UI, cmd.Config, messages, logErrs, appState, apiWarnings, errs)
   199  			if err != nil {
   200  				return err
   201  			}
   202  		}
   203  
   204  		cmd.UI.DisplayNewline()
   205  		appSummary, warnings, err := cmd.RestartActor.GetApplicationSummaryByNameAndSpace(appConfig.DesiredApplication.Name, cmd.Config.TargetedSpace().GUID)
   206  		cmd.UI.DisplayWarnings(warnings)
   207  		if err != nil {
   208  			return shared.HandleError(err)
   209  		}
   210  
   211  		shared.DisplayAppSummary(cmd.UI, appSummary, true)
   212  
   213  		if appNumber+1 <= len(appConfigs) {
   214  			cmd.UI.DisplayNewline()
   215  		}
   216  	}
   217  
   218  	return nil
   219  }
   220  
   221  func (cmd V2PushCommand) GetCommandLineSettings() (pushaction.CommandLineSettings, error) {
   222  	err := cmd.validateArgs()
   223  	if err != nil {
   224  		return pushaction.CommandLineSettings{}, shared.HandleError(err)
   225  	}
   226  
   227  	pwd, err := os.Getwd()
   228  	if err != nil {
   229  		return pushaction.CommandLineSettings{}, err
   230  	}
   231  
   232  	config := pushaction.CommandLineSettings{
   233  		Buildpack:          cmd.Buildpack.FilteredString,
   234  		Command:            cmd.Command.FilteredString,
   235  		CurrentDirectory:   pwd,
   236  		DiskQuota:          cmd.DiskQuota.Value,
   237  		DockerImage:        cmd.DockerImage.Path,
   238  		DockerUsername:     cmd.DockerUsername,
   239  		DockerPassword:     cmd.Config.DockerPassword(),
   240  		HealthCheckTimeout: cmd.HealthCheckTimeout,
   241  		HealthCheckType:    cmd.HealthCheckType.Type,
   242  		Instances:          cmd.Instances.NullInt,
   243  		Memory:             cmd.Memory.Value,
   244  		Name:               cmd.OptionalArgs.AppName,
   245  		ProvidedAppPath:    string(cmd.AppPath),
   246  		StackName:          cmd.StackName,
   247  	}
   248  
   249  	log.Debugln("Command Line Settings:", config)
   250  	return config, nil
   251  }
   252  
   253  func (cmd V2PushCommand) findAndReadManifest(settings pushaction.CommandLineSettings) ([]manifest.Application, error) {
   254  	var pathToManifest string
   255  
   256  	switch {
   257  	case cmd.NoManifest:
   258  		log.Debug("skipping reading of manifest")
   259  		return nil, nil
   260  	case cmd.PathToManifest != "":
   261  		log.Debug("using specified manifest file")
   262  		pathToManifest = string(cmd.PathToManifest)
   263  	default:
   264  		log.Debug("searching for manifest file")
   265  		pathToManifest = filepath.Join(settings.CurrentDirectory, "manifest.yml")
   266  		if _, err := os.Stat(pathToManifest); os.IsNotExist(err) {
   267  			log.WithField("pathToManifest", pathToManifest).Debug("could not find")
   268  
   269  			// While this is unlikely to be used, it is kept for backwards
   270  			// compatibility.
   271  			pathToManifest = filepath.Join(settings.CurrentDirectory, "manifest.yaml")
   272  			if _, err := os.Stat(pathToManifest); os.IsNotExist(err) {
   273  				log.WithField("pathToManifest", pathToManifest).Debug("could not find")
   274  				return nil, nil
   275  			}
   276  		}
   277  	}
   278  
   279  	log.WithField("pathToManifest", pathToManifest).Info("reading manifest")
   280  	cmd.UI.DisplayText("Using manifest file {{.Path}}", map[string]interface{}{
   281  		"Path": pathToManifest,
   282  	})
   283  	return cmd.Actor.ReadManifest(pathToManifest)
   284  }
   285  
   286  func (cmd V2PushCommand) processApplyStreams(
   287  	user configv3.User,
   288  	appConfig pushaction.ApplicationConfig,
   289  	configStream <-chan pushaction.ApplicationConfig,
   290  	eventStream <-chan pushaction.Event,
   291  	warningsStream <-chan pushaction.Warnings,
   292  	errorStream <-chan error,
   293  ) (pushaction.ApplicationConfig, error) {
   294  	var configClosed, eventClosed, warningsClosed, complete bool
   295  	var updatedConfig pushaction.ApplicationConfig
   296  
   297  	for {
   298  		select {
   299  		case config, ok := <-configStream:
   300  			if !ok {
   301  				log.Debug("processing config stream closed")
   302  				configClosed = true
   303  				break
   304  			}
   305  			updatedConfig = config
   306  			log.Debugf("updated config received: %#v", updatedConfig)
   307  		case event, ok := <-eventStream:
   308  			if !ok {
   309  				log.Debug("processing event stream closed")
   310  				eventClosed = true
   311  				break
   312  			}
   313  			complete = cmd.processEvent(user, appConfig, event)
   314  		case warnings, ok := <-warningsStream:
   315  			if !ok {
   316  				log.Debug("processing warnings stream closed")
   317  				warningsClosed = true
   318  				break
   319  			}
   320  			cmd.UI.DisplayWarnings(warnings)
   321  		case err, ok := <-errorStream:
   322  			if !ok {
   323  				log.Debug("processing error stream closed")
   324  				warningsClosed = true
   325  				break
   326  			}
   327  			return pushaction.ApplicationConfig{}, err
   328  		}
   329  
   330  		if configClosed && eventClosed && warningsClosed && complete {
   331  			log.Debug("breaking apply display loop")
   332  			break
   333  		}
   334  	}
   335  
   336  	return updatedConfig, nil
   337  }
   338  
   339  func (cmd V2PushCommand) processEvent(user configv3.User, appConfig pushaction.ApplicationConfig, event pushaction.Event) bool {
   340  	log.Infoln("received apply event:", event)
   341  
   342  	switch event {
   343  	case pushaction.ConfiguringRoutes:
   344  		cmd.UI.DisplayText("Mapping routes...")
   345  	case pushaction.ConfiguringServices:
   346  		cmd.UI.DisplayText("Binding services...")
   347  	case pushaction.ResourceMatching:
   348  		cmd.UI.DisplayText("Comparing local files to remote cache...")
   349  	case pushaction.CreatingArchive:
   350  		cmd.UI.DisplayText("Packaging files to upload...")
   351  	case pushaction.UploadingApplication:
   352  		cmd.UI.DisplayText("Uploading files...")
   353  		log.Debug("starting progress bar")
   354  		cmd.ProgressBar.Ready()
   355  	case pushaction.RetryUpload:
   356  		cmd.UI.DisplayText("Retrying upload due to an error...")
   357  	case pushaction.UploadComplete:
   358  		cmd.ProgressBar.Complete()
   359  		cmd.UI.DisplayNewline()
   360  		cmd.UI.DisplayText("Waiting for API to complete processing files...")
   361  	case pushaction.Complete:
   362  		return true
   363  	default:
   364  		log.WithField("event", event).Debug("ignoring event")
   365  	}
   366  	return false
   367  }
   368  
   369  func (cmd V2PushCommand) validateArgs() error {
   370  	switch {
   371  	case cmd.DockerImage.Path != "" && cmd.AppPath != "":
   372  		return translatableerror.ArgumentCombinationError{
   373  			Args: []string{"--docker-image, -o", "-p"},
   374  		}
   375  	case cmd.DockerImage.Path != "" && cmd.Buildpack.IsSet:
   376  		return translatableerror.ArgumentCombinationError{
   377  			Args: []string{"-b", "--docker-image, -o"},
   378  		}
   379  	case cmd.DockerUsername != "" && cmd.DockerImage.Path == "":
   380  		return translatableerror.RequiredFlagsError{
   381  			Arg1: "--docker-image, -o",
   382  			Arg2: "--docker-username",
   383  		}
   384  	case cmd.DockerUsername != "" && cmd.Config.DockerPassword() == "":
   385  		return translatableerror.DockerPasswordNotSetError{}
   386  	case cmd.PathToManifest != "" && cmd.NoManifest:
   387  		return translatableerror.ArgumentCombinationError{
   388  			Args: []string{"-f", "--no-manifest"},
   389  		}
   390  	}
   391  
   392  	return nil
   393  }