internal/pkg/cli/job_package.go (187 lines of code) (raw):
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
package cli
import (
"fmt"
"os"
"slices"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/service/ssm"
"github.com/aws/copilot-cli/internal/pkg/aws/identity"
"github.com/aws/copilot-cli/internal/pkg/version"
"github.com/aws/copilot-cli/internal/pkg/aws/sessions"
"github.com/aws/copilot-cli/internal/pkg/config"
"github.com/aws/copilot-cli/internal/pkg/exec"
"github.com/aws/copilot-cli/internal/pkg/manifest"
"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/workspace"
"github.com/spf13/afero"
"github.com/spf13/cobra"
)
const (
jobPackageJobNamePrompt = "Which job would you like to generate a CloudFormation template for?"
jobPackageEnvNamePrompt = "Which environment would you like to package this stack for?"
)
type packageJobVars struct {
name string
envName string
appName string
tag string
outputDir string
uploadAssets bool
showDiff bool
allowWkldDowngrade bool
}
type packageJobOpts struct {
packageJobVars
// Interfaces to interact with dependencies.
ws wsJobDirReader
store store
runner execRunner
sel wsSelector
prompt prompter
// Subcommand implementing svc_package's Execute()
packageCmd actionCommand
newPackageCmd func(*packageJobOpts)
}
func newPackageJobOpts(vars packageJobVars) (*packageJobOpts, error) {
sessProvider := sessions.ImmutableProvider(sessions.UserAgentExtras("job package"))
defaultSess, err := sessProvider.Default()
if err != nil {
return nil, err
}
store := config.NewSSMStore(identity.New(defaultSess), ssm.New(defaultSess), aws.StringValue(defaultSess.Config.Region))
fs := afero.NewOsFs()
ws, err := workspace.Use(fs)
if err != nil {
return nil, err
}
prompter := prompt.New()
opts := &packageJobOpts{
packageJobVars: vars,
ws: ws,
store: store,
runner: exec.NewCmd(),
sel: selector.NewLocalWorkloadSelector(prompter, store, ws, selector.OnlyInitializedWorkloads),
prompt: prompter,
}
opts.newPackageCmd = func(o *packageJobOpts) {
opts.packageCmd = &packageSvcOpts{
packageSvcVars: packageSvcVars{
name: o.name,
envName: o.envName,
appName: o.appName,
tag: o.tag,
outputDir: o.outputDir,
uploadAssets: o.uploadAssets,
allowWkldDowngrade: o.allowWkldDowngrade,
showDiff: o.showDiff,
},
runner: o.runner,
ws: ws,
store: o.store,
templateWriter: os.Stdout,
unmarshal: manifest.UnmarshalWorkload,
newInterpolator: newManifestInterpolator,
paramsWriter: discardFile{},
addonsWriter: discardFile{},
diffWriter: os.Stdout,
fs: fs,
sessProvider: sessProvider,
newStackGenerator: newWorkloadStackGenerator,
gitShortCommit: imageTagFromGit(o.runner),
templateVersion: version.LatestTemplateVersion(),
}
}
return opts, nil
}
// Validate returns an error if the values provided by the user are invalid.
func (o *packageJobOpts) Validate() error {
if o.appName == "" {
return errNoAppInWorkspace
}
if o.name != "" {
names, err := o.ws.ListJobs()
if err != nil {
return fmt.Errorf("list jobs in the workspace: %w", err)
}
if !slices.Contains(names, o.name) {
return fmt.Errorf("job '%s' does not exist in the workspace", o.name)
}
}
if o.envName != "" {
if _, err := o.store.GetEnvironment(o.appName, o.envName); err != nil {
return err
}
}
return nil
}
// Ask prompts the user for any missing required fields.
func (o *packageJobOpts) Ask() error {
if err := o.askJobName(); err != nil {
return err
}
if err := o.askEnvName(); err != nil {
return err
}
return nil
}
// Execute prints the CloudFormation template of the application for the environment.
func (o *packageJobOpts) Execute() error {
o.newPackageCmd(o)
return o.packageCmd.Execute()
}
// RecommendActions suggests recommended actions before the packaged template is used for deployment.
func (o *packageJobOpts) RecommendActions() error {
return o.packageCmd.RecommendActions()
}
func (o *packageJobOpts) askJobName() error {
if o.name != "" {
return nil
}
name, err := o.sel.Job(jobPackageJobNamePrompt, "")
if err != nil {
return fmt.Errorf("select job: %w", err)
}
o.name = name
return nil
}
func (o *packageJobOpts) askEnvName() error {
if o.envName != "" {
return nil
}
name, err := o.sel.Environment(jobPackageEnvNamePrompt, "", o.appName)
if err != nil {
return fmt.Errorf("select environment: %w", err)
}
o.envName = name
return nil
}
// buildJobPackageCmd builds the command for printing a job's CloudFormation template.
func buildJobPackageCmd() *cobra.Command {
vars := packageJobVars{}
cmd := &cobra.Command{
Use: "package",
Short: "Print the AWS CloudFormation template of a job.",
Long: `Print the CloudFormation template used to deploy a job to an environment.`,
Example: `
Print the CloudFormation template for the "report-generator" job parametrized for the "test" environment.
/code $ copilot job package -n report-generator -e test
Write the CloudFormation stack and configuration to a "infrastructure/" sub-directory instead of printing.
/startcodeblock
$ copilot job package -n report-generator -e test --output-dir ./infrastructure
$ ls ./infrastructure
report-generator-test.stack.yml report-generator-test.params.yml
/endcodeblock`,
RunE: runCmdE(func(cmd *cobra.Command, args []string) error {
opts, err := newPackageJobOpts(vars)
if err != nil {
return err
}
return run(opts)
}),
}
cmd.Flags().StringVarP(&vars.name, nameFlag, nameFlagShort, "", jobFlagDescription)
cmd.Flags().StringVarP(&vars.envName, envFlag, envFlagShort, "", envFlagDescription)
cmd.Flags().StringVarP(&vars.appName, appFlag, appFlagShort, tryReadingAppName(), appFlagDescription)
cmd.Flags().StringVar(&vars.tag, imageTagFlag, "", imageTagFlagDescription)
cmd.Flags().StringVar(&vars.outputDir, stackOutputDirFlag, "", stackOutputDirFlagDescription)
cmd.Flags().BoolVar(&vars.uploadAssets, uploadAssetsFlag, false, uploadAssetsFlagDescription)
cmd.Flags().BoolVar(&vars.showDiff, diffFlag, false, diffFlagDescription)
cmd.Flags().BoolVar(&vars.allowWkldDowngrade, allowDowngradeFlag, false, allowDowngradeFlagDescription)
cmd.MarkFlagsMutuallyExclusive(diffFlag, stackOutputDirFlag)
cmd.MarkFlagsMutuallyExclusive(diffFlag, uploadAssetsFlag)
return cmd
}