Skip to content

Commit

Permalink
chore: decrypt secrets from SSM and secrets manager (#5113)
Browse files Browse the repository at this point in the history
This is only for ECS services


By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the Apache 2.0 License.
  • Loading branch information
Varun359 authored Jul 27, 2023
1 parent ac4ded0 commit 0a886d6
Show file tree
Hide file tree
Showing 10 changed files with 350 additions and 9 deletions.
15 changes: 15 additions & 0 deletions internal/pkg/aws/secretsmanager/mocks/mock_secretsmanager.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

16 changes: 16 additions & 0 deletions internal/pkg/aws/secretsmanager/secretsmanager.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,14 @@ 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)
}

// SecretsManager wraps the AWS SecretManager client.
Expand Down Expand Up @@ -114,6 +118,18 @@ func (s *SecretsManager) DescribeSecret(secretName string) (*DescribeSecretOutpu
}, nil
}

// 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{
SecretId: aws.String(name),
})
if err != nil {
return "", fmt.Errorf("get secret %q from secrets manager: %w", name, err)
}
return aws.StringValue(resp.SecretString), nil
}

// ErrSecretAlreadyExists occurs if a secret with the same name already exists.
type ErrSecretAlreadyExists struct {
secretName string
Expand Down
15 changes: 15 additions & 0 deletions internal/pkg/aws/ssm/mocks/mock_ssm.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

17 changes: 17 additions & 0 deletions internal/pkg/aws/ssm/ssm.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,13 @@ import (
"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)
}

// SSM wraps an AWS SSM client.
Expand Down Expand Up @@ -61,6 +65,19 @@ func (s *SSM) PutSecret(in PutSecretInput) (*PutSecretOutput, error) {
return nil, err
}

// 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{
Name: aws.String(name),
WithDecryption: aws.Bool(true),
})
if err != nil {
return "", fmt.Errorf("get parameter %q from SSM: %w", name, err)
}
return aws.StringValue(resp.Parameter.Value), nil
}

