diff --git a/internal/pkg/aws/secretsmanager/mocks/mock_secretsmanager.go b/internal/pkg/aws/secretsmanager/mocks/mock_secretsmanager.go index c1e09ba1cb3..0fb10ffcc25 100644 --- a/internal/pkg/aws/secretsmanager/mocks/mock_secretsmanager.go +++ b/internal/pkg/aws/secretsmanager/mocks/mock_secretsmanager.go @@ -5,8 +5,10 @@ package mocks import ( + context "context" reflect "reflect" + request "github.com/aws/aws-sdk-go/aws/request" secretsmanager "github.com/aws/aws-sdk-go/service/secretsmanager" gomock "github.com/golang/mock/gomock" ) @@ -65,31 +67,36 @@ func (mr *MockapiMockRecorder) DeleteSecret(arg0 interface{}) *gomock.Call { } // DescribeSecret mocks base method. -func (m *Mockapi) DescribeSecret(input *secretsmanager.DescribeSecretInput) (*secretsmanager.DescribeSecretOutput, error) { +func (m *Mockapi) DescribeSecret(arg0 *secretsmanager.DescribeSecretInput) (*secretsmanager.DescribeSecretOutput, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "DescribeSecret", input) + ret := m.ctrl.Call(m, "DescribeSecret", arg0) ret0, _ := ret[0].(*secretsmanager.DescribeSecretOutput) ret1, _ := ret[1].(error) return ret0, ret1 } // DescribeSecret indicates an expected call of DescribeSecret. -func (mr *MockapiMockRecorder) DescribeSecret(input interface{}) *gomock.Call { +func (mr *MockapiMockRecorder) DescribeSecret(arg0 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DescribeSecret", reflect.TypeOf((*Mockapi)(nil).DescribeSecret), input) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DescribeSecret", reflect.TypeOf((*Mockapi)(nil).DescribeSecret), arg0) } -// GetSecretValue mocks base method. -func (m *Mockapi) GetSecretValue(input *secretsmanager.GetSecretValueInput) (*secretsmanager.GetSecretValueOutput, error) { +// GetSecretValueWithContext mocks base method. +func (m *Mockapi) GetSecretValueWithContext(arg0 context.Context, arg1 *secretsmanager.GetSecretValueInput, arg2 ...request.Option) (*secretsmanager.GetSecretValueOutput, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetSecretValue", input) + varargs := []interface{}{arg0, arg1} + for _, a := range arg2 { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "GetSecretValueWithContext", varargs...) ret0, _ := ret[0].(*secretsmanager.GetSecretValueOutput) ret1, _ := ret[1].(error) return ret0, ret1 } -// GetSecretValue indicates an expected call of GetSecretValue. -func (mr *MockapiMockRecorder) GetSecretValue(input interface{}) *gomock.Call { +// GetSecretValueWithContext indicates an expected call of GetSecretValueWithContext. +func (mr *MockapiMockRecorder) GetSecretValueWithContext(arg0, arg1 interface{}, arg2 ...interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetSecretValue", reflect.TypeOf((*Mockapi)(nil).GetSecretValue), input) + varargs := append([]interface{}{arg0, arg1}, arg2...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetSecretValueWithContext", reflect.TypeOf((*Mockapi)(nil).GetSecretValueWithContext), varargs...) } diff --git a/internal/pkg/aws/secretsmanager/secretsmanager.go b/internal/pkg/aws/secretsmanager/secretsmanager.go index a735345a24d..2a07d65c7e3 100644 --- a/internal/pkg/aws/secretsmanager/secretsmanager.go +++ b/internal/pkg/aws/secretsmanager/secretsmanager.go @@ -5,9 +5,11 @@ package secretsmanager import ( + "context" "fmt" "time" + "github.com/aws/aws-sdk-go/aws/request" "github.com/aws/aws-sdk-go/aws/session" "github.com/aws/aws-sdk-go/aws" @@ -15,14 +17,11 @@ import ( "github.com/aws/aws-sdk-go/service/secretsmanager" ) -// Namespace represents the AWS Secrets Manager service namespace. -const Namespace = "secretsmanager" - type api interface { CreateSecret(*secretsmanager.CreateSecretInput) (*secretsmanager.CreateSecretOutput, error) DeleteSecret(*secretsmanager.DeleteSecretInput) (*secretsmanager.DeleteSecretOutput, error) - DescribeSecret(input *secretsmanager.DescribeSecretInput) (*secretsmanager.DescribeSecretOutput, error) - GetSecretValue(input *secretsmanager.GetSecretValueInput) (*secretsmanager.GetSecretValueOutput, error) + DescribeSecret(*secretsmanager.DescribeSecretInput) (*secretsmanager.DescribeSecretOutput, error) + GetSecretValueWithContext(context.Context, *secretsmanager.GetSecretValueInput, ...request.Option) (*secretsmanager.GetSecretValueOutput, error) } // SecretsManager wraps the AWS SecretManager client. @@ -120,8 +119,8 @@ func (s *SecretsManager) DescribeSecret(secretName string) (*DescribeSecretOutpu // GetSecretValue retrieves the value of a secret from AWS Secrets Manager. // It takes the name of the secret as input and returns the corresponding value as a string. -func (s *SecretsManager) GetSecretValue(name string) (string, error) { - resp, err := s.secretsManager.GetSecretValue(&secretsmanager.GetSecretValueInput{ +func (s *SecretsManager) GetSecretValue(ctx context.Context, name string) (string, error) { + resp, err := s.secretsManager.GetSecretValueWithContext(ctx, &secretsmanager.GetSecretValueInput{ SecretId: aws.String(name), }) if err != nil { diff --git a/internal/pkg/aws/secretsmanager/secretsmanager_test.go b/internal/pkg/aws/secretsmanager/secretsmanager_test.go index 18a04a85f83..41a2a5bff29 100644 --- a/internal/pkg/aws/secretsmanager/secretsmanager_test.go +++ b/internal/pkg/aws/secretsmanager/secretsmanager_test.go @@ -5,17 +5,17 @@ package secretsmanager import ( + "context" "errors" "fmt" - "github.com/aws/aws-sdk-go/service/secretsmanager" "testing" "time" "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/aws/awserr" + "github.com/aws/aws-sdk-go/service/secretsmanager" "github.com/aws/copilot-cli/internal/pkg/aws/secretsmanager/mocks" "github.com/golang/mock/gomock" - "github.com/stretchr/testify/require" ) @@ -251,3 +251,54 @@ func TestSecretsManager_DescribeSecret(t *testing.T) { }) } } + +func TestSecretsManager_GetSecretValue(t *testing.T) { + tests := map[string]struct { + secretName string + setupMock func(m *mocks.Mockapi) + + want string + wantError string + }{ + "error": { + secretName: "asdf", + setupMock: func(m *mocks.Mockapi) { + m.EXPECT().GetSecretValueWithContext(gomock.Any(), &secretsmanager.GetSecretValueInput{ + SecretId: aws.String("asdf"), + }).Return(nil, errors.New("some error")) + }, + wantError: `get secret "asdf" from secrets manager: some error`, + }, + "success": { + secretName: "asdf", + setupMock: func(m *mocks.Mockapi) { + m.EXPECT().GetSecretValueWithContext(gomock.Any(), &secretsmanager.GetSecretValueInput{ + SecretId: aws.String("asdf"), + }).Return(&secretsmanager.GetSecretValueOutput{ + SecretString: aws.String("hi"), + }, nil) + }, + want: "hi", + }, + } + + for name, tc := range tests { + t.Run(name, func(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + api := mocks.NewMockapi(ctrl) + tc.setupMock(api) + + sm := SecretsManager{ + secretsManager: api, + } + + got, err := sm.GetSecretValue(context.Background(), tc.secretName) + if tc.wantError != "" { + require.EqualError(t, err, tc.wantError) + } + require.Equal(t, tc.want, got) + }) + } +} diff --git a/internal/pkg/aws/ssm/mocks/mock_ssm.go b/internal/pkg/aws/ssm/mocks/mock_ssm.go index 09dda821f97..3a4ab861e30 100644 --- a/internal/pkg/aws/ssm/mocks/mock_ssm.go +++ b/internal/pkg/aws/ssm/mocks/mock_ssm.go @@ -5,8 +5,10 @@ package mocks import ( + context "context" reflect "reflect" + request "github.com/aws/aws-sdk-go/aws/request" ssm "github.com/aws/aws-sdk-go/service/ssm" gomock "github.com/golang/mock/gomock" ) @@ -35,46 +37,51 @@ func (m *Mockapi) EXPECT() *MockapiMockRecorder { } // AddTagsToResource mocks base method. -func (m *Mockapi) AddTagsToResource(input *ssm.AddTagsToResourceInput) (*ssm.AddTagsToResourceOutput, error) { +func (m *Mockapi) AddTagsToResource(arg0 *ssm.AddTagsToResourceInput) (*ssm.AddTagsToResourceOutput, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "AddTagsToResource", input) + ret := m.ctrl.Call(m, "AddTagsToResource", arg0) ret0, _ := ret[0].(*ssm.AddTagsToResourceOutput) ret1, _ := ret[1].(error) return ret0, ret1 } // AddTagsToResource indicates an expected call of AddTagsToResource. -func (mr *MockapiMockRecorder) AddTagsToResource(input interface{}) *gomock.Call { +func (mr *MockapiMockRecorder) AddTagsToResource(arg0 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AddTagsToResource", reflect.TypeOf((*Mockapi)(nil).AddTagsToResource), input) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AddTagsToResource", reflect.TypeOf((*Mockapi)(nil).AddTagsToResource), arg0) } -// GetParameter mocks base method. -func (m *Mockapi) GetParameter(input *ssm.GetParameterInput) (*ssm.GetParameterOutput, error) { +// GetParameterWithContext mocks base method. +func (m *Mockapi) GetParameterWithContext(arg0 context.Context, arg1 *ssm.GetParameterInput, arg2 ...request.Option) (*ssm.GetParameterOutput, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetParameter", input) + varargs := []interface{}{arg0, arg1} + for _, a := range arg2 { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "GetParameterWithContext", varargs...) ret0, _ := ret[0].(*ssm.GetParameterOutput) ret1, _ := ret[1].(error) return ret0, ret1 } -// GetParameter indicates an expected call of GetParameter. -func (mr *MockapiMockRecorder) GetParameter(input interface{}) *gomock.Call { +// GetParameterWithContext indicates an expected call of GetParameterWithContext. +func (mr *MockapiMockRecorder) GetParameterWithContext(arg0, arg1 interface{}, arg2 ...interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetParameter", reflect.TypeOf((*Mockapi)(nil).GetParameter), input) + varargs := append([]interface{}{arg0, arg1}, arg2...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetParameterWithContext", reflect.TypeOf((*Mockapi)(nil).GetParameterWithContext), varargs...) } // PutParameter mocks base method. -func (m *Mockapi) PutParameter(input *ssm.PutParameterInput) (*ssm.PutParameterOutput, error) { +func (m *Mockapi) PutParameter(arg0 *ssm.PutParameterInput) (*ssm.PutParameterOutput, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "PutParameter", input) + ret := m.ctrl.Call(m, "PutParameter", arg0) ret0, _ := ret[0].(*ssm.PutParameterOutput) ret1, _ := ret[1].(error) return ret0, ret1 } // PutParameter indicates an expected call of PutParameter. -func (mr *MockapiMockRecorder) PutParameter(input interface{}) *gomock.Call { +func (mr *MockapiMockRecorder) PutParameter(arg0 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PutParameter", reflect.TypeOf((*Mockapi)(nil).PutParameter), input) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PutParameter", reflect.TypeOf((*Mockapi)(nil).PutParameter), arg0) } diff --git a/internal/pkg/aws/ssm/ssm.go b/internal/pkg/aws/ssm/ssm.go index d5ac6c66395..ddc00996c62 100644 --- a/internal/pkg/aws/ssm/ssm.go +++ b/internal/pkg/aws/ssm/ssm.go @@ -5,24 +5,23 @@ package ssm import ( + "context" "errors" "fmt" "sort" "github.com/aws/aws-sdk-go/aws/awserr" + "github.com/aws/aws-sdk-go/aws/request" "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/aws/session" "github.com/aws/aws-sdk-go/service/ssm" ) -// Namespace represents the AWS Systems Manager(SSM) service namespace. -const Namespace = "ssm" - type api interface { - PutParameter(input *ssm.PutParameterInput) (*ssm.PutParameterOutput, error) - AddTagsToResource(input *ssm.AddTagsToResourceInput) (*ssm.AddTagsToResourceOutput, error) - GetParameter(input *ssm.GetParameterInput) (*ssm.GetParameterOutput, error) + PutParameter(*ssm.PutParameterInput) (*ssm.PutParameterOutput, error) + AddTagsToResource(*ssm.AddTagsToResourceInput) (*ssm.AddTagsToResourceOutput, error) + GetParameterWithContext(context.Context, *ssm.GetParameterInput, ...request.Option) (*ssm.GetParameterOutput, error) } // SSM wraps an AWS SSM client. @@ -67,8 +66,8 @@ func (s *SSM) PutSecret(in PutSecretInput) (*PutSecretOutput, error) { // GetSecretValue retrieves the value of a parameter from AWS Systems Manager Parameter Store. // It takes the name of the parameter as input and returns the corresponding value as a string. -func (s *SSM) GetSecretValue(name string) (string, error) { - resp, err := s.client.GetParameter(&ssm.GetParameterInput{ +func (s *SSM) GetSecretValue(ctx context.Context, name string) (string, error) { + resp, err := s.client.GetParameterWithContext(ctx, &ssm.GetParameterInput{ Name: aws.String(name), WithDecryption: aws.Bool(true), }) diff --git a/internal/pkg/aws/ssm/ssm_test.go b/internal/pkg/aws/ssm/ssm_test.go index 50c2b40781b..6c032ac6b27 100644 --- a/internal/pkg/aws/ssm/ssm_test.go +++ b/internal/pkg/aws/ssm/ssm_test.go @@ -4,21 +4,18 @@ package ssm import ( + "context" "errors" "fmt" "testing" - "github.com/aws/aws-sdk-go/aws/awserr" - - "github.com/stretchr/testify/require" - - "github.com/aws/copilot-cli/internal/pkg/deploy" - "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/aws/awserr" "github.com/aws/aws-sdk-go/service/ssm" "github.com/aws/copilot-cli/internal/pkg/aws/ssm/mocks" - + "github.com/aws/copilot-cli/internal/pkg/deploy" "github.com/golang/mock/gomock" + "github.com/stretchr/testify/require" ) func TestSSM_PutSecret(t *testing.T) { @@ -361,3 +358,58 @@ func TestSSM_PutSecret(t *testing.T) { }) } } + +func TestSSM_GetSecretValue(t *testing.T) { + tests := map[string]struct { + secretName string + setupMock func(m *mocks.Mockapi) + + want string + wantError string + }{ + "error": { + secretName: "asdf", + setupMock: func(m *mocks.Mockapi) { + m.EXPECT().GetParameterWithContext(gomock.Any(), &ssm.GetParameterInput{ + Name: aws.String("asdf"), + WithDecryption: aws.Bool(true), + }).Return(nil, errors.New("some error")) + }, + wantError: `get parameter "asdf" from SSM: some error`, + }, + "success": { + secretName: "asdf", + setupMock: func(m *mocks.Mockapi) { + m.EXPECT().GetParameterWithContext(gomock.Any(), &ssm.GetParameterInput{ + Name: aws.String("asdf"), + WithDecryption: aws.Bool(true), + }).Return(&ssm.GetParameterOutput{ + Parameter: &ssm.Parameter{ + Value: aws.String("hi"), + }, + }, nil) + }, + want: "hi", + }, + } + + for name, tc := range tests { + t.Run(name, func(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + api := mocks.NewMockapi(ctrl) + tc.setupMock(api) + + ssm := SSM{ + client: api, + } + + got, err := ssm.GetSecretValue(context.Background(), tc.secretName) + if tc.wantError != "" { + require.EqualError(t, err, tc.wantError) + } + require.Equal(t, tc.want, got) + }) + } +} diff --git a/internal/pkg/cli/flag.go b/internal/pkg/cli/flag.go index 2cefeea9f34..4bdc0ed5d95 100644 --- a/internal/pkg/cli/flag.go +++ b/internal/pkg/cli/flag.go @@ -4,6 +4,7 @@ package cli import ( + "errors" "fmt" "strconv" "strings" @@ -61,6 +62,10 @@ const ( taskIDFlag = "task-id" containerFlag = "container" + // Run local flags + portOverrideFlag = "port-override" + envVarOverrideFlag = "env-var-override" + // Flags for CI/CD. githubURLFlag = "github-url" repoURLFlag = "url" @@ -299,6 +304,12 @@ Defaults to all logs. Only one of end-time / follow may be used.` localJobFlagDescription = "Only show jobs in the workspace." localPipelineFlagDescription = "Only show pipelines in the workspace." + // Run local + envVarOverrideFlagDescription = `Optional. Override environment variables passed to containers. +Format: [container]:KEY=VALUE. Omit container name to apply to all containers.` + portOverridesFlagDescription = `Optional. Override ports exposed by service. Format: :. +Example: --port-override 5000:80 binds localhost:5000 to the service's port 80.` + svcManifestFlagDescription = `Optional. Name of the environment in which the service was deployed; output the manifest file used for that deployment.` manifestFlagDescription = "Optional. Output the manifest file used for the deployment." @@ -410,3 +421,38 @@ are also accepted.` permissions boundary for all roles generated within the application.` prodEnvFlagDescription = "If the environment contains production services." ) + +type portOverride struct { + host string + container string +} + +type portOverrides []portOverride + +func (p *portOverrides) Set(val string) error { + err := errors.New("should be in format 8080:80") + split := strings.Split(val, ":") + if len(split) != 2 { + return err + } + if _, ok := strconv.Atoi(split[0]); ok != nil { + return err + } + if _, ok := strconv.Atoi(split[1]); ok != nil { + return err + } + + *p = append(*p, portOverride{ + host: split[0], + container: split[1], + }) + return nil +} + +func (p *portOverrides) Type() string { + return "list" +} + +func (p *portOverrides) String() string { + return fmt.Sprintf("%+v", *p) +} diff --git a/internal/pkg/cli/flag_test.go b/internal/pkg/cli/flag_test.go new file mode 100644 index 00000000000..f780eaf64b4 --- /dev/null +++ b/internal/pkg/cli/flag_test.go @@ -0,0 +1,75 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package cli + +import ( + "testing" + + "github.com/spf13/pflag" + "github.com/stretchr/testify/require" +) + +func TestFlag_portOverrides(t *testing.T) { + tests := map[string]struct { + in []string + want portOverrides + wantErr string + }{ + "error: string": { + in: []string{"--p", "asdf"}, + wantErr: `invalid argument "asdf" for "--p" flag: should be in format 8080:80`, + }, + "error: only one number": { + in: []string{"--p", "8080"}, + wantErr: `invalid argument "8080" for "--p" flag: should be in format 8080:80`, + }, + "error: host not a number": { + in: []string{"--p", "asdf:8080"}, + wantErr: `invalid argument "asdf:8080" for "--p" flag: should be in format 8080:80`, + }, + "error: container not a number": { + in: []string{"--p", "8080:asdf"}, + wantErr: `invalid argument "8080:asdf" for "--p" flag: should be in format 8080:80`, + }, + "success: no port overrides": {}, + "success: one port override": { + in: []string{"--p", "77:7777"}, + want: portOverrides{ + { + host: "77", + container: "7777", + }, + }, + }, + "success: multiple port override": { + in: []string{"--p", "77:7777", "--p=9999:50"}, + want: portOverrides{ + { + host: "77", + container: "7777", + }, + { + host: "9999", + container: "50", + }, + }, + }, + } + + for name, tc := range tests { + t.Run(name, func(t *testing.T) { + var got portOverrides + f := pflag.NewFlagSet("test", pflag.ContinueOnError) + f.Var(&got, "p", "") + + err := f.Parse(tc.in) + if tc.wantErr != "" { + require.EqualError(t, err, tc.wantErr) + return + } + require.NoError(t, err) + require.Equal(t, tc.want, got) + }) + } +} diff --git a/internal/pkg/cli/interfaces.go b/internal/pkg/cli/interfaces.go index 953e19cd2a8..2da7d232c05 100644 --- a/internal/pkg/cli/interfaces.go +++ b/internal/pkg/cli/interfaces.go @@ -185,7 +185,6 @@ type repositoryService interface { type ecsLocalClient interface { TaskDefinition(app, env, svc string) (*awsecs.TaskDefinition, error) - DecryptedSecrets(secrets []*awsecs.ContainerSecret) ([]ecs.EnvVar, error) } type logEventsWriter interface { @@ -740,3 +739,7 @@ type stackConfiguration interface { Tags() []*sdkcloudformation.Tag SerializedParameters() (string, error) } + +type secretGetter interface { + GetSecretValue(context.Context, string) (string, error) +} diff --git a/internal/pkg/cli/local_run.go b/internal/pkg/cli/local_run.go index 7c945e4f6bc..5e528b46b14 100644 --- a/internal/pkg/cli/local_run.go +++ b/internal/pkg/cli/local_run.go @@ -8,15 +8,21 @@ import ( "fmt" "os" "strconv" + "strings" + "sync" "time" "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/aws/arn" "github.com/aws/aws-sdk-go/aws/session" - "github.com/aws/aws-sdk-go/service/ssm" + sdksecretsmanager "github.com/aws/aws-sdk-go/service/secretsmanager" + sdkssm "github.com/aws/aws-sdk-go/service/ssm" "github.com/aws/copilot-cli/internal/pkg/aws/ecr" + awsecs "github.com/aws/copilot-cli/internal/pkg/aws/ecs" "github.com/aws/copilot-cli/internal/pkg/aws/identity" "github.com/aws/copilot-cli/internal/pkg/aws/secretsmanager" "github.com/aws/copilot-cli/internal/pkg/aws/sessions" + "github.com/aws/copilot-cli/internal/pkg/aws/ssm" clideploy "github.com/aws/copilot-cli/internal/pkg/cli/deploy" "github.com/aws/copilot-cli/internal/pkg/config" "github.com/aws/copilot-cli/internal/pkg/deploy" @@ -45,10 +51,12 @@ const ( ) type localRunVars struct { - wkldName string - wkldType string - appName string - envName string + wkldName string + wkldType string + appName string + envName string + envOverrides map[string]string + portOverrides portOverrides } type localRunOpts struct { @@ -56,8 +64,11 @@ type localRunOpts struct { sel deploySelector ecsLocalClient ecsLocalClient + ssm secretGetter + secretsManager secretGetter sessProvider sessionProvider sess *session.Session + envSess *session.Session targetEnv *config.Environment targetApp *config.Application store store @@ -85,7 +96,7 @@ func newLocalRunOpts(vars localRunVars) (*localRunOpts, error) { return nil, err } - store := config.NewSSMStore(identity.New(defaultSess), ssm.New(defaultSess), aws.StringValue(defaultSess.Config.Region)) + store := config.NewSSMStore(identity.New(defaultSess), sdkssm.New(defaultSess), aws.StringValue(defaultSess.Config.Region)) deployStore, err := deploy.NewStore(sessProvider, store) if err != nil { return nil, err @@ -117,7 +128,7 @@ func newLocalRunOpts(vars localRunVars) (*localRunOpts, error) { if err != nil { return fmt.Errorf("create default session with region %s: %w", o.targetEnv.Region, err) } - envSess, err := o.sessProvider.FromRole(o.targetEnv.ManagerRoleARN, o.targetEnv.Region) + o.envSess, err = o.sessProvider.FromRole(o.targetEnv.ManagerRoleARN, o.targetEnv.Region) if err != nil { return fmt.Errorf("create env session %s: %w", o.targetEnv.Region, err) } @@ -125,7 +136,9 @@ func newLocalRunOpts(vars localRunVars) (*localRunOpts, error) { // EnvManagerRole has permissions to get task def and get SSM values. // However, it doesn't have permissions to get secrets from secrets manager, // so use the default sess and *hope* they have permissions. - o.ecsLocalClient = ecs.NewWithOptions(envSess, ecs.WithSecretGetter(secretsmanager.New(defaultSessEnvRegion))) + o.ecsLocalClient = ecs.New(o.envSess) + o.ssm = ssm.New(o.envSess) + o.secretsManager = secretsmanager.New(defaultSessEnvRegion) resources, err := cloudformation.New(o.sess, cloudformation.WithProgressTracker(os.Stderr)).GetAppResourcesByRegion(o.targetApp, o.targetEnv.Region) if err != nil { @@ -212,44 +225,36 @@ func (o *localRunOpts) Execute() error { return err } + ctx := context.Background() + taskDef, err := o.ecsLocalClient.TaskDefinition(o.appName, o.envName, o.wkldName) if err != nil { return fmt.Errorf("get task definition: %w", err) } - secrets := taskDef.Secrets() - decryptedSecrets, err := o.ecsLocalClient.DecryptedSecrets(secrets) + // get env vars and secrets + envVars, err := o.getEnvVars(ctx, taskDef) if err != nil { - return fmt.Errorf("get secret values: %w", err) - } - - secretsList := make(map[string]string, len(decryptedSecrets)) - for _, s := range decryptedSecrets { - secretsList[s.Name] = s.Value - } - - envVars := make(map[string]string, len(taskDef.EnvironmentVariables())) - for _, e := range taskDef.EnvironmentVariables() { - envVars[e.Name] = e.Value + return fmt.Errorf("get env vars: %w", err) } - containerPorts := make(map[string]string, len(taskDef.ContainerDefinitions)) + // map of containerPort -> hostPort + ports := make(map[string]string) for _, container := range taskDef.ContainerDefinitions { - for _, portMapping := range container.PortMappings { - hostPort := strconv.FormatInt(aws.Int64Value(portMapping.HostPort), 10) + for _, mapping := range container.PortMappings { + host := strconv.FormatInt(aws.Int64Value(mapping.HostPort), 10) - containerPort := hostPort - if portMapping.ContainerPort == nil { - containerPort = strconv.FormatInt(aws.Int64Value(portMapping.ContainerPort), 10) + ctr := host + if mapping.ContainerPort != nil { + ctr = strconv.FormatInt(aws.Int64Value(mapping.ContainerPort), 10) } - containerPorts[hostPort] = containerPort + ports[ctr] = host } } - - envSess, err := o.sessProvider.FromRole(o.targetEnv.ManagerRoleARN, o.targetEnv.Region) - if err != nil { - return fmt.Errorf("get env session: %w", err) + for _, port := range o.portOverrides { + ports[port.container] = port.host } + mft, err := workloadManifest(&workloadManifestInput{ name: o.wkldName, appName: o.appName, @@ -257,7 +262,7 @@ func (o *localRunOpts) Execute() error { interpolator: o.newInterpolator(o.appName, o.envName), ws: o.ws, unmarshal: o.unmarshal, - sess: envSess, + sess: o.envSess, }) if err != nil { return err @@ -265,7 +270,7 @@ func (o *localRunOpts) Execute() error { o.appliedDynamicMft = mft if err := o.buildContainerImages(o); err != nil { - return err + return fmt.Errorf("build images: %w", err) } for name, imageInfo := range o.out.ImageDigests { @@ -292,12 +297,12 @@ func (o *localRunOpts) Execute() error { } o.imageInfoList = append(o.imageInfoList, sidecarImageLocations...) - err = o.runPauseContainer(context.Background(), containerPorts) + err = o.runPauseContainer(context.Background(), ports) if err != nil { - return err + return fmt.Errorf("run pause container: %w", err) } - err = o.runContainers(context.Background(), o.imageInfoList, secretsList, envVars) + err = o.runContainers(context.Background(), o.imageInfoList, envVars) if err != nil { return err } @@ -323,12 +328,17 @@ func getBuiltSidecarImageLocations(sidecars map[string]*manifest.SidecarConfig) return sideCarBuiltImageLocations } -func (o *localRunOpts) runPauseContainer(ctx context.Context, containerPorts map[string]string) error { +func (o *localRunOpts) runPauseContainer(ctx context.Context, ports map[string]string) error { + // flip ports to be host->ctr + flippedPorts := make(map[string]string, len(ports)) + for k, v := range ports { + flippedPorts[v] = k + } containerNameWithSuffix := fmt.Sprintf("%s-%s", pauseContainerName, o.containerSuffix) runOptions := &dockerengine.RunOptions{ ImageURI: pauseContainerURI, ContainerName: containerNameWithSuffix, - ContainerPorts: containerPorts, + ContainerPorts: flippedPorts, Command: []string{"sleep", "infinity"}, LogOptions: dockerengine.RunLogOptions{ Color: o.newColor(), @@ -350,7 +360,7 @@ func (o *localRunOpts) runPauseContainer(ctx context.Context, containerPorts map for { isRunning, err := o.dockerEngine.IsContainerRunning(containerNameWithSuffix) if err != nil { - errCh <- fmt.Errorf("check if pause container is running: %w", err) + errCh <- fmt.Errorf("check if container is running: %w", err) return } if isRunning { @@ -363,14 +373,13 @@ func (o *localRunOpts) runPauseContainer(ctx context.Context, containerPorts map }() err := <-errCh if err != nil { - return fmt.Errorf("run pause container: %w", err) + return err } return nil - } -func (o *localRunOpts) runContainers(ctx context.Context, imageInfoList []clideploy.ImagePerContainer, secrets map[string]string, envVars map[string]string) error { +func (o *localRunOpts) runContainers(ctx context.Context, imageInfoList []clideploy.ImagePerContainer, envVars map[string]containerEnv) error { g, ctx := errgroup.WithContext(ctx) // Iterate over the image info list and perform parallel container runs @@ -379,13 +388,23 @@ func (o *localRunOpts) runContainers(ctx context.Context, imageInfoList []clidep containerNameWithSuffix := fmt.Sprintf("%s-%s", imageInfo.ContainerName, o.containerSuffix) containerNetwork := fmt.Sprintf("%s-%s", pauseContainerName, o.containerSuffix) + + vars, secrets := make(map[string]string), make(map[string]string) + for k, v := range envVars[imageInfo.ContainerName] { + if v.Secret { + secrets[k] = v.Value + } else { + vars[k] = v.Value + } + } + // Execute each container run in a separate goroutine g.Go(func() error { runOptions := &dockerengine.RunOptions{ ImageURI: imageInfo.ImageURI, ContainerName: containerNameWithSuffix, Secrets: secrets, - EnvVars: envVars, + EnvVars: vars, ContainerNetwork: containerNetwork, LogOptions: dockerengine.RunLogOptions{ Color: o.newColor(), @@ -393,7 +412,7 @@ func (o *localRunOpts) runContainers(ctx context.Context, imageInfoList []clidep }, } if err := o.dockerEngine.Run(ctx, runOptions); err != nil { - return fmt.Errorf("run container: %w", err) + return fmt.Errorf("run container %q: %w", imageInfo.ContainerName, err) } return nil }) @@ -406,6 +425,150 @@ func (o *localRunOpts) runContainers(ctx context.Context, imageInfoList []clidep return nil } +type containerEnv map[string]envVarValue + +type envVarValue struct { + Value string + Secret bool + Override bool +} + +// getEnvVars uses env overrides passed by flags and environment variables/secrets +// specified in the Task Definition to return a set of environment varibles for each +// continer defined in the TaskDefinition. The returned map is a map of container names, +// each of which contains a mapping of key->envVarValue, which defines if the variable is a secret or not. +func (o *localRunOpts) getEnvVars(ctx context.Context, taskDef *awsecs.TaskDefinition) (map[string]containerEnv, error) { + envVars := make(map[string]containerEnv) + for _, ctr := range taskDef.ContainerDefinitions { + envVars[aws.StringValue(ctr.Name)] = make(map[string]envVarValue) + } + + for _, e := range taskDef.EnvironmentVariables() { + envVars[e.Container][e.Name] = envVarValue{ + Value: e.Value, + } + } + + if err := o.fillEnvOverrides(envVars); err != nil { + return nil, fmt.Errorf("parse env overrides: %w", err) + } + + if err := o.fillSecrets(ctx, envVars, taskDef); err != nil { + return nil, fmt.Errorf("get secrets: %w", err) + } + return envVars, nil +} + +// fillEnvOverrides parses environment variable overrides passed via flag. +// The expected format of the flag values is KEY=VALUE, with an optional container name +// in the format of [containerName]:KEY=VALUE. If the container name is omitted, +// the environment variable override is applied to all containers in the task definition. +func (o *localRunOpts) fillEnvOverrides(envVars map[string]containerEnv) error { + for k, v := range o.envOverrides { + if !strings.Contains(k, ":") { + // apply override to all containers + for ctr := range envVars { + envVars[ctr][k] = envVarValue{ + Value: v, + Override: true, + } + } + continue + } + + // only apply override to the specified container + split := strings.SplitN(k, ":", 2) + ctr, key := split[0], split[1] // len(split) will always be 2 since we know there is a ":" + if _, ok := envVars[ctr]; !ok { + return fmt.Errorf("%q targets invalid container", k) + } + envVars[ctr][key] = envVarValue{ + Value: v, + Override: true, + } + } + + return nil +} + +// fillSecrets collects non-overridden secrets from the task definition and +// makes requests to SSM and Secrets Manager to get their value. +func (o *localRunOpts) fillSecrets(ctx context.Context, envVars map[string]containerEnv, taskDef *awsecs.TaskDefinition) error { + // figure out which secrets we need to get, set value to ValueFrom + unique := make(map[string]string) + for _, s := range taskDef.Secrets() { + cur, ok := envVars[s.Container][s.Name] + if cur.Override { + // ignore secrets that were overridden + continue + } + if ok { + return fmt.Errorf("secret names must be unique, but an environment variable %q already exists", s.Name) + } + + envVars[s.Container][s.Name] = envVarValue{ + Value: s.ValueFrom, + Secret: true, + } + unique[s.ValueFrom] = "" + } + + // get value of all needed secrets + g, ctx := errgroup.WithContext(ctx) + mu := &sync.Mutex{} + mu.Lock() // lock until finished ranging over unique + for valueFrom := range unique { + valueFrom := valueFrom + g.Go(func() error { + val, err := o.getSecret(ctx, valueFrom) + if err != nil { + return fmt.Errorf("get secret %q: %w", valueFrom, err) + } + + mu.Lock() + defer mu.Unlock() + unique[valueFrom] = val + return nil + }) + } + mu.Unlock() + if err := g.Wait(); err != nil { + return err + } + + // replace secrets with resolved values + for ctr, vars := range envVars { + for key, val := range vars { + if val.Secret { + envVars[ctr][key] = envVarValue{ + Value: unique[val.Value], + Secret: true, + } + } + } + } + + return nil +} + +func (o *localRunOpts) getSecret(ctx context.Context, valueFrom string) (string, error) { + // SSM secrets can be specified as parameter name instead of an ARN. + // Default to ssm if valueFrom is not an ARN. + getter := o.ssm + if parsed, err := arn.Parse(valueFrom); err == nil { // only overwrite if successful + switch parsed.Service { + case sdkssm.ServiceName: + getter = o.ssm + case sdksecretsmanager.ServiceName: + getter = o.secretsManager + default: + return "", fmt.Errorf("invalid ARN; not a SSM or Secrets Manager ARN") + } + } + + return getter.GetSecretValue(ctx, valueFrom) +} + // BuildLocalRunCmd builds the command for running a workload locally func BuildLocalRunCmd() *cobra.Command { vars := localRunVars{} @@ -425,5 +588,7 @@ func BuildLocalRunCmd() *cobra.Command { cmd.Flags().StringVarP(&vars.wkldName, nameFlag, nameFlagShort, "", workloadFlagDescription) cmd.Flags().StringVarP(&vars.envName, envFlag, envFlagShort, "", envFlagDescription) cmd.Flags().StringVarP(&vars.appName, appFlag, appFlagShort, tryReadingAppName(), appFlagDescription) + cmd.Flags().Var(&vars.portOverrides, portOverrideFlag, portOverridesFlagDescription) + cmd.Flags().StringToStringVar(&vars.envOverrides, envVarOverrideFlag, nil, envVarOverrideFlagDescription) return cmd } diff --git a/internal/pkg/cli/local_run_test.go b/internal/pkg/cli/local_run_test.go index 30eb340bbb9..58fc8c0e2d3 100644 --- a/internal/pkg/cli/local_run_test.go +++ b/internal/pkg/cli/local_run_test.go @@ -4,18 +4,19 @@ package cli import ( + "context" "errors" "fmt" "testing" "github.com/aws/aws-sdk-go/aws" - "github.com/aws/aws-sdk-go/aws/session" - ecsapi "github.com/aws/aws-sdk-go/service/ecs" + sdkecs "github.com/aws/aws-sdk-go/service/ecs" + "github.com/aws/copilot-cli/internal/pkg/aws/ecs" awsecs "github.com/aws/copilot-cli/internal/pkg/aws/ecs" clideploy "github.com/aws/copilot-cli/internal/pkg/cli/deploy" "github.com/aws/copilot-cli/internal/pkg/cli/mocks" "github.com/aws/copilot-cli/internal/pkg/config" - "github.com/aws/copilot-cli/internal/pkg/ecs" + "github.com/aws/copilot-cli/internal/pkg/docker/dockerengine" "github.com/aws/copilot-cli/internal/pkg/manifest" "github.com/aws/copilot-cli/internal/pkg/term/selector" "github.com/fatih/color" @@ -32,27 +33,20 @@ type localRunAskMocks struct { func TestLocalRunOpts_Validate(t *testing.T) { testCases := map[string]struct { - inputAppName string - setupMocks func(m *localRunAskMocks) - wantedAppName string - wantedError error + inAppName string + setupMocks func(m *localRunAskMocks) + wantAppName string + wantError error }{ "no app in workspace": { - wantedError: errNoAppInWorkspace, + wantError: errNoAppInWorkspace, }, "fail to read the application from SSM store": { - inputAppName: "testApp", + inAppName: "testApp", setupMocks: func(m *localRunAskMocks) { m.store.EXPECT().GetApplication("testApp").Return(nil, testError) }, - wantedError: fmt.Errorf("get application testApp: %w", testError), - }, - "successful validation": { - inputAppName: "testApp", - setupMocks: func(m *localRunAskMocks) { - m.store.EXPECT().GetApplication("testApp").Return(&config.Application{Name: "testApp"}, nil) - }, - wantedAppName: "testApp", + wantError: fmt.Errorf("get application testApp: %w", testError), }, } for name, tc := range testCases { @@ -68,7 +62,7 @@ func TestLocalRunOpts_Validate(t *testing.T) { } opts := localRunOpts{ localRunVars: localRunVars{ - appName: tc.inputAppName, + appName: tc.inAppName, }, store: m.store, } @@ -76,8 +70,8 @@ func TestLocalRunOpts_Validate(t *testing.T) { err := opts.Validate() // THEN - if tc.wantedError != nil { - require.EqualError(t, err, tc.wantedError.Error()) + if tc.wantError != nil { + require.EqualError(t, err, tc.wantError.Error()) } else { require.NoError(t, err) } @@ -198,15 +192,17 @@ func TestLocalRunOpts_Ask(t *testing.T) { } type localRunExecuteMocks struct { - ecsLocalClient *mocks.MockecsLocalClient - store *mocks.Mockstore - sessProvider *mocks.MocksessionProvider - mockInterpolator *mocks.Mockinterpolator - mockWsReader *mocks.MockwsWlDirReader - mockMft *mockWorkloadMft - mockRunner *mocks.MockexecRunner - mockDockerEngine *mocks.MockdockerEngineRunner - mockrepositorySerivce *mocks.MockrepositoryService + ecsLocalClient *mocks.MockecsLocalClient + store *mocks.Mockstore + sessProvider *mocks.MocksessionProvider + interpolator *mocks.Mockinterpolator + ws *mocks.MockwsWlDirReader + mockMft *mockWorkloadMft + mockRunner *mocks.MockexecRunner + dockerEngine *mocks.MockdockerEngineRunner + repository *mocks.MockrepositoryService + ssm *mocks.MocksecretGetter + secretsManager *mocks.MocksecretGetter } func TestLocalRunOpts_Execute(t *testing.T) { @@ -219,62 +215,132 @@ func TestLocalRunOpts_Execute(t *testing.T) { testContainerName = "testConatiner" ) - mockContainerSuffix := fmt.Sprintf("%s-%s-%s", testAppName, testEnvName, testWkldName) - mockPauseContainerName := pauseContainerName + "-" + mockContainerSuffix - mockApp := config.Application{ Name: "testApp", } - mockEnv := config.Environment{ - App: "testApp", - Name: "testEnv", - Region: "us-test", - AccountID: "123456789", + App: "testApp", + Name: "testEnv", + Region: "us-test", + AccountID: "123456789", + ManagerRoleARN: "arn::env-manager", } - mockDecryptedSecrets := []ecs.EnvVar{ - { - Name: "my-secret", - Value: "Password123", - }, { - Name: "secret2", - Value: "admin123", - }, - } + mockContainerSuffix := fmt.Sprintf("%s-%s-%s", testAppName, testEnvName, testWkldName) + mockPauseContainerName := pauseContainerName + "-" + mockContainerSuffix mockImageInfoList := []clideploy.ImagePerContainer{ { - ContainerName: testWkldName, + ContainerName: "foo", ImageURI: "image1", }, { - ContainerName: "testSvc", + ContainerName: "bar", ImageURI: "image2", }, } - var taskDefinition = &awsecs.TaskDefinition{ - ContainerDefinitions: []*ecsapi.ContainerDefinition{ + taskDef := &ecs.TaskDefinition{ + ContainerDefinitions: []*sdkecs.ContainerDefinition{ + { + Name: aws.String("foo"), + Environment: []*sdkecs.KeyValuePair{ + { + Name: aws.String("FOO_VAR"), + Value: aws.String("foo-value"), + }, + }, + Secrets: []*sdkecs.Secret{ + { + Name: aws.String("SHARED_SECRET"), + ValueFrom: aws.String("mysecret"), + }, + }, + PortMappings: []*sdkecs.PortMapping{ + { + HostPort: aws.Int64(80), + ContainerPort: aws.Int64(8080), + }, + { + HostPort: aws.Int64(9999), + }, + }, + }, { - Name: aws.String("container"), - Environment: []*ecsapi.KeyValuePair{ + Name: aws.String("bar"), + Environment: []*sdkecs.KeyValuePair{ + { + Name: aws.String("BAR_VAR"), + Value: aws.String("bar-value"), + }, + }, + Secrets: []*sdkecs.Secret{ + { + Name: aws.String("SHARED_SECRET"), + ValueFrom: aws.String("mysecret"), + }, + }, + PortMappings: []*sdkecs.PortMapping{ { - Name: aws.String("COPILOT_SERVICE_NAME"), - Value: aws.String("testWkld"), + HostPort: aws.Int64(10000), }, { - Name: aws.String("COPILOT_ENVIRONMENT_NAME"), - Value: aws.String("testEnv"), + HostPort: aws.Int64(77), + ContainerPort: aws.Int64(7777), }, }, }, }, } + expectedRunPauseArgs := &dockerengine.RunOptions{ + ImageURI: pauseContainerURI, + ContainerName: mockPauseContainerName, + ContainerPorts: map[string]string{ + "80": "8080", + "999": "9999", + "10000": "10000", + "777": "7777", + }, + Command: []string{"sleep", "infinity"}, + LogOptions: dockerengine.RunLogOptions{ + LinePrefix: "[pause] ", + }, + } + expectedRunFooArgs := &dockerengine.RunOptions{ + ContainerName: "foo" + "-" + mockContainerSuffix, + ImageURI: "image1", + EnvVars: map[string]string{ + "FOO_VAR": "foo-value", + }, + Secrets: map[string]string{ + "SHARED_SECRET": "secretvalue", + }, + ContainerNetwork: mockPauseContainerName, + LogOptions: dockerengine.RunLogOptions{ + LinePrefix: "[foo] ", + }, + } + expectedRunBarArgs := &dockerengine.RunOptions{ + ContainerName: "bar" + "-" + mockContainerSuffix, + ImageURI: "image2", + EnvVars: map[string]string{ + "BAR_VAR": "bar-value", + }, + Secrets: map[string]string{ + "SHARED_SECRET": "secretvalue", + }, + ContainerNetwork: mockPauseContainerName, + LogOptions: dockerengine.RunLogOptions{ + LinePrefix: "[bar] ", + }, + } testCases := map[string]struct { - inputAppName string - inputEnvName string - inputWkldName string + inputAppName string + inputEnvName string + inputWkldName string + inputEnvOverrides map[string]string + inputPortOverrides []string + buildImagesError error setupMocks func(m *localRunExecuteMocks) wantedWkldName string @@ -291,131 +357,138 @@ func TestLocalRunOpts_Execute(t *testing.T) { }, wantedError: fmt.Errorf("get task definition: %w", testError), }, - "error decryting secrets from task definition": { + "error getting env vars due to bad override": { inputAppName: testAppName, inputWkldName: testWkldName, inputEnvName: testEnvName, + inputEnvOverrides: map[string]string{ + "bad:OVERRIDE": "i fail", + }, setupMocks: func(m *localRunExecuteMocks) { - m.ecsLocalClient.EXPECT().TaskDefinition(testAppName, testEnvName, testWkldName).Return(taskDefinition, nil) - m.ecsLocalClient.EXPECT().DecryptedSecrets(gomock.Any()).Return(nil, testError) + m.ecsLocalClient.EXPECT().TaskDefinition(testAppName, testEnvName, testWkldName).Return(taskDef, nil) }, - wantedError: fmt.Errorf("get secret values: %w", testError), + wantedError: errors.New(`get env vars: parse env overrides: "bad:OVERRIDE" targets invalid container`), }, - "error getting the session configured for the input role and region": { + "error reading workload manifest": { inputAppName: testAppName, inputWkldName: testWkldName, inputEnvName: testEnvName, setupMocks: func(m *localRunExecuteMocks) { - m.ecsLocalClient.EXPECT().TaskDefinition(testAppName, testEnvName, testWkldName).Return(taskDefinition, nil) - m.ecsLocalClient.EXPECT().DecryptedSecrets(gomock.Any()).Return(mockDecryptedSecrets, nil) - m.sessProvider.EXPECT().FromRole(gomock.Any(), gomock.Any()).Return(nil, testError) + m.ecsLocalClient.EXPECT().TaskDefinition(testAppName, testEnvName, testWkldName).Return(taskDef, nil) + m.ssm.EXPECT().GetSecretValue(gomock.Any(), "mysecret").Return("secretvalue", nil) + m.ws.EXPECT().ReadWorkloadManifest(testWkldName).Return(nil, errors.New("some error")) }, - wantedError: fmt.Errorf("get env session: %w", testError), + wantedError: errors.New(`read manifest file for testWkld: some error`), }, - "error reading workload manifest": { + "error interpolating workload manifest": { inputAppName: testAppName, inputWkldName: testWkldName, inputEnvName: testEnvName, setupMocks: func(m *localRunExecuteMocks) { - m.ecsLocalClient.EXPECT().TaskDefinition(testAppName, testEnvName, testWkldName).Return(taskDefinition, nil) - m.ecsLocalClient.EXPECT().DecryptedSecrets(gomock.Any()).Return(mockDecryptedSecrets, nil) - m.sessProvider.EXPECT().FromRole(gomock.Any(), gomock.Any()).Return(&session.Session{ - Config: &aws.Config{ - Region: aws.String("us-test"), - }, - }, nil) - m.mockWsReader.EXPECT().ReadWorkloadManifest(testWkldName).Return(nil, testError) + m.ecsLocalClient.EXPECT().TaskDefinition(testAppName, testEnvName, testWkldName).Return(taskDef, nil) + m.ssm.EXPECT().GetSecretValue(gomock.Any(), "mysecret").Return("secretvalue", nil) + m.ws.EXPECT().ReadWorkloadManifest(testWkldName).Return([]byte(""), nil) + m.interpolator.EXPECT().Interpolate("").Return("", errors.New("some error")) + }, + wantedError: errors.New(`interpolate environment variables for testWkld manifest: some error`), + }, + "error building container images": { + inputAppName: testAppName, + inputWkldName: testWkldName, + inputEnvName: testEnvName, + buildImagesError: errors.New("some error"), + setupMocks: func(m *localRunExecuteMocks) { + m.ecsLocalClient.EXPECT().TaskDefinition(testAppName, testEnvName, testWkldName).Return(taskDef, nil) + m.ssm.EXPECT().GetSecretValue(gomock.Any(), "mysecret").Return("secretvalue", nil) + m.ws.EXPECT().ReadWorkloadManifest(testWkldName).Return([]byte(""), nil) + m.interpolator.EXPECT().Interpolate("").Return("", nil) }, - wantedError: fmt.Errorf("read manifest file for %s: %w", testWkldName, testError), + wantedError: errors.New(`build images: some error`), }, - "error if failed to interpolate workload manifest": { + "error if fail to run pause container": { inputAppName: testAppName, inputWkldName: testWkldName, inputEnvName: testEnvName, setupMocks: func(m *localRunExecuteMocks) { - m.ecsLocalClient.EXPECT().TaskDefinition(testAppName, testEnvName, testWkldName).Return(taskDefinition, nil) - m.ecsLocalClient.EXPECT().DecryptedSecrets(gomock.Any()).Return(mockDecryptedSecrets, nil) - m.sessProvider.EXPECT().FromRole(gomock.Any(), gomock.Any()).Return(&session.Session{ - Config: &aws.Config{ - Region: aws.String("us-test"), - }, - }, nil) - m.mockWsReader.EXPECT().ReadWorkloadManifest(testWkldName).Return([]byte(""), nil) - m.mockInterpolator.EXPECT().Interpolate("").Return("", testError) + m.ecsLocalClient.EXPECT().TaskDefinition(testAppName, testEnvName, testWkldName).Return(taskDef, nil) + m.ssm.EXPECT().GetSecretValue(gomock.Any(), "mysecret").Return("secretvalue", nil) + m.ws.EXPECT().ReadWorkloadManifest(testWkldName).Return([]byte(""), nil) + m.interpolator.EXPECT().Interpolate("").Return("", nil) + m.dockerEngine.EXPECT().Run(gomock.Any(), expectedRunPauseArgs).Return(errors.New("some error")) + m.dockerEngine.EXPECT().IsContainerRunning(mockPauseContainerName).Return(false, nil).AnyTimes() }, - wantedError: fmt.Errorf("interpolate environment variables for %s manifest: %w", testWkldName, testError), + wantedError: errors.New(`run pause container: some error`), }, - "return error if failed to run the pause container": { + "error if fail to check if pause container running": { inputAppName: testAppName, inputWkldName: testWkldName, inputEnvName: testEnvName, setupMocks: func(m *localRunExecuteMocks) { - m.ecsLocalClient.EXPECT().TaskDefinition(testAppName, testEnvName, testWkldName).Return(taskDefinition, nil) - m.ecsLocalClient.EXPECT().DecryptedSecrets(gomock.Any()).Return(mockDecryptedSecrets, nil) - m.sessProvider.EXPECT().FromRole(gomock.Any(), gomock.Any()).Return(&session.Session{ - Config: &aws.Config{ - Region: aws.String("us-test"), - }, - }, nil) - m.mockWsReader.EXPECT().ReadWorkloadManifest(testWkldName).Return([]byte(""), nil) - m.mockInterpolator.EXPECT().Interpolate("").Return("", nil) - m.mockMft = &mockWorkloadMft{ - mockRequiredEnvironmentFeatures: func() []string { - return []string{"mockFeature1"} - }, - } - m.mockDockerEngine.EXPECT().IsContainerRunning(mockPauseContainerName).AnyTimes() - m.mockDockerEngine.EXPECT().Run(gomock.Any(), gomock.Any()).Return(testError) + m.ecsLocalClient.EXPECT().TaskDefinition(testAppName, testEnvName, testWkldName).Return(taskDef, nil) + m.ssm.EXPECT().GetSecretValue(gomock.Any(), "mysecret").Return("secretvalue", nil) + m.ws.EXPECT().ReadWorkloadManifest(testWkldName).Return([]byte(""), nil) + m.interpolator.EXPECT().Interpolate("").Return("", nil) + runCalled := make(chan struct{}) + isRunningCalled := make(chan struct{}) + m.dockerEngine.EXPECT().Run(gomock.Any(), expectedRunPauseArgs).DoAndReturn(func(ctx context.Context, opts *dockerengine.RunOptions) error { + close(runCalled) + <-isRunningCalled + return nil + }) + m.dockerEngine.EXPECT().IsContainerRunning(mockPauseContainerName).DoAndReturn(func(name string) (bool, error) { + <-runCalled + defer close(isRunningCalled) + return false, errors.New("some error") + }) }, - wantedError: fmt.Errorf("run pause container: %w", testError), + wantedError: errors.New(`run pause container: check if container is running: some error`), }, - "return error if failed to run service containers": { + "error if fail to run service container": { inputAppName: testAppName, inputWkldName: testWkldName, inputEnvName: testEnvName, setupMocks: func(m *localRunExecuteMocks) { - m.ecsLocalClient.EXPECT().TaskDefinition(testAppName, testEnvName, testWkldName).Return(taskDefinition, nil) - m.ecsLocalClient.EXPECT().DecryptedSecrets(gomock.Any()).Return(mockDecryptedSecrets, nil) - m.sessProvider.EXPECT().FromRole(gomock.Any(), gomock.Any()).Return(&session.Session{ - Config: &aws.Config{ - Region: aws.String("us-test"), - }, - }, nil) - m.mockWsReader.EXPECT().ReadWorkloadManifest(testWkldName).Return([]byte(""), nil) - m.mockInterpolator.EXPECT().Interpolate("").Return("", nil) - m.mockMft = &mockWorkloadMft{ - mockRequiredEnvironmentFeatures: func() []string { - return []string{"mockFeature1"} - }, - } - m.mockDockerEngine.EXPECT().Run(gomock.Any(), gomock.Any()).Return(nil).Times(2) - m.mockDockerEngine.EXPECT().IsContainerRunning(mockPauseContainerName).Return(true, nil) - m.mockDockerEngine.EXPECT().Run(gomock.Any(), gomock.Any()).Return(testError) + m.ecsLocalClient.EXPECT().TaskDefinition(testAppName, testEnvName, testWkldName).Return(taskDef, nil) + m.ssm.EXPECT().GetSecretValue(gomock.Any(), "mysecret").Return("secretvalue", nil) + m.ws.EXPECT().ReadWorkloadManifest(testWkldName).Return([]byte(""), nil) + m.interpolator.EXPECT().Interpolate("").Return("", nil) + + runCalled := make(chan struct{}) + m.dockerEngine.EXPECT().Run(gomock.Any(), expectedRunPauseArgs).DoAndReturn(func(ctx context.Context, opts *dockerengine.RunOptions) error { + close(runCalled) + return nil + }) + m.dockerEngine.EXPECT().IsContainerRunning(mockPauseContainerName).DoAndReturn(func(name string) (bool, error) { + <-runCalled + return true, nil + }) + m.dockerEngine.EXPECT().Run(gomock.Any(), expectedRunFooArgs).Return(errors.New("some error")) + m.dockerEngine.EXPECT().Run(gomock.Any(), expectedRunBarArgs).Return(nil) }, - wantedError: fmt.Errorf("run container: %w", testError), + wantedError: errors.New(`run container "foo": some error`), }, - "successfully run all the containers": { + "success": { inputAppName: testAppName, inputWkldName: testWkldName, inputEnvName: testEnvName, setupMocks: func(m *localRunExecuteMocks) { - m.ecsLocalClient.EXPECT().TaskDefinition(testAppName, testEnvName, testWkldName).Return(taskDefinition, nil) - m.ecsLocalClient.EXPECT().DecryptedSecrets(gomock.Any()).Return(mockDecryptedSecrets, nil) - m.sessProvider.EXPECT().FromRole(gomock.Any(), gomock.Any()).Return(&session.Session{ - Config: &aws.Config{ - Region: aws.String("us-test"), - }, - }, nil) - m.mockWsReader.EXPECT().ReadWorkloadManifest(testWkldName).Return([]byte(""), nil) - m.mockInterpolator.EXPECT().Interpolate("").Return("", nil) - m.mockMft = &mockWorkloadMft{ - mockRequiredEnvironmentFeatures: func() []string { - return []string{"mockFeature1"} - }, - } - m.mockDockerEngine.EXPECT().Run(gomock.Any(), gomock.Any()).Return(nil).Times(3) - m.mockDockerEngine.EXPECT().IsContainerRunning(mockPauseContainerName).Return(true, nil) + m.ecsLocalClient.EXPECT().TaskDefinition(testAppName, testEnvName, testWkldName).Return(taskDef, nil) + m.ssm.EXPECT().GetSecretValue(gomock.Any(), "mysecret").Return("secretvalue", nil) + m.ws.EXPECT().ReadWorkloadManifest(testWkldName).Return([]byte(""), nil) + m.interpolator.EXPECT().Interpolate("").Return("", nil) + + runCalled := make(chan struct{}) + m.dockerEngine.EXPECT().Run(gomock.Any(), expectedRunPauseArgs).DoAndReturn(func(ctx context.Context, opts *dockerengine.RunOptions) error { + close(runCalled) + return nil + }) + m.dockerEngine.EXPECT().IsContainerRunning(mockPauseContainerName).DoAndReturn(func(name string) (bool, error) { + <-runCalled + return true, nil + }) + m.dockerEngine.EXPECT().Run(gomock.Any(), expectedRunFooArgs).Return(nil) + m.dockerEngine.EXPECT().Run(gomock.Any(), expectedRunBarArgs).Return(nil) }, }, } @@ -425,24 +498,37 @@ func TestLocalRunOpts_Execute(t *testing.T) { ctrl := gomock.NewController(t) defer ctrl.Finish() m := &localRunExecuteMocks{ - ecsLocalClient: mocks.NewMockecsLocalClient(ctrl), - store: mocks.NewMockstore(ctrl), - sessProvider: mocks.NewMocksessionProvider(ctrl), - mockInterpolator: mocks.NewMockinterpolator(ctrl), - mockWsReader: mocks.NewMockwsWlDirReader(ctrl), - mockRunner: mocks.NewMockexecRunner(ctrl), - mockDockerEngine: mocks.NewMockdockerEngineRunner(ctrl), - mockrepositorySerivce: mocks.NewMockrepositoryService(ctrl), + ecsLocalClient: mocks.NewMockecsLocalClient(ctrl), + ssm: mocks.NewMocksecretGetter(ctrl), + secretsManager: mocks.NewMocksecretGetter(ctrl), + store: mocks.NewMockstore(ctrl), + sessProvider: mocks.NewMocksessionProvider(ctrl), + interpolator: mocks.NewMockinterpolator(ctrl), + ws: mocks.NewMockwsWlDirReader(ctrl), + mockRunner: mocks.NewMockexecRunner(ctrl), + dockerEngine: mocks.NewMockdockerEngineRunner(ctrl), + repository: mocks.NewMockrepositoryService(ctrl), } tc.setupMocks(m) opts := localRunOpts{ localRunVars: localRunVars{ - appName: tc.inputAppName, - wkldName: tc.inputWkldName, - envName: tc.inputEnvName, + appName: tc.inputAppName, + wkldName: tc.inputWkldName, + envName: tc.inputEnvName, + envOverrides: tc.inputEnvOverrides, + portOverrides: portOverrides{ + { + host: "777", + container: "7777", + }, + { + host: "999", + container: "9999", + }, + }, }, newInterpolator: func(app, env string) interpolator { - return m.mockInterpolator + return m.interpolator }, unmarshal: func(b []byte) (manifest.DynamicWorkload, error) { return m.mockMft, nil @@ -451,16 +537,18 @@ func TestLocalRunOpts_Execute(t *testing.T) { return nil }, buildContainerImages: func(o *localRunOpts) error { - return nil + return tc.buildImagesError }, imageInfoList: mockImageInfoList, - ws: m.mockWsReader, + ws: m.ws, ecsLocalClient: m.ecsLocalClient, + ssm: m.ssm, + secretsManager: m.secretsManager, store: m.store, sessProvider: m.sessProvider, cmd: m.mockRunner, - dockerEngine: m.mockDockerEngine, - repository: m.mockrepositorySerivce, + dockerEngine: m.dockerEngine, + repository: m.repository, targetEnv: &mockEnv, targetApp: &mockApp, containerSuffix: mockContainerSuffix, @@ -480,3 +568,333 @@ func TestLocalRunOpts_Execute(t *testing.T) { }) } } + +func TestLocalRunOpts_getEnvVars(t *testing.T) { + newVar := func(v string, overridden, secret bool) envVarValue { + return envVarValue{ + Value: v, + Override: overridden, + Secret: secret, + } + } + + tests := map[string]struct { + taskDef *awsecs.TaskDefinition + envOverrides map[string]string + setupMocks func(m *localRunExecuteMocks) + + want map[string]containerEnv + wantError string + }{ + "invalid container in env override": { + taskDef: &awsecs.TaskDefinition{}, + envOverrides: map[string]string{ + "bad:OVERRIDE": "bad", + }, + wantError: `parse env overrides: "bad:OVERRIDE" targets invalid container`, + }, + "overrides parsed and applied correctly": { + taskDef: &awsecs.TaskDefinition{ + ContainerDefinitions: []*sdkecs.ContainerDefinition{ + { + Name: aws.String("foo"), + }, + { + Name: aws.String("bar"), + }, + }, + }, + envOverrides: map[string]string{ + "OVERRIDE_ALL": "all", + "foo:OVERRIDE": "foo", + "bar:OVERRIDE": "bar", + }, + want: map[string]containerEnv{ + "foo": { + "OVERRIDE_ALL": newVar("all", true, false), + "OVERRIDE": newVar("foo", true, false), + }, + "bar": { + "OVERRIDE_ALL": newVar("all", true, false), + "OVERRIDE": newVar("bar", true, false), + }, + }, + }, + "overrides merged with existing env vars correctly": { + taskDef: &awsecs.TaskDefinition{ + ContainerDefinitions: []*sdkecs.ContainerDefinition{ + { + Name: aws.String("foo"), + Environment: []*sdkecs.KeyValuePair{ + { + Name: aws.String("RANDOM_FOO"), + Value: aws.String("foo"), + }, + { + Name: aws.String("OVERRIDE_ALL"), + Value: aws.String("bye"), + }, + { + Name: aws.String("OVERRIDE"), + Value: aws.String("bye"), + }, + }, + }, + { + Name: aws.String("bar"), + Environment: []*sdkecs.KeyValuePair{ + { + Name: aws.String("RANDOM_BAR"), + Value: aws.String("bar"), + }, + { + Name: aws.String("OVERRIDE_ALL"), + Value: aws.String("bye"), + }, + { + Name: aws.String("OVERRIDE"), + Value: aws.String("bye"), + }, + }, + }, + }, + }, + envOverrides: map[string]string{ + "OVERRIDE_ALL": "all", + "foo:OVERRIDE": "foo", + "bar:OVERRIDE": "bar", + }, + want: map[string]containerEnv{ + "foo": { + "RANDOM_FOO": newVar("foo", false, false), + "OVERRIDE_ALL": newVar("all", true, false), + "OVERRIDE": newVar("foo", true, false), + }, + "bar": { + "RANDOM_BAR": newVar("bar", false, false), + "OVERRIDE_ALL": newVar("all", true, false), + "OVERRIDE": newVar("bar", true, false), + }, + }, + }, + "error getting secret": { + taskDef: &awsecs.TaskDefinition{ + ContainerDefinitions: []*sdkecs.ContainerDefinition{ + { + Name: aws.String("foo"), + Secrets: []*sdkecs.Secret{ + { + Name: aws.String("SECRET"), + ValueFrom: aws.String("defaultSSM"), + }, + }, + }, + }, + }, + setupMocks: func(m *localRunExecuteMocks) { + m.ssm.EXPECT().GetSecretValue(gomock.Any(), "defaultSSM").Return("", errors.New("some error")) + }, + wantError: `get secrets: get secret "defaultSSM": some error`, + }, + "error getting secret if invalid arn": { + taskDef: &awsecs.TaskDefinition{ + ContainerDefinitions: []*sdkecs.ContainerDefinition{ + { + Name: aws.String("foo"), + Secrets: []*sdkecs.Secret{ + { + Name: aws.String("SECRET"), + ValueFrom: aws.String("arn:aws:ecs:us-west-2:123456789:service/mycluster/myservice"), + }, + }, + }, + }, + }, + wantError: `get secrets: get secret "arn:aws:ecs:us-west-2:123456789:service/mycluster/myservice": invalid ARN; not a SSM or Secrets Manager ARN`, + }, + "error if secret redefines a var": { + taskDef: &awsecs.TaskDefinition{ + ContainerDefinitions: []*sdkecs.ContainerDefinition{ + { + Name: aws.String("foo"), + Environment: []*sdkecs.KeyValuePair{ + { + Name: aws.String("SHOULD_BE_A_VAR"), + Value: aws.String("foo"), + }, + }, + Secrets: []*sdkecs.Secret{ + { + Name: aws.String("SHOULD_BE_A_VAR"), + ValueFrom: aws.String("bad"), + }, + }, + }, + }, + }, + wantError: `get secrets: secret names must be unique, but an environment variable "SHOULD_BE_A_VAR" already exists`, + }, + "correct service used based on arn": { + taskDef: &awsecs.TaskDefinition{ + ContainerDefinitions: []*sdkecs.ContainerDefinition{ + { + Name: aws.String("foo"), + Secrets: []*sdkecs.Secret{ + { + Name: aws.String("SSM"), + ValueFrom: aws.String("arn:aws:ssm:us-east-2:123456789:parameter/myparam"), + }, + { + Name: aws.String("SECRETS_MANAGER"), + ValueFrom: aws.String("arn:aws:secretsmanager:us-west-2:123456789:secret:mysecret"), + }, + { + Name: aws.String("DEFAULT"), + ValueFrom: aws.String("myparam"), + }, + }, + }, + }, + }, + setupMocks: func(m *localRunExecuteMocks) { + m.ssm.EXPECT().GetSecretValue(gomock.Any(), "arn:aws:ssm:us-east-2:123456789:parameter/myparam").Return("ssm", nil) + m.secretsManager.EXPECT().GetSecretValue(gomock.Any(), "arn:aws:secretsmanager:us-west-2:123456789:secret:mysecret").Return("secretsmanager", nil) + m.ssm.EXPECT().GetSecretValue(gomock.Any(), "myparam").Return("default", nil) + }, + want: map[string]containerEnv{ + "foo": { + "SSM": newVar("ssm", false, true), + "SECRETS_MANAGER": newVar("secretsmanager", false, true), + "DEFAULT": newVar("default", false, true), + }, + }, + }, + "only unique secrets pulled": { + taskDef: &awsecs.TaskDefinition{ + ContainerDefinitions: []*sdkecs.ContainerDefinition{ + { + Name: aws.String("foo"), + Secrets: []*sdkecs.Secret{ + { + Name: aws.String("ONE"), + ValueFrom: aws.String("shared"), + }, + { + Name: aws.String("TWO"), + ValueFrom: aws.String("foo"), + }, + }, + }, + { + Name: aws.String("bar"), + Secrets: []*sdkecs.Secret{ + { + Name: aws.String("THREE"), + ValueFrom: aws.String("shared"), + }, + { + Name: aws.String("FOUR"), + ValueFrom: aws.String("bar"), + }, + }, + }, + }, + }, + setupMocks: func(m *localRunExecuteMocks) { + m.ssm.EXPECT().GetSecretValue(gomock.Any(), "shared").Return("shared-value", nil) + m.ssm.EXPECT().GetSecretValue(gomock.Any(), "foo").Return("foo-value", nil) + m.ssm.EXPECT().GetSecretValue(gomock.Any(), "bar").Return("bar-value", nil) + }, + want: map[string]containerEnv{ + "foo": { + "ONE": newVar("shared-value", false, true), + "TWO": newVar("foo-value", false, true), + }, + "bar": { + "THREE": newVar("shared-value", false, true), + "FOUR": newVar("bar-value", false, true), + }, + }, + }, + "secrets set via overrides not pulled": { + taskDef: &awsecs.TaskDefinition{ + ContainerDefinitions: []*sdkecs.ContainerDefinition{ + { + Name: aws.String("foo"), + Secrets: []*sdkecs.Secret{ + { + Name: aws.String("ONE"), + ValueFrom: aws.String("shared"), + }, + { + Name: aws.String("TWO"), + ValueFrom: aws.String("foo"), + }, + }, + }, + { + Name: aws.String("bar"), + Secrets: []*sdkecs.Secret{ + { + Name: aws.String("THREE"), + ValueFrom: aws.String("shared"), + }, + { + Name: aws.String("FOUR"), + ValueFrom: aws.String("bar"), + }, + }, + }, + }, + }, + envOverrides: map[string]string{ + "ONE": "one-overridden", + "bar:FOUR": "four-overridden", + }, + setupMocks: func(m *localRunExecuteMocks) { + m.ssm.EXPECT().GetSecretValue(gomock.Any(), "shared").Return("shared-value", nil) + m.ssm.EXPECT().GetSecretValue(gomock.Any(), "foo").Return("foo-value", nil) + }, + want: map[string]containerEnv{ + "foo": { + "ONE": newVar("one-overridden", true, false), + "TWO": newVar("foo-value", false, true), + }, + "bar": { + "ONE": newVar("one-overridden", true, false), + "THREE": newVar("shared-value", false, true), + "FOUR": newVar("four-overridden", true, false), + }, + }, + }, + } + for name, tc := range tests { + t.Run(name, func(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + m := &localRunExecuteMocks{ + ssm: mocks.NewMocksecretGetter(ctrl), + secretsManager: mocks.NewMocksecretGetter(ctrl), + } + if tc.setupMocks != nil { + tc.setupMocks(m) + } + + o := &localRunOpts{ + localRunVars: localRunVars{ + envOverrides: tc.envOverrides, + }, + ssm: m.ssm, + secretsManager: m.secretsManager, + } + + got, err := o.getEnvVars(context.Background(), tc.taskDef) + if tc.wantError != "" { + require.EqualError(t, err, tc.wantError) + return + } + require.NoError(t, err) + require.Equal(t, tc.want, got) + }) + } +} diff --git a/internal/pkg/cli/mocks/mock_interfaces.go b/internal/pkg/cli/mocks/mock_interfaces.go index 738694eaed9..fe836f0cb73 100644 --- a/internal/pkg/cli/mocks/mock_interfaces.go +++ b/internal/pkg/cli/mocks/mock_interfaces.go @@ -1738,21 +1738,6 @@ func (m *MockecsLocalClient) EXPECT() *MockecsLocalClientMockRecorder { return m.recorder } -// DecryptedSecrets mocks base method. -func (m *MockecsLocalClient) DecryptedSecrets(secrets []*ecs.ContainerSecret) ([]ecs0.EnvVar, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "DecryptedSecrets", secrets) - ret0, _ := ret[0].([]ecs0.EnvVar) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// DecryptedSecrets indicates an expected call of DecryptedSecrets. -func (mr *MockecsLocalClientMockRecorder) DecryptedSecrets(secrets interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DecryptedSecrets", reflect.TypeOf((*MockecsLocalClient)(nil).DecryptedSecrets), secrets) -} - // TaskDefinition mocks base method. func (m *MockecsLocalClient) TaskDefinition(app, env, svc string) (*ecs.TaskDefinition, error) { m.ctrl.T.Helper() @@ -8064,3 +8049,41 @@ func (mr *MockstackConfigurationMockRecorder) Template() *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Template", reflect.TypeOf((*MockstackConfiguration)(nil).Template)) } + +// MocksecretGetter is a mock of secretGetter interface. +type MocksecretGetter struct { + ctrl *gomock.Controller + recorder *MocksecretGetterMockRecorder +} + +// MocksecretGetterMockRecorder is the mock recorder for MocksecretGetter. +type MocksecretGetterMockRecorder struct { + mock *MocksecretGetter +} + +// NewMocksecretGetter creates a new mock instance. +func NewMocksecretGetter(ctrl *gomock.Controller) *MocksecretGetter { + mock := &MocksecretGetter{ctrl: ctrl} + mock.recorder = &MocksecretGetterMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MocksecretGetter) EXPECT() *MocksecretGetterMockRecorder { + return m.recorder +} + +// GetSecretValue mocks base method. +func (m *MocksecretGetter) GetSecretValue(arg0 context.Context, arg1 string) (string, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetSecretValue", arg0, arg1) + ret0, _ := ret[0].(string) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetSecretValue indicates an expected call of GetSecretValue. +func (mr *MocksecretGetterMockRecorder) GetSecretValue(arg0, arg1 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetSecretValue", reflect.TypeOf((*MocksecretGetter)(nil).GetSecretValue), arg0, arg1) +} diff --git a/internal/pkg/ecs/ecs.go b/internal/pkg/ecs/ecs.go index f86fb7cae13..e5ed82c01ab 100644 --- a/internal/pkg/ecs/ecs.go +++ b/internal/pkg/ecs/ecs.go @@ -16,8 +16,6 @@ import ( "github.com/aws/aws-sdk-go/aws/session" "github.com/aws/copilot-cli/internal/pkg/aws/ecs" "github.com/aws/copilot-cli/internal/pkg/aws/resourcegroups" - "github.com/aws/copilot-cli/internal/pkg/aws/secretsmanager" - "github.com/aws/copilot-cli/internal/pkg/aws/ssm" "github.com/aws/copilot-cli/internal/pkg/aws/stepfunctions" "github.com/aws/copilot-cli/internal/pkg/deploy" ) @@ -55,10 +53,6 @@ type stepFunctionsClient interface { StateMachineDefinition(stateMachineARN string) (string, error) } -type secretGetter interface { - GetSecretValue(secretName string) (string, error) -} - // EnvVar contains the value of an environment variable type EnvVar struct { Name string @@ -76,40 +70,17 @@ type ServiceDesc struct { // Client retrieves Copilot information from ECS endpoint. type Client struct { rgGetter resourceGetter - ssm secretGetter - secretManager secretGetter ecsClient ecsClient StepFuncClient stepFunctionsClient } // New creates a new Client. func New(sess *session.Session) *Client { - return NewWithOptions(sess) -} - -// NewWithOptions creates a new Client with opts. -func NewWithOptions(sess *session.Session, opts ...Option) *Client { - c := &Client{ + return &Client{ rgGetter: resourcegroups.New(sess), ecsClient: ecs.New(sess), - ssm: ssm.New(sess), - secretManager: secretsmanager.New(sess), StepFuncClient: stepfunctions.New(sess), } - for _, opt := range opts { - opt(c) - } - return c -} - -// Option is for functional options. -type Option func(*Client) - -// WithSecretGetter returns an option to set a custom secret getter on Client. -func WithSecretGetter(s secretGetter) Option { - return func(c *Client) { - c.secretManager = s - } } // ClusterARN returns the ARN of the cluster in an environment. @@ -267,48 +238,6 @@ func (c Client) StopDefaultClusterTasks(familyName string) error { return c.ecsClient.StopTasks(taskIDs, ecs.WithStopTaskReason(taskStopReason)) } -func secretNameSpace(secret string) (string, error) { - // A secret value can be a SSM Parameter Name/ SSM Parameter ARN/ Secrets Manager ARN - // Note: If there is an error while parsing the secret value, this functions assumes it to be SSM Parameter. - // Refer to https://docs.aws.amazon.com/AmazonECS/latest/APIReference/API_Secret.html - parsed, err := arn.Parse(secret) - if err != nil { - return ssm.Namespace, nil - } - switch parsed.Service { - case ssm.Namespace, secretsmanager.Namespace: - return parsed.Service, nil - default: - return "", fmt.Errorf("invalid ARN: not an SSM or Secrets Manager ARN") - } -} - -// DecryptedSecrets returns the decrypted parameters from either SSM parameter store or Secrets Manager. -func (c Client) DecryptedSecrets(secrets []*ecs.ContainerSecret) ([]EnvVar, error) { - var vars []EnvVar - for _, secret := range secrets { - namespace, err := secretNameSpace(secret.ValueFrom) - if err != nil { - return nil, err - } - var secretValue string - switch namespace { - case ssm.Namespace: - secretValue, err = c.ssm.GetSecretValue(secret.ValueFrom) - case secretsmanager.Namespace: - secretValue, err = c.secretManager.GetSecretValue(secret.ValueFrom) - } - if err != nil { - return nil, err - } - vars = append(vars, EnvVar{ - Name: secret.Name, - Value: secretValue, - }) - } - return vars, nil -} - // TaskDefinition returns the task definition of the service. func (c Client) TaskDefinition(app, env, svc string) (*ecs.TaskDefinition, error) { taskDefName := fmt.Sprintf("%s-%s-%s", app, env, svc) diff --git a/internal/pkg/ecs/mocks/mock_ecs.go b/internal/pkg/ecs/mocks/mock_ecs.go index 8616f4208b7..698d6d45ed2 100644 --- a/internal/pkg/ecs/mocks/mock_ecs.go +++ b/internal/pkg/ecs/mocks/mock_ecs.go @@ -322,41 +322,3 @@ func (mr *MockstepFunctionsClientMockRecorder) StateMachineDefinition(stateMachi mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "StateMachineDefinition", reflect.TypeOf((*MockstepFunctionsClient)(nil).StateMachineDefinition), stateMachineARN) } - -// MocksecretGetter is a mock of secretGetter interface. -type MocksecretGetter struct { - ctrl *gomock.Controller - recorder *MocksecretGetterMockRecorder -} - -// MocksecretGetterMockRecorder is the mock recorder for MocksecretGetter. -type MocksecretGetterMockRecorder struct { - mock *MocksecretGetter -} - -// NewMocksecretGetter creates a new mock instance. -func NewMocksecretGetter(ctrl *gomock.Controller) *MocksecretGetter { - mock := &MocksecretGetter{ctrl: ctrl} - mock.recorder = &MocksecretGetterMockRecorder{mock} - return mock -} - -// EXPECT returns an object that allows the caller to indicate expected use. -func (m *MocksecretGetter) EXPECT() *MocksecretGetterMockRecorder { - return m.recorder -} - -// GetSecretValue mocks base method. -func (m *MocksecretGetter) GetSecretValue(secretName string) (string, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetSecretValue", secretName) - ret0, _ := ret[0].(string) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// GetSecretValue indicates an expected call of GetSecretValue. -func (mr *MocksecretGetterMockRecorder) GetSecretValue(secretName interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetSecretValue", reflect.TypeOf((*MocksecretGetter)(nil).GetSecretValue), secretName) -}