diff --git a/internal/pkg/aws/sessions/errors.go b/internal/pkg/aws/sessions/errors.go index a3fa934f5b4..132d2ace7a5 100644 --- a/internal/pkg/aws/sessions/errors.go +++ b/internal/pkg/aws/sessions/errors.go @@ -39,16 +39,11 @@ func (e *errCredRetrieval) Error() string { // RecommendActions returns recommended actions to be taken after the error. // Implements main.actionRecommender interface. func (e *errCredRetrieval) RecommendActions() string { - notice := "It looks like your credential settings are misconfigured or missing" + notice := "It looks like AWS credentials are misconfigured or missing" if e.profile != "" { - notice = fmt.Sprintf("It looks like your profile [%s] is misconfigured or missing", e.profile) + notice = fmt.Sprintf("It looks like AWS profile [%s] is misconfigured or missing", e.profile) } return fmt.Sprintf(`%s: -https://docs.aws.amazon.com/sdk-for-go/v1/developer-guide/configuring-sdk.html#specifying-credentials -- We recommend including your credentials in the shared credentials file. -- Alternatively, you can also set credentials through - * Environment Variables - * EC2 Instance Metadata (credentials only) More information: https://aws.github.io/copilot-cli/docs/credentials/`, notice) } diff --git a/internal/pkg/cli/app_show.go b/internal/pkg/cli/app_show.go index a996af17779..19ad5570576 100644 --- a/internal/pkg/cli/app_show.go +++ b/internal/pkg/cli/app_show.go @@ -217,14 +217,15 @@ func (o *showAppOpts) description() (*describe.App, error) { return nil, fmt.Errorf("get version for application %s: %w", o.name, err) } return &describe.App{ - Name: app.Name, - Version: version, - URI: app.Domain, - Envs: trimmedEnvs, - Services: trimmedSvcs, - Jobs: trimmedJobs, - Pipelines: pipelineInfo, - WkldDeployedtoEnvs: wkldDeployedtoEnvs, + Name: app.Name, + Version: version, + URI: app.Domain, + PermissionsBoundary: app.PermissionsBoundary, + Envs: trimmedEnvs, + Services: trimmedSvcs, + Jobs: trimmedJobs, + Pipelines: pipelineInfo, + WkldDeployedtoEnvs: wkldDeployedtoEnvs, }, nil } diff --git a/internal/pkg/cli/app_show_test.go b/internal/pkg/cli/app_show_test.go index b179636e6b9..03e4b4d454d 100644 --- a/internal/pkg/cli/app_show_test.go +++ b/internal/pkg/cli/app_show_test.go @@ -188,8 +188,9 @@ func TestShowAppOpts_Execute(t *testing.T) { setupMocks: func(m showAppMocks) { m.storeSvc.EXPECT().GetApplication("my-app").Return(&config.Application{ - Name: "my-app", - Domain: "example.com", + Name: "my-app", + Domain: "example.com", + PermissionsBoundary: "examplePermissionsBoundaryPolicy", }, nil) m.storeSvc.EXPECT().ListServices("my-app").Return([]*config.Workload{ { @@ -231,13 +232,14 @@ func TestShowAppOpts_Execute(t *testing.T) { m.versionGetter.EXPECT().Version().Return("v0.0.0", nil) }, - wantedContent: "{\"name\":\"my-app\",\"version\":\"v0.0.0\",\"uri\":\"example.com\",\"environments\":[{\"app\":\"\",\"name\":\"test\",\"region\":\"us-west-2\",\"accountID\":\"123456789\",\"registryURL\":\"\",\"executionRoleARN\":\"\",\"managerRoleARN\":\"\"},{\"app\":\"\",\"name\":\"prod\",\"region\":\"us-west-1\",\"accountID\":\"123456789\",\"registryURL\":\"\",\"executionRoleARN\":\"\",\"managerRoleARN\":\"\"}],\"services\":[{\"app\":\"\",\"name\":\"my-svc\",\"type\":\"lb-web-svc\"}],\"jobs\":[{\"app\":\"\",\"name\":\"my-job\",\"type\":\"Scheduled Job\"}],\"pipelines\":[{\"pipelineName\":\"my-pipeline-repo\",\"region\":\"\",\"accountId\":\"\",\"stages\":null,\"createdAt\":\"0001-01-01T00:00:00Z\",\"updatedAt\":\"0001-01-01T00:00:00Z\"},{\"pipelineName\":\"bad-goose\",\"region\":\"\",\"accountId\":\"\",\"stages\":null,\"createdAt\":\"0001-01-01T00:00:00Z\",\"updatedAt\":\"0001-01-01T00:00:00Z\"}]}\n", + wantedContent: "{\"name\":\"my-app\",\"version\":\"v0.0.0\",\"uri\":\"example.com\",\"permissionsBoundary\":\"examplePermissionsBoundaryPolicy\",\"environments\":[{\"app\":\"\",\"name\":\"test\",\"region\":\"us-west-2\",\"accountID\":\"123456789\",\"registryURL\":\"\",\"executionRoleARN\":\"\",\"managerRoleARN\":\"\"},{\"app\":\"\",\"name\":\"prod\",\"region\":\"us-west-1\",\"accountID\":\"123456789\",\"registryURL\":\"\",\"executionRoleARN\":\"\",\"managerRoleARN\":\"\"}],\"services\":[{\"app\":\"\",\"name\":\"my-svc\",\"type\":\"lb-web-svc\"}],\"jobs\":[{\"app\":\"\",\"name\":\"my-job\",\"type\":\"Scheduled Job\"}],\"pipelines\":[{\"pipelineName\":\"my-pipeline-repo\",\"region\":\"\",\"accountId\":\"\",\"stages\":null,\"createdAt\":\"0001-01-01T00:00:00Z\",\"updatedAt\":\"0001-01-01T00:00:00Z\"},{\"pipelineName\":\"bad-goose\",\"region\":\"\",\"accountId\":\"\",\"stages\":null,\"createdAt\":\"0001-01-01T00:00:00Z\",\"updatedAt\":\"0001-01-01T00:00:00Z\"}]}\n", }, "correctly shows human output": { setupMocks: func(m showAppMocks) { m.storeSvc.EXPECT().GetApplication("my-app").Return(&config.Application{ - Name: "my-app", - Domain: "example.com", + Name: "my-app", + Domain: "example.com", + PermissionsBoundary: "examplePermissionsBoundaryPolicy", }, nil) m.storeSvc.EXPECT().ListServices("my-app").Return([]*config.Workload{ { @@ -281,9 +283,10 @@ func TestShowAppOpts_Execute(t *testing.T) { wantedContent: `About - Name my-app - Version v0.0.0 (latest available: v1.1.0) - URI example.com + Name my-app + Version v0.0.0 (latest available: v1.1.0) + URI example.com + Permissions Boundary examplePermissionsBoundaryPolicy Environments @@ -310,8 +313,9 @@ Pipelines "correctly shows human output with latest version": { setupMocks: func(m showAppMocks) { m.storeSvc.EXPECT().GetApplication("my-app").Return(&config.Application{ - Name: "my-app", - Domain: "example.com", + Name: "my-app", + Domain: "example.com", + PermissionsBoundary: "examplePermissionsBoundaryPolicy", }, nil) m.storeSvc.EXPECT().ListServices("my-app").Return([]*config.Workload{ { @@ -347,9 +351,77 @@ Pipelines wantedContent: `About - Name my-app - Version v1.1.0 - URI example.com + Name my-app + Version v1.1.0 + URI example.com + Permissions Boundary examplePermissionsBoundaryPolicy + +Environments + + Name AccountID Region + ---- --------- ------ + test 123456789 us-west-2 + prod 123456789 us-west-1 + +Workloads + + Name Type Environments + ---- ---- ------------ + my-svc lb-web-svc prod, test + my-job Scheduled Job prod, test + +Pipelines + + Name + ---- +`, + }, + "correctly shows human output when URI and Permissions Boundary are empty": { + setupMocks: func(m showAppMocks) { + m.storeSvc.EXPECT().GetApplication("my-app").Return(&config.Application{ + Name: "my-app", + Domain: "", + PermissionsBoundary: "", + }, nil) + m.storeSvc.EXPECT().ListServices("my-app").Return([]*config.Workload{ + { + Name: "my-svc", + Type: "lb-web-svc", + }, + }, nil) + m.storeSvc.EXPECT().ListJobs("my-app").Return([]*config.Workload{ + { + Name: "my-job", + Type: "Scheduled Job", + }, + }, nil) + m.storeSvc.EXPECT().ListEnvironments("my-app").Return([]*config.Environment{ + { + Name: "test", + Region: "us-west-2", + AccountID: "123456789", + }, + { + Name: "prod", + AccountID: "123456789", + Region: "us-west-1", + }, + }, nil) + m.deployStore.EXPECT().ListDeployedJobs("my-app", "test").Return([]string{"my-job"}, nil) + m.deployStore.EXPECT().ListDeployedJobs("my-app", "prod").Return([]string{"my-job"}, nil) + m.deployStore.EXPECT().ListDeployedServices("my-app", "test").Return([]string{"my-svc"}, nil) + m.deployStore.EXPECT().ListDeployedServices("my-app", "prod").Return([]string{"my-svc"}, nil) + m.pipelineLister.EXPECT().ListDeployedPipelines(mockAppName).Return([]deploy.Pipeline{}, nil) + m.versionGetter.EXPECT().Version().Return(deploy.LatestAppTemplateVersion, nil) + + }, + + wantedContent: `About + + Name my-app + Version v1.1.0 + URI N/A + Permissions Boundary N/A Environments @@ -374,8 +446,9 @@ Pipelines "when service/job is not deployed": { setupMocks: func(m showAppMocks) { m.storeSvc.EXPECT().GetApplication("my-app").Return(&config.Application{ - Name: "my-app", - Domain: "example.com", + Name: "my-app", + Domain: "example.com", + PermissionsBoundary: "examplePermissionsBoundaryPolicy", }, nil) m.storeSvc.EXPECT().ListServices("my-app").Return([]*config.Workload{ { @@ -415,9 +488,10 @@ Pipelines wantedContent: `About - Name my-app - Version v1.1.0 - URI example.com + Name my-app + Version v1.1.0 + URI example.com + Permissions Boundary examplePermissionsBoundaryPolicy Environments @@ -442,8 +516,9 @@ Pipelines }, "when multiple services/jobs are deployed": { setupMocks: func(m showAppMocks) { m.storeSvc.EXPECT().GetApplication("my-app").Return(&config.Application{ - Name: "my-app", - Domain: "example.com", + Name: "my-app", + Domain: "example.com", + PermissionsBoundary: "examplePermissionsBoundaryPolicy", }, nil) m.storeSvc.EXPECT().ListServices("my-app").Return([]*config.Workload{ { @@ -504,9 +579,10 @@ Pipelines wantedContent: `About - Name my-app - Version v1.1.0 - URI example.com + Name my-app + Version v1.1.0 + URI example.com + Permissions Boundary examplePermissionsBoundaryPolicy Environments @@ -544,8 +620,9 @@ Pipelines "returns error if fail to list environment": { setupMocks: func(m showAppMocks) { m.storeSvc.EXPECT().GetApplication("my-app").Return(&config.Application{ - Name: "my-app", - Domain: "example.com", + Name: "my-app", + Domain: "example.com", + PermissionsBoundary: "examplePermissionsBoundaryPolicy", }, nil) m.storeSvc.EXPECT().ListEnvironments("my-app").Return(nil, testError) }, @@ -557,8 +634,9 @@ Pipelines setupMocks: func(m showAppMocks) { m.storeSvc.EXPECT().GetApplication("my-app").Return(&config.Application{ - Name: "my-app", - Domain: "example.com", + Name: "my-app", + Domain: "example.com", + PermissionsBoundary: "examplePermissionsBoundaryPolicy", }, nil) m.storeSvc.EXPECT().ListEnvironments("my-app").Return([]*config.Environment{ { @@ -582,8 +660,9 @@ Pipelines setupMocks: func(m showAppMocks) { m.storeSvc.EXPECT().GetApplication("my-app").Return(&config.Application{ - Name: "my-app", - Domain: "example.com", + Name: "my-app", + Domain: "example.com", + PermissionsBoundary: "examplePermissionsBoundaryPolicy", }, nil) m.storeSvc.EXPECT().ListEnvironments("my-app").Return([]*config.Environment{ { @@ -613,8 +692,9 @@ Pipelines setupMocks: func(m showAppMocks) { m.storeSvc.EXPECT().GetApplication("my-app").Return(&config.Application{ - Name: "my-app", - Domain: "example.com", + Name: "my-app", + Domain: "example.com", + PermissionsBoundary: "examplePermissionsBoundaryPolicy", }, nil) m.storeSvc.EXPECT().ListEnvironments("my-app").Return([]*config.Environment{ { @@ -653,8 +733,9 @@ Pipelines setupMocks: func(m showAppMocks) { m.storeSvc.EXPECT().GetApplication("my-app").Return(&config.Application{ - Name: "my-app", - Domain: "example.com", + Name: "my-app", + Domain: "example.com", + PermissionsBoundary: "examplePermissionsBoundaryPolicy", }, nil) m.storeSvc.EXPECT().ListEnvironments("my-app").Return([]*config.Environment{ { @@ -695,8 +776,9 @@ Pipelines setupMocks: func(m showAppMocks) { m.storeSvc.EXPECT().GetApplication("my-app").Return(&config.Application{ - Name: "my-app", - Domain: "example.com", + Name: "my-app", + Domain: "example.com", + PermissionsBoundary: "examplePermissionsBoundaryPolicy", }, nil) m.storeSvc.EXPECT().ListEnvironments("my-app").Return([]*config.Environment{ { diff --git a/internal/pkg/deploy/cloudformation/stack/testdata/environments/template-with-basic-manifest.yml b/internal/pkg/deploy/cloudformation/stack/testdata/environments/template-with-basic-manifest.yml index 42095000dd3..c906eb6fd6a 100644 --- a/internal/pkg/deploy/cloudformation/stack/testdata/environments/template-with-basic-manifest.yml +++ b/internal/pkg/deploy/cloudformation/stack/testdata/environments/template-with-basic-manifest.yml @@ -480,6 +480,8 @@ Resources: SubnetId: !Ref PublicSubnet2 NatGateway1Attachment: + Metadata: + 'aws:copilot:description': 'An Elastic IP for NAT Gateway 1' Type: AWS::EC2::EIP Condition: CreateNATGateways DependsOn: InternetGatewayAttachment @@ -515,6 +517,8 @@ Resources: RouteTableId: !Ref PrivateRouteTable1 SubnetId: !Ref PrivateSubnet1 NatGateway2Attachment: + Metadata: + 'aws:copilot:description': 'An Elastic IP for NAT Gateway 2' Type: AWS::EC2::EIP Condition: CreateNATGateways DependsOn: InternetGatewayAttachment diff --git a/internal/pkg/deploy/cloudformation/stack/testdata/environments/template-with-custom-empty-security-group.yml b/internal/pkg/deploy/cloudformation/stack/testdata/environments/template-with-custom-empty-security-group.yml index 669b4efd734..333e6d8245f 100644 --- a/internal/pkg/deploy/cloudformation/stack/testdata/environments/template-with-custom-empty-security-group.yml +++ b/internal/pkg/deploy/cloudformation/stack/testdata/environments/template-with-custom-empty-security-group.yml @@ -171,6 +171,8 @@ Resources: SubnetId: !Ref PublicSubnet2 NatGateway1Attachment: + Metadata: + 'aws:copilot:description': 'An Elastic IP for NAT Gateway 1' Type: AWS::EC2::EIP Condition: CreateNATGateways DependsOn: InternetGatewayAttachment @@ -206,6 +208,8 @@ Resources: RouteTableId: !Ref PrivateRouteTable1 SubnetId: !Ref PrivateSubnet1 NatGateway2Attachment: + Metadata: + 'aws:copilot:description': 'An Elastic IP for NAT Gateway 2' Type: AWS::EC2::EIP Condition: CreateNATGateways DependsOn: InternetGatewayAttachment diff --git a/internal/pkg/deploy/cloudformation/stack/testdata/environments/template-with-custom-security-group.yml b/internal/pkg/deploy/cloudformation/stack/testdata/environments/template-with-custom-security-group.yml index 0673cbe29d9..3ad2b44dd8b 100644 --- a/internal/pkg/deploy/cloudformation/stack/testdata/environments/template-with-custom-security-group.yml +++ b/internal/pkg/deploy/cloudformation/stack/testdata/environments/template-with-custom-security-group.yml @@ -182,6 +182,8 @@ Resources: SubnetId: !Ref PublicSubnet2 NatGateway1Attachment: + Metadata: + 'aws:copilot:description': 'An Elastic IP for NAT Gateway 1' Type: AWS::EC2::EIP Condition: CreateNATGateways DependsOn: InternetGatewayAttachment @@ -217,6 +219,8 @@ Resources: RouteTableId: !Ref PrivateRouteTable1 SubnetId: !Ref PrivateSubnet1 NatGateway2Attachment: + Metadata: + 'aws:copilot:description': 'An Elastic IP for NAT Gateway 2' Type: AWS::EC2::EIP Condition: CreateNATGateways DependsOn: InternetGatewayAttachment diff --git a/internal/pkg/deploy/cloudformation/stack/testdata/environments/template-with-default-access-log-config.yml b/internal/pkg/deploy/cloudformation/stack/testdata/environments/template-with-default-access-log-config.yml index 3a051429693..5475b10cedb 100644 --- a/internal/pkg/deploy/cloudformation/stack/testdata/environments/template-with-default-access-log-config.yml +++ b/internal/pkg/deploy/cloudformation/stack/testdata/environments/template-with-default-access-log-config.yml @@ -537,6 +537,8 @@ Resources: SubnetId: !Ref PublicSubnet2 NatGateway1Attachment: + Metadata: + 'aws:copilot:description': 'An Elastic IP for NAT Gateway 1' Type: AWS::EC2::EIP Condition: CreateNATGateways DependsOn: InternetGatewayAttachment @@ -572,6 +574,8 @@ Resources: RouteTableId: !Ref PrivateRouteTable1 SubnetId: !Ref PrivateSubnet1 NatGateway2Attachment: + Metadata: + 'aws:copilot:description': 'An Elastic IP for NAT Gateway 2' Type: AWS::EC2::EIP Condition: CreateNATGateways DependsOn: InternetGatewayAttachment diff --git a/internal/pkg/deploy/cloudformation/stack/testdata/environments/template-with-imported-certs-observability.yml b/internal/pkg/deploy/cloudformation/stack/testdata/environments/template-with-imported-certs-observability.yml index 3541cd4ed09..3170eaef594 100644 --- a/internal/pkg/deploy/cloudformation/stack/testdata/environments/template-with-imported-certs-observability.yml +++ b/internal/pkg/deploy/cloudformation/stack/testdata/environments/template-with-imported-certs-observability.yml @@ -181,6 +181,8 @@ Resources: SubnetId: !Ref PublicSubnet2 NatGateway1Attachment: + Metadata: + 'aws:copilot:description': 'An Elastic IP for NAT Gateway 1' Type: AWS::EC2::EIP Condition: CreateNATGateways DependsOn: InternetGatewayAttachment @@ -216,6 +218,8 @@ Resources: RouteTableId: !Ref PrivateRouteTable1 SubnetId: !Ref PrivateSubnet1 NatGateway2Attachment: + Metadata: + 'aws:copilot:description': 'An Elastic IP for NAT Gateway 2' Type: AWS::EC2::EIP Condition: CreateNATGateways DependsOn: InternetGatewayAttachment diff --git a/internal/pkg/deploy/cloudformation/stack/transformers.go b/internal/pkg/deploy/cloudformation/stack/transformers.go index 88fb9267082..8f6878e5ffa 100644 --- a/internal/pkg/deploy/cloudformation/stack/transformers.go +++ b/internal/pkg/deploy/cloudformation/stack/transformers.go @@ -537,6 +537,7 @@ func convertStorageOpts(wlName *string, in manifest.Storage) *template.StorageOp } return &template.StorageOpts{ Ephemeral: convertEphemeral(in.Ephemeral), + ReadonlyRootFS: in.ReadonlyRootFS, Volumes: convertVolumes(in.Volumes), MountPoints: convertMountPoints(in.Volumes), EFSPerms: convertEFSPermissions(in.Volumes), diff --git a/internal/pkg/describe/app.go b/internal/pkg/describe/app.go index 60a99dfc274..14265be48a4 100644 --- a/internal/pkg/describe/app.go +++ b/internal/pkg/describe/app.go @@ -23,14 +23,15 @@ import ( // App contains serialized parameters for an application. type App struct { - Name string `json:"name"` - Version string `json:"version"` - URI string `json:"uri"` - Envs []*config.Environment `json:"environments"` - Services []*config.Workload `json:"services"` - Jobs []*config.Workload `json:"jobs"` - Pipelines []*codepipeline.Pipeline `json:"pipelines"` - WkldDeployedtoEnvs map[string][]string `json:"-"` + Name string `json:"name"` + Version string `json:"version"` + URI string `json:"uri"` + PermissionsBoundary string `json:"permissionsBoundary"` + Envs []*config.Environment `json:"environments"` + Services []*config.Workload `json:"services"` + Jobs []*config.Workload `json:"jobs"` + Pipelines []*codepipeline.Pipeline `json:"pipelines"` + WkldDeployedtoEnvs map[string][]string `json:"-"` } // JSONString returns the stringified App struct with json format. @@ -53,8 +54,15 @@ func (a *App) HumanString() string { if deploy.LatestAppTemplateVersion != a.Version { availableVersion = color.Yellow.Sprintf("(latest available: %s)", deploy.LatestAppTemplateVersion) } + if a.URI == "" { + a.URI = "N/A" + } + if a.PermissionsBoundary == "" { + a.PermissionsBoundary = "N/A" + } fmt.Fprintf(writer, " %s\t%s %s\n", "Version", a.Version, availableVersion) fmt.Fprintf(writer, " %s\t%s\n", "URI", a.URI) + fmt.Fprintf(writer, " %s\t%s\n", "Permissions Boundary", a.PermissionsBoundary) fmt.Fprint(writer, color.Bold.Sprint("\nEnvironments\n\n")) writer.Flush() headers := []string{"Name", "AccountID", "Region"} diff --git a/internal/pkg/manifest/storage.go b/internal/pkg/manifest/storage.go index a83c79f415a..f0db4f4baf0 100644 --- a/internal/pkg/manifest/storage.go +++ b/internal/pkg/manifest/storage.go @@ -17,13 +17,14 @@ var ( // Storage represents the options for external and native storage. type Storage struct { - Ephemeral *int `yaml:"ephemeral"` - Volumes map[string]*Volume `yaml:"volumes"` // NOTE: keep the pointers because `mergo` doesn't automatically deep merge map's value unless it's a pointer type. + Ephemeral *int `yaml:"ephemeral"` + ReadonlyRootFS *bool `yaml:"readonly_fs"` + Volumes map[string]*Volume `yaml:"volumes"` // NOTE: keep the pointers because `mergo` doesn't automatically deep merge map's value unless it's a pointer type. } // IsEmpty returns empty if the struct has all zero members. func (s *Storage) IsEmpty() bool { - return s.Ephemeral == nil && s.Volumes == nil + return s.Ephemeral == nil && s.Volumes == nil && s.ReadonlyRootFS == nil } func (s *Storage) requiredEnvFeatures() []string { diff --git a/internal/pkg/manifest/storage_test.go b/internal/pkg/manifest/storage_test.go index 848a7183609..4d49d698345 100644 --- a/internal/pkg/manifest/storage_test.go +++ b/internal/pkg/manifest/storage_test.go @@ -257,6 +257,11 @@ func TestStorage_IsEmpty(t *testing.T) { in: Storage{}, wanted: true, }, + "non empty storage with ReadOnlyFS": { + in: Storage{ + ReadonlyRootFS: aws.Bool(true), + }, + }, "non empty storage": { in: Storage{ Volumes: map[string]*Volume{ diff --git a/internal/pkg/manifest/testdata/backend-svc-customhealthcheck.yml b/internal/pkg/manifest/testdata/backend-svc-customhealthcheck.yml index 82f3dc93561..d6d74c13ff5 100644 --- a/internal/pkg/manifest/testdata/backend-svc-customhealthcheck.yml +++ b/internal/pkg/manifest/testdata/backend-svc-customhealthcheck.yml @@ -26,6 +26,9 @@ memory: 512 # Amount of memory in MiB used by the task. count: 1 # Number of tasks that should be running in your service. exec: true # Enable running commands in your container. +storage: + readonly_fs: true # Limit to read-only access to mounted root filesystems by default. + # Optional fields for more advanced use-cases. # #variables: # Pass environment variables as key value pairs. diff --git a/internal/pkg/manifest/testdata/backend-svc-nohealthcheck.yml b/internal/pkg/manifest/testdata/backend-svc-nohealthcheck.yml index d8854755d4c..e23f81f8337 100644 --- a/internal/pkg/manifest/testdata/backend-svc-nohealthcheck.yml +++ b/internal/pkg/manifest/testdata/backend-svc-nohealthcheck.yml @@ -18,6 +18,9 @@ memory: 512 # Amount of memory in MiB used by the task. count: 1 # Number of tasks that should be running in your service. exec: true # Enable running commands in your container. +storage: + readonly_fs: true # Limit to read-only access to mounted root filesystems by default. + # Optional fields for more advanced use-cases. # #variables: # Pass environment variables as key value pairs. diff --git a/internal/pkg/manifest/testdata/lb-svc.yml b/internal/pkg/manifest/testdata/lb-svc.yml index acf26414154..bf8ea1bce0d 100644 --- a/internal/pkg/manifest/testdata/lb-svc.yml +++ b/internal/pkg/manifest/testdata/lb-svc.yml @@ -26,6 +26,9 @@ memory: 512 # Amount of memory in MiB used by the task. count: 1 # Number of tasks that should be running in your service. exec: true # Enable running commands in your container. +storage: + readonly_fs: true # Limit to read-only access to mounted root filesystems by default. + # Optional fields for more advanced use-cases. # #variables: # Pass environment variables as key value pairs. @@ -39,4 +42,4 @@ exec: true # Enable running commands in your container. # test: # count: 2 # Number of tasks to run for the "test" environment. # deployment: # The deployment strategy for the "test" environment. -# rolling: 'recreate' # Stops existing tasks before new ones are started for faster deployments. +# rolling: 'recreate' # Stops existing tasks before new ones are started for faster deployments. \ No newline at end of file diff --git a/internal/pkg/manifest/testdata/worker-svc-nosubscribe.yml b/internal/pkg/manifest/testdata/worker-svc-nosubscribe.yml index 2bf3924104b..47ab8e26710 100644 --- a/internal/pkg/manifest/testdata/worker-svc-nosubscribe.yml +++ b/internal/pkg/manifest/testdata/worker-svc-nosubscribe.yml @@ -16,6 +16,10 @@ memory: 512 # Amount of memory in MiB used by the task. count: 1 # Number of tasks that should be running in your service. exec: true # Enable running commands in your container. +storage: + readonly_fs: true # Limit to read-only access to mounted root filesystems by default. + + # You can register to topics from other services. # The events can be be received from an SQS queue via the env var $COPILOT_QUEUE_URI. # subscribe: @@ -36,4 +40,4 @@ exec: true # Enable running commands in your container. # test: # count: 2 # Number of tasks to run for the "test" environment. # deployment: # The deployment strategy for the "test" environment. -# rolling: 'recreate' # Stops existing tasks before new ones are started for faster deployments. +# rolling: 'recreate' # Stops existing tasks before new ones are started for faster deployments. \ No newline at end of file diff --git a/internal/pkg/manifest/testdata/worker-svc-subscribe.yml b/internal/pkg/manifest/testdata/worker-svc-subscribe.yml index 457dbabf944..45532afecd8 100644 --- a/internal/pkg/manifest/testdata/worker-svc-subscribe.yml +++ b/internal/pkg/manifest/testdata/worker-svc-subscribe.yml @@ -16,6 +16,10 @@ memory: 512 # Amount of memory in MiB used by the task. count: 1 # Number of tasks that should be running in your service. exec: true # Enable running commands in your container. +storage: + readonly_fs: true # Limit to read-only access to mounted root filesystems by default. + + # The events can be be received from an SQS queue via the env var $COPILOT_QUEUE_URI. subscribe: topics: @@ -37,4 +41,4 @@ subscribe: # test: # count: 2 # Number of tasks to run for the "test" environment. # deployment: # The deployment strategy for the "test" environment. -# rolling: 'recreate' # Stops existing tasks before new ones are started for faster deployments. +# rolling: 'recreate' # Stops existing tasks before new ones are started for faster deployments. \ No newline at end of file diff --git a/internal/pkg/manifest/testdata/worker-svc-with-default-fifo-queue.yml b/internal/pkg/manifest/testdata/worker-svc-with-default-fifo-queue.yml index 00e1da5e041..ad30eadcb99 100644 --- a/internal/pkg/manifest/testdata/worker-svc-with-default-fifo-queue.yml +++ b/internal/pkg/manifest/testdata/worker-svc-with-default-fifo-queue.yml @@ -16,6 +16,10 @@ memory: 512 # Amount of memory in MiB used by the task. count: 1 # Number of tasks that should be running in your service. exec: true # Enable running commands in your container. +storage: + readonly_fs: true # Limit to read-only access to mounted root filesystems by default. + + # The events can be be received from an SQS queue via the env var $COPILOT_QUEUE_URI. subscribe: topics: @@ -37,4 +41,4 @@ subscribe: # test: # count: 2 # Number of tasks to run for the "test" environment. # deployment: # The deployment strategy for the "test" environment. -# rolling: 'recreate' # Stops existing tasks before new ones are started for faster deployments. +# rolling: 'recreate' # Stops existing tasks before new ones are started for faster deployments. \ No newline at end of file diff --git a/internal/pkg/manifest/validate.go b/internal/pkg/manifest/validate.go index bd4a442ee8e..716efe366bd 100644 --- a/internal/pkg/manifest/validate.go +++ b/internal/pkg/manifest/validate.go @@ -162,6 +162,7 @@ func (l LoadBalancedWebServiceConfig) validate() error { if l.TaskConfig.IsWindows() { if err = validateWindows(validateWindowsOpts{ efsVolumes: l.Storage.Volumes, + readOnlyFS: l.Storage.ReadonlyRootFS, }); err != nil { return fmt.Errorf("validate Windows: %w", err) } @@ -262,6 +263,7 @@ func (b BackendServiceConfig) validate() error { if b.TaskConfig.IsWindows() { if err = validateWindows(validateWindowsOpts{ efsVolumes: b.Storage.Volumes, + readOnlyFS: b.Storage.ReadonlyRootFS, }); err != nil { return fmt.Errorf("validate Windows: %w", err) } @@ -374,6 +376,7 @@ func (w WorkerServiceConfig) validate() error { if w.TaskConfig.IsWindows() { if err = validateWindows(validateWindowsOpts{ efsVolumes: w.Storage.Volumes, + readOnlyFS: w.Storage.ReadonlyRootFS, }); err != nil { return fmt.Errorf(`validate Windows: %w`, err) } @@ -449,6 +452,7 @@ func (s ScheduledJobConfig) validate() error { if s.TaskConfig.IsWindows() { if err = validateWindows(validateWindowsOpts{ efsVolumes: s.Storage.Volumes, + readOnlyFS: s.Storage.ReadonlyRootFS, }); err != nil { return fmt.Errorf(`validate Windows: %w`, err) } @@ -1624,6 +1628,7 @@ type validateTargetContainerOpts struct { } type validateWindowsOpts struct { + readOnlyFS *bool efsVolumes map[string]*Volume } @@ -1772,6 +1777,9 @@ func isValidSubSvcName(name string) bool { } func validateWindows(opts validateWindowsOpts) error { + if aws.BoolValue(opts.readOnlyFS) { + return fmt.Errorf(`%q can not be set to 'true' when deploying a Windows container`, "readonly_fs") + } for _, volume := range opts.efsVolumes { if !volume.EmptyVolume() { return errors.New(`'EFS' is not supported when deploying a Windows container`) diff --git a/internal/pkg/manifest/validate_test.go b/internal/pkg/manifest/validate_test.go index d1d0515f7d3..f8aaeb82e94 100644 --- a/internal/pkg/manifest/validate_test.go +++ b/internal/pkg/manifest/validate_test.go @@ -3269,10 +3269,22 @@ func TestValidateWindows(t *testing.T) { }, wantedError: errors.New(`'EFS' is not supported when deploying a Windows container`), }, - "should return nil efs not specified": { + "should return nil when no fields are specified": { in: validateWindowsOpts{}, wantedError: nil, }, + "error if readonlyfs is true": { + in: validateWindowsOpts{ + readOnlyFS: aws.Bool(true), + }, + wantedError: fmt.Errorf(`%q can not be set to 'true' when deploying a Windows container`, "readonly_fs"), + }, + "should return nil if readonly_fs is false": { + in: validateWindowsOpts{ + readOnlyFS: aws.Bool(false), + }, + wantedError: nil, + }, } for name, tc := range testCases { t.Run(name, func(t *testing.T) { diff --git a/internal/pkg/template/templates/environment/partials/nat-gateways.yml b/internal/pkg/template/templates/environment/partials/nat-gateways.yml index 9b6762a3ea8..69e87a979e9 100644 --- a/internal/pkg/template/templates/environment/partials/nat-gateways.yml +++ b/internal/pkg/template/templates/environment/partials/nat-gateways.yml @@ -1,5 +1,7 @@ {{- range $ind, $cidr := .PrivateSubnetCIDRs}} NatGateway{{inc $ind}}Attachment: + Metadata: + 'aws:copilot:description': 'An Elastic IP for NAT Gateway {{inc $ind}}' Type: AWS::EC2::EIP Condition: CreateNATGateways DependsOn: InternetGatewayAttachment diff --git a/internal/pkg/template/templates/workloads/partials/cf/workload-container.yml b/internal/pkg/template/templates/workloads/partials/cf/workload-container.yml index b140f7aacc4..0034fc26634 100644 --- a/internal/pkg/template/templates/workloads/partials/cf/workload-container.yml +++ b/internal/pkg/template/templates/workloads/partials/cf/workload-container.yml @@ -60,6 +60,9 @@ StartPeriod: {{.HealthCheck.StartPeriod}} Timeout: {{.HealthCheck.Timeout}} {{- end}} +{{- if and .Storage .Storage.ReadonlyRootFS}} + ReadonlyRootFilesystem: {{.Storage.ReadonlyRootFS}} +{{- end}} {{- if .CredentialsParameter}} RepositoryCredentials: CredentialsParameter: {{.CredentialsParameter}} diff --git a/internal/pkg/template/templates/workloads/services/backend/manifest.yml b/internal/pkg/template/templates/workloads/services/backend/manifest.yml index 1e644fdb871..6b4be00d4d2 100644 --- a/internal/pkg/template/templates/workloads/services/backend/manifest.yml +++ b/internal/pkg/template/templates/workloads/services/backend/manifest.yml @@ -44,6 +44,11 @@ count: {{.Count.Value}} # Number of tasks that should be running in your s exec: true # Enable running commands in your container. {{- end}} +storage: +{{- if not .TaskConfig.IsWindows}} + readonly_fs: true # Limit to read-only access to mounted root filesystems by default. +{{- end}} + # Optional fields for more advanced use-cases. # #variables: # Pass environment variables as key value pairs. diff --git a/internal/pkg/template/templates/workloads/services/lb-web/manifest.yml b/internal/pkg/template/templates/workloads/services/lb-web/manifest.yml index 7f0c4f4b7fe..8667f3574bb 100644 --- a/internal/pkg/template/templates/workloads/services/lb-web/manifest.yml +++ b/internal/pkg/template/templates/workloads/services/lb-web/manifest.yml @@ -39,6 +39,11 @@ count: {{.Count.Value}} # Number of tasks that should be running in your s exec: true # Enable running commands in your container. {{- end}} +storage: +{{- if not .TaskConfig.IsWindows}} + readonly_fs: true # Limit to read-only access to mounted root filesystems by default. +{{- end}} + # Optional fields for more advanced use-cases. # #variables: # Pass environment variables as key value pairs. @@ -52,4 +57,4 @@ exec: true # Enable running commands in your container. # test: # count: 2 # Number of tasks to run for the "test" environment. # deployment: # The deployment strategy for the "test" environment. -# rolling: 'recreate' # Stops existing tasks before new ones are started for faster deployments. +# rolling: 'recreate' # Stops existing tasks before new ones are started for faster deployments. \ No newline at end of file diff --git a/internal/pkg/template/templates/workloads/services/worker/manifest.yml b/internal/pkg/template/templates/workloads/services/worker/manifest.yml index a2f36c7a064..7c478504886 100644 --- a/internal/pkg/template/templates/workloads/services/worker/manifest.yml +++ b/internal/pkg/template/templates/workloads/services/worker/manifest.yml @@ -34,6 +34,12 @@ count: {{.Count.Value}} # Number of tasks that should be running in your s {{- if not .TaskConfig.IsWindows }} exec: true # Enable running commands in your container. {{- end}} + +storage: +{{- if not .TaskConfig.IsWindows}} + readonly_fs: true # Limit to read-only access to mounted root filesystems by default. +{{- end}} + {{if .Subscribe}}{{- if .Subscribe.Topics}} # The events can be be received from an SQS queue via the env var $COPILOT_QUEUE_URI. subscribe: @@ -71,4 +77,4 @@ subscribe: # test: # count: 2 # Number of tasks to run for the "test" environment. # deployment: # The deployment strategy for the "test" environment. -# rolling: 'recreate' # Stops existing tasks before new ones are started for faster deployments. +# rolling: 'recreate' # Stops existing tasks before new ones are started for faster deployments. \ No newline at end of file diff --git a/internal/pkg/template/workload.go b/internal/pkg/template/workload.go index b10baf5933a..9d33608caaf 100644 --- a/internal/pkg/template/workload.go +++ b/internal/pkg/template/workload.go @@ -145,6 +145,7 @@ type SidecarStorageOpts struct { // StorageOpts holds data structures for rendering Volumes and Mount Points type StorageOpts struct { Ephemeral *int + ReadonlyRootFS *bool Volumes []*Volume MountPoints []*MountPoint EFSPerms []*EFSPermission