func (s *SSM) createSecret(in PutSecretInput) (*PutSecretOutput, error) {
// Create a secret while adding the tags in a single call instead of separate calls to `PutParameter` and
// `AddTagsToResource` so that there won't be a case where the parameter is created while the tags are not added.
Expand Down
5 changes: 5 additions & 0 deletions internal/pkg/cli/interfaces.go
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,11 @@ type repositoryService interface {
imageBuilderPusher
}

type ecsLocalClient interface {
TaskDefinition(app, env, svc string) (*awsecs.TaskDefinition, error)
DecryptedSecrets(secrets []*awsecs.ContainerSecret) ([]ecs.EnvVar, error)
}

type logEventsWriter interface {
WriteLogEvents(opts logging.WriteLogEventsOpts) error
}
Expand Down
27 changes: 21 additions & 6 deletions internal/pkg/cli/local_run.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,13 @@ import (
"fmt"

"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/session"
"github.com/aws/aws-sdk-go/service/ssm"
"github.com/aws/copilot-cli/internal/pkg/aws/identity"
"github.com/aws/copilot-cli/internal/pkg/aws/sessions"
"github.com/aws/copilot-cli/internal/pkg/config"
"github.com/aws/copilot-cli/internal/pkg/deploy"
"github.com/aws/copilot-cli/internal/pkg/ecs"
"github.com/aws/copilot-cli/internal/pkg/term/prompt"
"github.com/aws/copilot-cli/internal/pkg/term/selector"
"github.com/spf13/cobra"
Expand All @@ -29,8 +31,10 @@ type localRunVars struct {
type localRunOpts struct {
localRunVars

sel deploySelector
store store
sel deploySelector
ecsLocalClient ecsLocalClient
sess *session.Session
store store
}

func newLocalRunOpts(vars localRunVars) (*localRunOpts, error) {
Expand All @@ -46,9 +50,11 @@ func newLocalRunOpts(vars localRunVars) (*localRunOpts, error) {
return nil, err
}
opts := &localRunOpts{
localRunVars: vars,
sel: selector.NewDeploySelect(prompt.New(), store, deployStore),
store: store,
localRunVars: vars,
sel: selector.NewDeploySelect(prompt.New(), store, deployStore),
store: store,
ecsLocalClient: ecs.New(defaultSess),
sess: defaultSess,
}
return opts, nil
}
Expand Down Expand Up @@ -94,7 +100,16 @@ func (o *localRunOpts) validateAndAskWkldEnvName() error {

// Execute builds and runs the workload images locally.
func (o *localRunOpts) Execute() error {
//TODO(varun359): Get build information from the manifest and task definition for workloads
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()
_, err = o.ecsLocalClient.DecryptedSecrets(secrets)
if err != nil {
return fmt.Errorf("get secret values: %w", err)
}

return nil
}
Expand Down
110 changes: 110 additions & 0 deletions internal/pkg/cli/local_run_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,12 @@ import (
"fmt"
"testing"

"github.com/aws/aws-sdk-go/aws"
ecsapi "github.com/aws/aws-sdk-go/service/ecs"
awsecs "github.com/aws/copilot-cli/internal/pkg/aws/ecs"
"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/term/selector"
"github.com/golang/mock/gomock"
"github.com/stretchr/testify/require"
Expand Down Expand Up @@ -187,3 +191,109 @@ func TestLocalRunOpts_Ask(t *testing.T) {
})
}
}

type localRunExecuteMocks struct {
ecsLocalClient *mocks.MockecsLocalClient
}

func TestLocalRunOpts_Execute(t *testing.T) {
const (
testAppName = "testApp"
testEnvName = "testEnv"
testWkldName = "testWkld"
testWkldType = "testWkldType"
)
var taskDefinition = &awsecs.TaskDefinition{
ContainerDefinitions: []*ecsapi.ContainerDefinition{
{
Name: aws.String("container"),
Environment: []*ecsapi.KeyValuePair{
{
Name: aws.String("COPILOT_SERVICE_NAME"),
Value: aws.String("testWkld"),
},
{
Name: aws.String("COPILOT_ENVIRONMENT_NAME"),
Value: aws.String("testEnv"),
},
},
},
},
}
testCases := map[string]struct {
inputAppName string
inputEnvName string
inputWkldName string

setupMocks func(m *localRunExecuteMocks)
wantedWkldName string
wantedEnvName string
wantedWkldType string
wantedError error
}{
"error getting the task Definition": {
inputAppName: testAppName,
inputWkldName: testWkldName,
inputEnvName: testEnvName,
setupMocks: func(m *localRunExecuteMocks) {
m.ecsLocalClient.EXPECT().TaskDefinition(testAppName, testEnvName, testWkldName).Return(nil, testError)
},
wantedError: fmt.Errorf("get task definition: %w", testError),
},
"error decryting secrets from task definition": {
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(nil, testError)
},
wantedError: fmt.Errorf("get secret values: %w", testError),
},
"success decrypting secrets from task definition": {
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([]ecs.EnvVar{{
Name: "my-secret",
Value: "Password123",
}, {
Name: "secret2",
Value: "admin123",
},
}, nil)
},
},
}
for name, tc := range testCases {
t.Run(name, func(t *testing.T) {
// GIVEN
ctrl := gomock.NewController(t)
defer ctrl.Finish()
m := &localRunExecuteMocks{
ecsLocalClient: mocks.NewMockecsLocalClient(ctrl),
}
tc.setupMocks(m)
opts := localRunOpts{
localRunVars: localRunVars{
appName: tc.inputAppName,
wkldName: tc.inputWkldName,
envName: tc.inputEnvName,
},
ecsLocalClient: m.ecsLocalClient,
}

// WHEN
err := opts.Execute()

// THEN
if tc.wantedError == nil {
require.NoError(t, err)
} else {
require.EqualError(t, err, tc.wantedError.Error())
}
})
}
}
53 changes: 53 additions & 0 deletions internal/pkg/cli/mocks/mock_interfaces.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading

0 comments on commit 0a886d6

Please sign in to comment.