diff --git a/ecs/cmd/commands/secret.go b/ecs/cmd/commands/secret.go index 8488e6cb1..f964f1eae 100644 --- a/ecs/cmd/commands/secret.go +++ b/ecs/cmd/commands/secret.go @@ -16,7 +16,10 @@ import ( ) type createSecretOptions struct { - Label string + Label string + Username string + Password string + Description string } type deleteSecretOptions struct { @@ -39,9 +42,9 @@ func SecretCommand(dockerCli command.Cli) *cobra.Command { } func CreateSecret(dockerCli command.Cli) *cobra.Command { - //opts := createSecretOptions{} + opts := createSecretOptions{} cmd := &cobra.Command{ - Use: "create NAME SECRET", + Use: "create NAME", Short: "Creates a secret.", RunE: docker.WithAwsContext(dockerCli, func(clusteropts docker.AwsContext, args []string) error { client, err := amazon.NewClient(clusteropts.Profile, clusteropts.Cluster, clusteropts.Region) @@ -52,12 +55,16 @@ func CreateSecret(dockerCli command.Cli) *cobra.Command { return errors.New("Missing mandatory parameter: NAME") } name := args[0] - secret := args[1] - id, err := client.CreateSecret(context.Background(), name, secret) + + secret := docker.NewSecret(name, opts.Username, opts.Password, opts.Description) + id, err := client.CreateSecret(context.Background(), secret) fmt.Println(id) return err }), } + cmd.Flags().StringVarP(&opts.Username, "username", "u", "", "username") + cmd.Flags().StringVarP(&opts.Password, "password", "p", "", "password") + cmd.Flags().StringVarP(&opts.Description, "description", "d", "", "Secret description") return cmd } diff --git a/ecs/pkg/amazon/cloudformation.go b/ecs/pkg/amazon/cloudformation.go index 45740b48b..fa7f2f8ac 100644 --- a/ecs/pkg/amazon/cloudformation.go +++ b/ecs/pkg/amazon/cloudformation.go @@ -55,17 +55,28 @@ func (c client) Convert(ctx context.Context, project *compose.Project) (*cloudfo } taskExecutionRole := fmt.Sprintf("%sTaskExecutionRole", service.Name) + policy, err := c.getPolicy(ctx, definition) + if err != nil { + return nil, err + } + rolePolicies := []iam.Role_Policy{} + if policy != nil { + rolePolicies = append(rolePolicies, iam.Role_Policy{ + PolicyDocument: policy, + PolicyName: taskExecutionRole, + }) + + } + definition.ExecutionRoleArn = cloudformation.Ref(taskExecutionRole) + + taskDefinition := fmt.Sprintf("%sTaskDefinition", service.Name) template.Resources[taskExecutionRole] = &iam.Role{ AssumeRolePolicyDocument: assumeRolePolicyDocument, - // Here we can grant access to secrets/configs using a Policy { Allow,ssm:GetParameters,secret|config ARN} + Policies: rolePolicies, ManagedPolicyArns: []string{ ECSTaskExecutionPolicy, }, } - definition.ExecutionRoleArn = cloudformation.Ref(taskExecutionRole) - // FIXME definition.TaskRoleArn = ? - - taskDefinition := fmt.Sprintf("%sTaskDefinition", service.Name) template.Resources[taskDefinition] = definition var healthCheck *cloudmap.Service_HealthCheckConfig @@ -182,6 +193,33 @@ func (c client) GetVPC(ctx context.Context, project *compose.Project) (string, e return defaultVPC, nil } +func (c client) getPolicy(ctx context.Context, taskDef *ecs.TaskDefinition) (*PolicyDocument, error) { + + arns := []string{} + for _, container := range taskDef.ContainerDefinitions { + if container.RepositoryCredentials != nil { + arns = append(arns, container.RepositoryCredentials.CredentialsParameter) + } + if len(container.Secrets) > 0 { + for _, s := range container.Secrets { + arns = append(arns, s.ValueFrom) + } + } + + } + if len(arns) > 0 { + return &PolicyDocument{ + Statement: []PolicyStatement{ + { + Effect: "Allow", + Action: []string{"secretsmanager:GetSecretValue", "ssm:GetParameters", "kms:Decrypt"}, + Resource: arns, + }}, + }, nil + } + return nil, nil +} + type convertAPI interface { GetDefaultVPC(ctx context.Context) (string, error) VpcExists(ctx context.Context, vpcID string) (bool, error) diff --git a/ecs/pkg/amazon/convert.go b/ecs/pkg/amazon/convert.go index 4d75072c1..50c722ffd 100644 --- a/ecs/pkg/amazon/convert.go +++ b/ecs/pkg/amazon/convert.go @@ -44,7 +44,7 @@ func Convert(project *compose.Project, service types.ServiceConfig) (*ecs.TaskDe FirelensConfiguration: nil, HealthCheck: toHealthCheck(service.HealthCheck), Hostname: service.Hostname, - Image: service.Image, + Image: getImage(service.Image), Interactive: false, Links: nil, LinuxParameters: toLinuxParameters(service), @@ -282,22 +282,27 @@ func toKeyValuePair(environment types.MappingWithEquals) []ecs.TaskDefinition_Ke return pairs } +func getImage(image string) string { + switch f := strings.Split(image, "/"); len(f) { + case 1: + return "docker.io/library/" + image + case 2: + return "docker.io/" + image + default: + return image + } +} + func getRepoCredentials(service types.ServiceConfig) (*ecs.TaskDefinition_RepositoryCredentials, error) { // extract registry and namespace string from image name - fields := strings.Split(service.Image, "/") - regPath := "" - for i, field := range fields { - if i < len(fields)-1 { - regPath = regPath + field + credential := "" + for key, value := range service.Extras { + if strings.HasPrefix(key, "x-aws-pull_credentials") { + credential = value.(string) } } - if regPath == "" || len(service.Secrets) == 0 { - return nil, nil - } - for _, secret := range service.Secrets { - if secret.Source == regPath { - return &ecs.TaskDefinition_RepositoryCredentials{CredentialsParameter: secret.Target}, nil - } + if credential != "" { + return &ecs.TaskDefinition_RepositoryCredentials{CredentialsParameter: credential}, nil } return nil, nil } @@ -306,7 +311,7 @@ func getSecrets(service types.ServiceConfig) ([]ecs.TaskDefinition_Secret, error secrets := []ecs.TaskDefinition_Secret{} for _, secret := range service.Secrets { - secrets = append(secrets, ecs.TaskDefinition_Secret{Name: secret.Target}) + secrets = append(secrets, ecs.TaskDefinition_Secret{Name: secret.Target, ValueFrom: secret.Source}) } return secrets, nil } diff --git a/ecs/pkg/amazon/mock/api.go b/ecs/pkg/amazon/mock/api.go index 4a1163315..7eba94054 100644 --- a/ecs/pkg/amazon/mock/api.go +++ b/ecs/pkg/amazon/mock/api.go @@ -6,11 +6,12 @@ package mock import ( context "context" + reflect "reflect" + cloudformation "github.com/aws/aws-sdk-go/service/cloudformation" cloudformation0 "github.com/awslabs/goformation/v4/cloudformation" docker "github.com/docker/ecs-plugin/pkg/docker" gomock "github.com/golang/mock/gomock" - reflect "reflect" ) // MockAPI is a mock of API interface @@ -67,18 +68,18 @@ func (mr *MockAPIMockRecorder) CreateCluster(arg0, arg1 interface{}) *gomock.Cal } // CreateSecret mocks base method -func (m *MockAPI) CreateSecret(arg0 context.Context, arg1, arg2 string) (string, error) { +func (m *MockAPI) CreateSecret(arg0 context.Context, arg1 docker.Secret) (string, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "CreateSecret", arg0, arg1, arg2) + ret := m.ctrl.Call(m, "CreateSecret", arg0, arg1) ret0, _ := ret[0].(string) ret1, _ := ret[1].(error) return ret0, ret1 } // CreateSecret indicates an expected call of CreateSecret -func (mr *MockAPIMockRecorder) CreateSecret(arg0, arg1, arg2 interface{}) *gomock.Call { +func (mr *MockAPIMockRecorder) CreateSecret(arg0, arg1 docker.Secret) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateSecret", reflect.TypeOf((*MockAPI)(nil).CreateSecret), arg0, arg1, arg2) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateSecret", reflect.TypeOf((*MockAPI)(nil).CreateSecret), arg0, arg1) } // CreateStack mocks base method diff --git a/ecs/pkg/amazon/sdk.go b/ecs/pkg/amazon/sdk.go index 74cb04484..4bd9eea85 100644 --- a/ecs/pkg/amazon/sdk.go +++ b/ecs/pkg/amazon/sdk.go @@ -223,9 +223,18 @@ func (s sdk) DeleteStack(ctx context.Context, name string) error { return err } -func (s sdk) CreateSecret(ctx context.Context, name string, secret string) (string, error) { - logrus.Debug("Create secret " + name) - response, err := s.SM.CreateSecret(&secretsmanager.CreateSecretInput{Name: &name, SecretString: &secret}) +func (s sdk) CreateSecret(ctx context.Context, secret docker.Secret) (string, error) { + logrus.Debug("Create secret " + secret.Name) + secretStr, err := secret.GetCredString() + if err != nil { + return "", err + } + + response, err := s.SM.CreateSecret(&secretsmanager.CreateSecretInput{ + Name: &secret.Name, + SecretString: &secretStr, + Description: &secret.Description, + }) if err != nil { return "", err } diff --git a/ecs/pkg/amazon/secrets.go b/ecs/pkg/amazon/secrets.go index 649705f02..96a2a476d 100644 --- a/ecs/pkg/amazon/secrets.go +++ b/ecs/pkg/amazon/secrets.go @@ -7,14 +7,14 @@ import ( ) type secretsAPI interface { - CreateSecret(ctx context.Context, name string, content string) (string, error) + CreateSecret(ctx context.Context, secret docker.Secret) (string, error) InspectSecret(ctx context.Context, id string) (docker.Secret, error) ListSecrets(ctx context.Context) ([]docker.Secret, error) DeleteSecret(ctx context.Context, id string, recover bool) error } -func (c client) CreateSecret(ctx context.Context, name string, content string) (string, error) { - return c.api.CreateSecret(ctx, name, content) +func (c client) CreateSecret(ctx context.Context, secret docker.Secret) (string, error) { + return c.api.CreateSecret(ctx, secret) } func (c client) InspectSecret(ctx context.Context, id string) (docker.Secret, error) { diff --git a/ecs/pkg/compose/api.go b/ecs/pkg/compose/api.go index e23651b63..6fd8409a5 100644 --- a/ecs/pkg/compose/api.go +++ b/ecs/pkg/compose/api.go @@ -12,7 +12,7 @@ type API interface { ComposeUp(ctx context.Context, project *Project) error ComposeDown(ctx context.Context, projectName string, deleteCluster bool) error - CreateSecret(ctx context.Context, name string, secret string) (string, error) + CreateSecret(ctx context.Context, secret docker.Secret) (string, error) InspectSecret(ctx context.Context, id string) (docker.Secret, error) ListSecrets(ctx context.Context) ([]docker.Secret, error) DeleteSecret(ctx context.Context, id string, recover bool) error diff --git a/ecs/pkg/docker/secret.go b/ecs/pkg/docker/secret.go index 0efae5d67..613c62638 100644 --- a/ecs/pkg/docker/secret.go +++ b/ecs/pkg/docker/secret.go @@ -9,6 +9,17 @@ type Secret struct { Name string `json:"Name"` Labels map[string]string `json:"Labels"` Description string `json:"Description"` + username string + password string +} + +func NewSecret(name, username, password, description string) Secret { + return Secret{ + Name: name, + username: username, + password: password, + Description: description, + } } func (s Secret) ToJSON() (string, error) { @@ -18,3 +29,15 @@ func (s Secret) ToJSON() (string, error) { } return string(b), nil } + +func (s Secret) GetCredString() (string, error) { + creds := map[string]string{ + "username": s.username, + "password": s.password, + } + b, err := json.Marshal(&creds) + if err != nil { + return "", err + } + return string(b), nil +}