internal/pkg/cli/deploy.go (665 lines of code) (raw):

// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 package cli import ( "errors" "fmt" "math" "slices" "strconv" "strings" "github.com/aws/copilot-cli/internal/pkg/queue" "github.com/dustin/go-humanize" "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/service/ssm" "github.com/dustin/go-humanize/english" "github.com/spf13/afero" "github.com/spf13/cobra" "github.com/aws/copilot-cli/cmd/copilot/template" "github.com/aws/copilot-cli/internal/pkg/aws/identity" "github.com/aws/copilot-cli/internal/pkg/aws/sessions" "github.com/aws/copilot-cli/internal/pkg/cli/group" "github.com/aws/copilot-cli/internal/pkg/config" "github.com/aws/copilot-cli/internal/pkg/deploy/cloudformation" "github.com/aws/copilot-cli/internal/pkg/exec" "github.com/aws/copilot-cli/internal/pkg/initialize" "github.com/aws/copilot-cli/internal/pkg/manifest" "github.com/aws/copilot-cli/internal/pkg/manifest/manifestinfo" "github.com/aws/copilot-cli/internal/pkg/term/color" "github.com/aws/copilot-cli/internal/pkg/term/log" termprogress "github.com/aws/copilot-cli/internal/pkg/term/progress" "github.com/aws/copilot-cli/internal/pkg/term/prompt" "github.com/aws/copilot-cli/internal/pkg/term/selector" "github.com/aws/copilot-cli/internal/pkg/version" "github.com/aws/copilot-cli/internal/pkg/workspace" ) const ( svcWkldType = "svc" jobWkldType = "job" optionAllRemainingWorkloads = "All remaining workloads" ) // filterItemsByStrings is a generic function to return the subset of wantedStrings that exists in possibleItems. // stringFunc is a method to convert the generic item type (T) to a string; for example, one can convert a struct of type // *config.Workload to a string by passing // // func(w *config.Workload) string { return w.Name }. // // Likewise, filterItemsByStrings can work on a list of strings by returning the unmodified item: // // filterItemsByStrings(wantedStrings, stringSlice2, func(s string) string { return s }) // // It returns a list of strings (items whose stringFunc() exists in the list of wantedStrings). func filterItemsByStrings[T any](wantedStrings []string, possibleItems []T, stringFunc func(T) string) []string { m := make(map[string]bool) for _, item := range wantedStrings { m[item] = true } res := make([]string, 0, len(wantedStrings)) for _, item := range possibleItems { if m[stringFunc(item)] { res = append(res, stringFunc(item)) } } return res } type deployVars struct { deployWkldVars workloadNames []string deployAllWorkloads bool yesInitWkld bool deployEnv *bool yesInitEnv *bool region string tempCreds tempCredsVars profile string } type deployOpts struct { deployVars newWorkloadAdder func() wkldInitializerWithoutManifest setupDeployCmd func(*deployOpts, string, string) (actionCommand, error) newInitEnvCmd func(o *deployOpts) (cmd, error) newDeployEnvCmd func(o *deployOpts) (cmd, error) sel wsSelector store store ws wsWlDirReader prompt prompter // values for logging wlType string // values for initialization logic envExistsInApp bool envExistsInWs bool // Cached variables wsEnvironments []string wsWorkloads []string storeWorkloads []*config.Workload initializedWsWorkloads []string deploymentOrderMap map[string]int } func newDeployOpts(vars deployVars) (*deployOpts, error) { sessProvider := sessions.ImmutableProvider(sessions.UserAgentExtras("deploy")) defaultSess, err := sessProvider.Default() if err != nil { return nil, fmt.Errorf("default session: %v", err) } store := config.NewSSMStore(identity.New(defaultSess), ssm.New(defaultSess), aws.StringValue(defaultSess.Config.Region)) ws, err := workspace.Use(afero.NewOsFs()) if err != nil { return nil, err } prompter := prompt.New() return &deployOpts{ deployVars: vars, store: store, sel: selector.NewLocalWorkloadSelector(prompter, store, ws), ws: ws, prompt: prompter, newWorkloadAdder: func() wkldInitializerWithoutManifest { return &initialize.WorkloadInitializer{ Store: store, Deployer: cloudformation.New(defaultSess), Ws: ws, Prog: termprogress.NewSpinner(log.DiagnosticWriter), } }, newDeployEnvCmd: func(o *deployOpts) (cmd, error) { // This command passes flags down from return newEnvDeployOpts(deployEnvVars{ appName: o.appName, name: o.envName, forceNewUpdate: o.forceNewUpdate, disableRollback: o.disableRollback, showDiff: o.showDiff, skipDiffPrompt: o.skipDiffPrompt, allowEnvDowngrade: o.allowWkldDowngrade, detach: o.detach, }) }, newInitEnvCmd: func(o *deployOpts) (cmd, error) { // This vars struct sets "default config" so that no vpc questions are asked during env init and the manifest // is not written. It passes in credential flags and allow-downgrade from the parent command. return newInitEnvOpts(initEnvVars{ appName: o.appName, name: o.envName, profile: o.profile, defaultConfig: true, allowAppDowngrade: o.allowWkldDowngrade, tempCreds: o.tempCreds, region: o.region, }) }, setupDeployCmd: func(o *deployOpts, workloadName, workloadType string) (actionCommand, error) { switch { case slices.Contains(manifestinfo.JobTypes(), workloadType): opts := &deployJobOpts{ deployWkldVars: o.deployWkldVars, store: o.store, ws: o.ws, newInterpolator: newManifestInterpolator, unmarshal: manifest.UnmarshalWorkload, sel: selector.NewLocalWorkloadSelector(o.prompt, o.store, ws), cmd: exec.NewCmd(), templateVersion: version.LatestTemplateVersion(), sessProvider: sessProvider, } opts.newJobDeployer = func() (workloadDeployer, error) { return newJobDeployer(opts) } opts.name = workloadName return opts, nil case slices.Contains(manifestinfo.ServiceTypes(), workloadType): opts := &deploySvcOpts{ deployWkldVars: o.deployWkldVars, store: o.store, ws: o.ws, newInterpolator: newManifestInterpolator, unmarshal: manifest.UnmarshalWorkload, spinner: termprogress.NewSpinner(log.DiagnosticWriter), sel: selector.NewLocalWorkloadSelector(o.prompt, o.store, ws), prompt: o.prompt, cmd: exec.NewCmd(), sessProvider: sessProvider, templateVersion: version.LatestTemplateVersion(), } opts.newSvcDeployer = func() (workloadDeployer, error) { return newSvcDeployer(opts) } opts.name = workloadName // Multi-deployments can have flags specified which are not compatible with all service types. // Currently only forceNewUpdate is incompatible with Static Site types. if workloadType == manifestinfo.StaticSiteType { opts.forceNewUpdate = false } return opts, nil } return nil, fmt.Errorf("unrecognized workload type %s", workloadType) }, }, nil } // maybeInitWkld decides whether a workload needs to be initialized before deployment. // We do not prompt for workload initialization; specifying the workload by name suffices // to convey the customer intention. When the customer specifies --all and --init-wkld, // we will add all un-initialized local workloads to the list to be deployed. // When the customer does not specify --init-wkld with --all, we will only deploy initialized workloads. func (o *deployOpts) maybeInitWkld(name string) error { // Confirm that the workload needs to be initialized after asking for the name. initializedWorkloads, err := o.listInitializedLocalWorkloads() if err != nil { return err } // Workload is already initialized. Return early. if slices.Contains(initializedWorkloads, name) { return nil } // Get workload type and confirm readable manifest. mf, err := o.ws.ReadWorkloadManifest(name) if err != nil { return fmt.Errorf("read manifest for workload %s: %w", name, err) } workloadType, err := mf.WorkloadType() if err != nil { return fmt.Errorf("get workload type from manifest for workload %s: %w", name, err) } if !slices.Contains(manifestinfo.WorkloadTypes(), workloadType) { return fmt.Errorf("unrecognized workload type %q in manifest for workload %s", workloadType, name) } wkldAdder := o.newWorkloadAdder() if err = wkldAdder.AddWorkloadToApp(o.appName, name, workloadType); err != nil { return fmt.Errorf("add workload to app: %w", err) } return nil } type workloadPriority struct { priority int workloads []string } func (w workloadPriority) LessThan(a workloadPriority) bool { return w.priority < a.priority } // parseDeploymentOrderTags takes a list of workload names, optionally tagged with a priority. Lower priorities will be // deployed first. // // []string{"fe/1", "be/1", "worker/2"} // // It returns a map from the workload name to its priority, and errors out if the order tag is incorrectly formatted. // // map[string]int{"fe": 1, "be": 1, "worker": 2} // // It is possible to have multiple workloads of the same priority. We don't guarantee that workloads within priority // groups will have identical deployment orders each time. func (o *deployOpts) parseDeploymentOrderTags(namesWithOptionalOrder []string) error { if len(o.deploymentOrderMap) > 0 { return nil } prioritiesMap := make(map[string]int) // First pass through flags to identify priority groups for _, wkldName := range namesWithOptionalOrder { parts := strings.Split(wkldName, "/") // If there's no priority tag, deploy this service after everything else. Signify this with math.MaxInt priority. // If there's a valid tag, add it to the map of priorities. if len(parts) == 1 { prioritiesMap[parts[0]] = math.MaxInt } else if len(parts) == 2 { order, err := strconv.Atoi(parts[1]) if err != nil { return fmt.Errorf("parse deployment order for workloads: %w", err) } prioritiesMap[parts[0]] = order } else { return fmt.Errorf("invalid deployment order for workload %s", wkldName) } } o.deploymentOrderMap = prioritiesMap return nil } // getDeploymentOrder parses names and tags from the --name flag, respecting "all". // It returns an ordered list of which workloads should be deployed and when. // For example, when a customer specifies "fe/2,be/1,worker,job" and the --all flag // in a workspace where there also exists a `db` service, this function will return // // [][]string{ {"be"}, {"fe"}, {"worker", "job", "db"} }. // // TODO: when there's a dependsOn field in the manifest, we should modify this function to respect it. func (o *deployOpts) getDeploymentOrder() ([][]string, error) { // Get a map from workload name to deployment priority if err := o.parseDeploymentOrderTags(o.workloadNames); err != nil { return nil, err } // Iterate over priority map to invert it and get groups of workloads with the same priority. groupsMap := make(map[int][]string) for k, v := range o.deploymentOrderMap { groupsMap[v] = append(groupsMap[v], k) } // If --all is specified, we need to add the remainder of workloads with math.MaxInt priority according to whether or not --init-wkld is specified. if o.deployAllWorkloads { specifiedWorkloadList := make([]string, 0, len(o.deploymentOrderMap)) for k := range o.deploymentOrderMap { specifiedWorkloadList = append(specifiedWorkloadList, k) } if o.yesInitWkld { // Add all unspecified local workloads to the list of workloads to be deployed. localWorkloads, err := o.listLocalWorkloads() if err != nil { return nil, err } workloadsToAppend := slices.DeleteFunc(slices.Clone(localWorkloads), func(s string) bool { return slices.Contains(specifiedWorkloadList, s) }) if len(workloadsToAppend) != 0 { groupsMap[math.MaxInt] = append(groupsMap[math.MaxInt], workloadsToAppend...) } } else { // Otherwise (--init-wkld is false): get only get initialized local workloads. initializedWorkloads, err := o.listInitializedLocalWorkloads() if err != nil { return nil, err } workloadsToAppend := slices.DeleteFunc(slices.Clone(initializedWorkloads), func(s string) bool { return slices.Contains(specifiedWorkloadList, s) }) if len(workloadsToAppend) != 0 { groupsMap[math.MaxInt] = append(groupsMap[math.MaxInt], workloadsToAppend...) } } } if len(groupsMap) == 0 { log.Infoln("No workloads to deploy. Did you mean to specify --init-wkld with --all?") return nil, errors.New("generate deployment groups: no workloads were specified") } deploymentGroups := queue.NewPriorityQueue[workloadPriority]() for k, v := range groupsMap { deploymentGroups.Push(workloadPriority{ priority: k, workloads: v, }) } res := make([][]string, 0, deploymentGroups.Len()) for deploymentGroups.Len() > 0 { v, ok := deploymentGroups.Pop() if !ok || v == nil { continue } res = append(res, v.workloads) } return res, nil } func (o *deployOpts) listStoreWorkloads() ([]*config.Workload, error) { if o.storeWorkloads != nil { return o.storeWorkloads, nil } wls, err := o.store.ListWorkloads(o.appName) if err != nil { return nil, fmt.Errorf("retrieve store workloads: %w", err) } o.storeWorkloads = wls return o.storeWorkloads, nil } func (o *deployOpts) listLocalWorkloads() ([]string, error) { if o.wsWorkloads != nil { return o.wsWorkloads, nil } localWorkloads, err := o.ws.ListWorkloads() if err != nil { return nil, fmt.Errorf("retrieve workspace workloads: %w", err) } o.wsWorkloads = localWorkloads return o.wsWorkloads, nil } func (o *deployOpts) listInitializedLocalWorkloads() ([]string, error) { if o.initializedWsWorkloads != nil { return o.initializedWsWorkloads, nil } storeWls, err := o.listStoreWorkloads() if err != nil { return nil, err } localWorkloads, err := o.listLocalWorkloads() if err != nil { return nil, err } o.initializedWsWorkloads = filterItemsByStrings(localWorkloads, storeWls, func(workload *config.Workload) string { return workload.Name }) return o.initializedWsWorkloads, nil } type workloadCommand struct { actionCommand name string } func getTotalNumberOfWorkloads(deploymentGroups [][]workloadCommand) int { var count int for i := 0; i < len(deploymentGroups); i++ { count += len(deploymentGroups[i]) } return count } func (o *deployOpts) Run() error { if err := o.askNames(); err != nil { return err } if err := o.askEnv(); err != nil { return err } if err := o.checkEnvExists(); err != nil { return err } if err := o.maybeInitEnv(); err != nil { return err } if err := o.maybeDeployEnv(); err != nil { return err } deploymentOrderGroups, err := o.getDeploymentOrder() if err != nil { return err } cmds := make([][]workloadCommand, len(deploymentOrderGroups)) // Do all our asking before executing deploy commands. // Also initialize workloads that need initialization (if they're specified by name and un-initialized, // it means the customer probably wants to init them). log.Infoln("Checking for all required information. We may ask you some questions.") for order, deploymentGroup := range deploymentOrderGroups { for _, workload := range deploymentGroup { // 1. Decide whether the current workload needs initialization. if err := o.maybeInitWkld(workload); err != nil { return err } // 2. Set up workload command. deployCmd, err := o.loadWkldCmd(workload) if err != nil { return err } cmds[order] = append(cmds[order], workloadCommand{ name: workload, actionCommand: deployCmd, }) // 3. Ask() and Validate() for required info. if err := deployCmd.Ask(); err != nil { return fmt.Errorf("ask %s deploy: %w", o.wlType, err) } if err := deployCmd.Validate(); err != nil { return fmt.Errorf("validate %s deploy: %w", o.wlType, err) } } } if count := getTotalNumberOfWorkloads(cmds); count > 1 { logDeploymentOrderInfo(cmds, count) } for g, deploymentGroup := range cmds { // TODO parallelize this. Steps involve: // 1. Modify the cmd to optionally disable progress tracker // 2. Modify labeledSyncBuffer so it can display a spinner. // 3. Wrap Execute() in a goroutine with ErrorGroup and context for i, cmd := range deploymentGroup { if err := cmd.Execute(); err != nil { var errNoInfraChanges *errNoInfrastructureChanges if !errors.As(err, &errNoInfraChanges) { return fmt.Errorf("execute deployment %d of %d in group %d: %w", i+1, len(deploymentGroup), g+1, err) } // Don't run recommended actions if there is an infra error. It's possible for RecommendActions to panic // when it returns an error (the actionRecommender return is nil in error cases and the struct member // is never set. continue } if err := cmd.RecommendActions(); err != nil { return err } } } return nil } func logDeploymentOrderInfo(cmds [][]workloadCommand, totalCount int) { log.Infof("Will deploy %d %s in the following order.\n", totalCount, english.PluralWord(totalCount, "workload", "")) for i := 0; i < len(cmds); i++ { names := make([]string, 0, len(cmds)) for _, cmd := range cmds[i] { names = append(names, cmd.name) } log.Infof("%d. %s\n", i+1, strings.Join(names, ", ")) } } func (o *deployOpts) askNames() error { if o.workloadNames != nil || len(o.workloadNames) != 0 { return nil } if o.deployAllWorkloads { // --all and --init-wkld=true, means we should use ALL local workloads as our list of names. if o.yesInitWkld { o.workloadNames = o.wsWorkloads return nil } // --all and --init-wkld=false means we should only use the initialized local workloads. o.workloadNames = o.initializedWsWorkloads return nil } names, err := o.sel.Workloads("Select one or more services or jobs in your workspace.", "") if err != nil { return fmt.Errorf("select service or job: %w", err) } if len(names) == 1 { o.workloadNames = names return nil } // Select workload priority by repeatedly prompting with a multiselect until the list of options is depleted. options := names taggedOptions := make([]string, 0, len(options)) currentPriority := 1 for len(options) > 0 { selectedNames, err := o.prompt.MultiSelect( fmt.Sprintf("Which workload(s) would you like to deploy in your %s deployment group?", humanize.Ordinal(currentPriority)), "These workloads will be deployed together.", append(options, optionAllRemainingWorkloads), prompt.RequireMinItems(1), prompt.WithFinalMessage(fmt.Sprintf("Group %d", currentPriority)), ) if err != nil { return fmt.Errorf("select workloads for deployment group: %w", err) } if slices.Contains(selectedNames, optionAllRemainingWorkloads) { selectedNames = options } // Tag all selected workloads with the given priority. for _, name := range selectedNames { taggedOptions = append(taggedOptions, fmt.Sprintf("%s/%d", name, currentPriority)) } options = slices.DeleteFunc(options, func(s string) bool { return slices.Contains(selectedNames, s) }) currentPriority++ } if err := o.parseDeploymentOrderTags(taggedOptions); err != nil { return err } return nil } func (o *deployOpts) listWsEnvironments() ([]string, error) { if o.wsEnvironments == nil { envs, err := o.ws.ListEnvironments() if err != nil { return nil, err } if len(envs) == 0 { envs = []string{} } o.wsEnvironments = envs return o.wsEnvironments, nil } return o.wsEnvironments, nil } func (o *deployOpts) askEnv() error { if o.envName != "" { return nil } localEnvs, err := o.listWsEnvironments() if err != nil { return fmt.Errorf("get workspace environments: %w", err) } initializedEnvs, err := o.store.ListEnvironments(o.appName) if err != nil { return fmt.Errorf("get initialized environments: %w", err) } // Get uninitialized local environments and append them to the env selector call. var extraOptions []prompt.Option for _, localEnv := range localEnvs { var envIsInitted bool for _, inittedEnv := range initializedEnvs { if inittedEnv.Name == localEnv { envIsInitted = true break } } if envIsInitted { continue } extraOptions = append(extraOptions, prompt.Option{Value: localEnv, Hint: "uninitialized"}) } o.envName, err = o.sel.Environment("Select an environment to deploy to", "", o.appName, extraOptions...) if err != nil { return fmt.Errorf("get environment name: %w", err) } return nil } // checkEnvExists checks whether the environment is initialized and has a local manifest. func (o *deployOpts) checkEnvExists() error { o.envExistsInApp = true _, err := o.store.GetEnvironment(o.appName, o.envName) if err != nil { var errNotFound *config.ErrNoSuchEnvironment if !errors.As(err, &errNotFound) { return fmt.Errorf("get environment from config store: %w", err) } o.envExistsInApp = false } envs, err := o.listWsEnvironments() if err != nil { return fmt.Errorf("list environments in workspace: %w", err) } o.envExistsInWs = slices.Contains(envs, o.envName) // the desired environment doesn't actually exist. if !o.envExistsInApp && !o.envExistsInWs { log.Errorf("Environment %q does not exist in the current application or workspace. Please initialize it by running %s.\n", o.envName, color.HighlightCode("copilot env init")) return fmt.Errorf("environment %q does not exist in the workspace", o.envName) } if o.envExistsInApp && !o.envExistsInWs { log.Infof("Manifest for environment %q does not exist in the current workspace. To deploy this environment, generate a manifest with %s", o.envName, color.HighlightCode("copilot env show --manifest")) } return nil } func (o *deployOpts) maybeInitEnv() error { if o.envExistsInApp { return nil } // If no initialization flags were specified and the env wasn't initialized, ask to confirm. if !o.envExistsInApp && o.yesInitEnv == nil { v, err := o.prompt.Confirm(fmt.Sprintf("Environment %q does not exist in app %q. Initialize it?", o.envName, o.appName), "") if err != nil { return fmt.Errorf("confirm env init: %w", err) } o.yesInitEnv = aws.Bool(v) } if aws.BoolValue(o.yesInitEnv) { cmd, err := o.newInitEnvCmd(o) if err != nil { return fmt.Errorf("load env init command : %w", err) } if err = cmd.Validate(); err != nil { return err } if err = cmd.Ask(); err != nil { return err } if err = cmd.Execute(); err != nil { return err } if o.deployEnv == nil { log.Infof("Environment %q was just initialized. We'll deploy it now.\n", o.envName) o.deployEnv = aws.Bool(true) } else if !aws.BoolValue(o.deployEnv) { log.Errorf("Environment is not deployed but --%s=false was specified. Deploy the environment with %s in order to deploy a workload to it.\n", deployEnvFlag, color.HighlightCode("copilot env deploy")) return fmt.Errorf("environment %s was initialized but has not been deployed", o.envName) } return nil } log.Errorf("Environment %q does not exist in application %q and was not initialized after prompting.\n", o.envName, o.appName) return fmt.Errorf("env %s does not exist in app %s", o.envName, o.appName) } func (o *deployOpts) maybeDeployEnv() error { if !o.envExistsInWs { return nil } if aws.BoolValue(o.deployEnv) { cmd, err := o.newDeployEnvCmd(o) if err != nil { return fmt.Errorf("set up env deploy command: %w", err) } if err = cmd.Validate(); err != nil { return err } if err = cmd.Ask(); err != nil { return err } return cmd.Execute() } return nil } func (o *deployOpts) loadWkldCmd(name string) (actionCommand, error) { wl, err := o.store.GetWorkload(o.appName, name) if err != nil { return nil, fmt.Errorf("retrieve %s from application %s: %w", o.appName, name, err) } cmd, err := o.setupDeployCmd(o, name, wl.Type) if err != nil { return nil, err } if slices.Contains(manifestinfo.JobTypes(), wl.Type) { o.wlType = jobWkldType return cmd, nil } o.wlType = svcWkldType return cmd, nil } // BuildDeployCmd is the deploy command. func BuildDeployCmd() *cobra.Command { vars := deployVars{} var initEnvironment bool var deployEnvironment bool cmd := &cobra.Command{ Use: "deploy", Short: "Deploy one or more Copilot jobs or services.", Long: "Deploy one or more Copilot jobs or services.", Example: ` Deploys a service named "frontend" to a "test" environment. /code $ copilot deploy --name frontend --env test Deploys a job named "mailer" with additional resource tags to a "prod" environment. /code $ copilot deploy -n mailer -e prod --resource-tags source/revision=bb133e7,deployment/initiator=manual Initializes and deploys an environment named "test" in us-west-2 under the "default" profile with local manifest, then deploys a service named "api" /code $ copilot deploy --init-env --deploy-env --env test --name api --profile default --region us-west-2 Initializes and deploys a service named "backend" to a "prod" environment. /code $ copilot deploy --init-wkld --env prod --name backend Deploys all local, initialized workloads in no particular order. /code $ copilot deploy --all --env prod --name backend Deploys multiple workloads in a prescribed order (fe and worker, then be). /code $ copilot deploy -n fe/1 -n be/2 -n worker/1 Initializes and deploys all local workloads after deploying environment changes. /code $ copilot deploy --all --init-wkld --deploy-env -e prod`, RunE: runCmdE(func(cmd *cobra.Command, args []string) error { opts, err := newDeployOpts(vars) if err != nil { return err } if cmd.Flags().Changed(yesInitEnvFlag) { opts.yesInitEnv = aws.Bool(false) if initEnvironment { opts.yesInitEnv = aws.Bool(true) } } if cmd.Flags().Changed(deployEnvFlag) { opts.deployEnv = aws.Bool(false) if deployEnvironment { opts.deployEnv = aws.Bool(true) } } if err := opts.Run(); err != nil { return err } return nil }), } cmd.Flags().StringVarP(&vars.appName, appFlag, appFlagShort, tryReadingAppName(), appFlagDescription) cmd.Flags().StringSliceVarP(&vars.workloadNames, nameFlag, nameFlagShort, nil, workloadsFlagDescription) cmd.Flags().StringVarP(&vars.envName, envFlag, envFlagShort, "", envFlagDescription) cmd.Flags().StringVar(&vars.imageTag, imageTagFlag, "", imageTagFlagDescription) cmd.Flags().StringToStringVar(&vars.resourceTags, resourceTagsFlag, nil, resourceTagsFlagDescription) cmd.Flags().BoolVar(&vars.forceNewUpdate, forceFlag, false, forceFlagDescription) cmd.Flags().BoolVar(&vars.disableRollback, noRollbackFlag, false, noRollbackFlagDescription) cmd.Flags().BoolVar(&vars.allowWkldDowngrade, allowDowngradeFlag, false, allowDowngradeFlagDescription) cmd.Flags().BoolVar(&vars.detach, detachFlag, false, detachFlagDescription) cmd.Flags().BoolVar(&deployEnvironment, deployEnvFlag, false, deployEnvFlagDescription) cmd.Flags().BoolVar(&initEnvironment, yesInitEnvFlag, false, yesInitEnvFlagDescription) cmd.Flags().BoolVar(&vars.yesInitWkld, yesInitWorkloadFlag, false, yesInitWorkloadFlagDescription) cmd.Flags().BoolVar(&vars.deployAllWorkloads, allFlag, false, allWorkloadsFlagDescription) cmd.Flags().StringVar(&vars.profile, profileFlag, "", profileFlagDescription) cmd.Flags().StringVar(&vars.tempCreds.AccessKeyID, accessKeyIDFlag, "", accessKeyIDFlagDescription) cmd.Flags().StringVar(&vars.tempCreds.SecretAccessKey, secretAccessKeyFlag, "", secretAccessKeyFlagDescription) cmd.Flags().StringVar(&vars.tempCreds.SessionToken, sessionTokenFlag, "", sessionTokenFlagDescription) cmd.Flags().StringVar(&vars.region, regionFlag, "", envRegionTokenFlagDescription) cmd.SetUsageTemplate(template.Usage) cmd.Annotations = map[string]string{ "group": group.Release, } return cmd }