Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

chore: decrypt secrets from SSM and secrets manager #5113

Merged
merged 23 commits into from
Jul 27, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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)
Varun359 marked this conversation as resolved.
Show resolved Hide resolved
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
Loading