diff --git a/ecs-cli/modules/cli/regcreds/create_task_execution_role.go b/ecs-cli/modules/cli/regcreds/create_task_execution_role.go index 56f3b90d7..d704752c8 100644 --- a/ecs-cli/modules/cli/regcreds/create_task_execution_role.go +++ b/ecs-cli/modules/cli/regcreds/create_task_execution_role.go @@ -34,6 +34,7 @@ type executionRoleParams struct { CredEntries map[string]regcredio.CredsOutputEntry RoleName string Region string + Tags map[string]*string } // returns the time of IAM policy creation so that other resources (i.e., output file) can be dated to match @@ -41,7 +42,7 @@ func createTaskExecutionRole(params executionRoleParams, iamClient iamClient.Cli log.Infof("Creating resources for task execution role %s...", params.RoleName) // create role - roleName, err := createOrFindRole(params.RoleName, iamClient) + roleName, err := createOrFindRole(params.RoleName, iamClient, convertToIAMTags(params.Tags)) if err != nil { return nil, err } @@ -88,8 +89,8 @@ func createRegistryCredentialsPolicy(roleName, policyDoc string, createTime time return policyResult.Policy, nil } -func createOrFindRole(roleName string, client iamClient.Client) (string, error) { - roleResult, err := client.CreateOrFindRole(roleName, roleDescriptionString, assumeRolePolicyDocString) +func createOrFindRole(roleName string, client iamClient.Client, tags []*iam.Tag) (string, error) { + roleResult, err := client.CreateOrFindRole(roleName, roleDescriptionString, assumeRolePolicyDocString, tags) if err != nil { return "", err } @@ -119,3 +120,15 @@ func attachRolePolicies(secretPolicyARN, roleName, region string, client iamClie return nil } + +func convertToIAMTags(tags map[string]*string) []*iam.Tag { + var iamTags []*iam.Tag + for key, value := range tags { + iamTags = append(iamTags, &iam.Tag{ + Key: aws.String(key), + Value: value, + }) + } + + return iamTags +} diff --git a/ecs-cli/modules/cli/regcreds/create_task_execution_role_test.go b/ecs-cli/modules/cli/regcreds/create_task_execution_role_test.go index 3ab688c46..175819dc6 100644 --- a/ecs-cli/modules/cli/regcreds/create_task_execution_role_test.go +++ b/ecs-cli/modules/cli/regcreds/create_task_execution_role_test.go @@ -41,7 +41,7 @@ func TestCreateTaskExecutionRole(t *testing.T) { mocks := setupTestController(t) gomock.InOrder( - mocks.MockIAM.EXPECT().CreateOrFindRole(testRoleName, roleDescriptionString, assumeRolePolicyDocString).Return(*testRoleArn, nil), + mocks.MockIAM.EXPECT().CreateOrFindRole(testRoleName, roleDescriptionString, assumeRolePolicyDocString, nil).Return(*testRoleArn, nil), mocks.MockIAM.EXPECT().CreateRole(gomock.Any()).Return(&iam.CreateRoleOutput{Role: &iam.Role{Arn: testRoleArn}}, nil), ) gomock.InOrder( @@ -76,7 +76,7 @@ func TestCreateTaskExecutionRole_NoKMSKey(t *testing.T) { mocks := setupTestController(t) gomock.InOrder( - mocks.MockIAM.EXPECT().CreateOrFindRole(testRoleName, roleDescriptionString, assumeRolePolicyDocString).Return(*testRoleArn, nil), + mocks.MockIAM.EXPECT().CreateOrFindRole(testRoleName, roleDescriptionString, assumeRolePolicyDocString, nil).Return(*testRoleArn, nil), mocks.MockIAM.EXPECT().CreateRole(gomock.Any()).Return(&iam.CreateRoleOutput{Role: &iam.Role{Arn: testRoleArn}}, nil), ) gomock.InOrder( @@ -110,7 +110,7 @@ func TestCreateTaskExecutionRole_RoleExists(t *testing.T) { mocks := setupTestController(t) gomock.InOrder( // CreateOrFindRole should return nil if given role already exists - mocks.MockIAM.EXPECT().CreateOrFindRole(testRoleName, roleDescriptionString, assumeRolePolicyDocString).Return("", nil), + mocks.MockIAM.EXPECT().CreateOrFindRole(testRoleName, roleDescriptionString, assumeRolePolicyDocString, nil).Return("", nil), mocks.MockIAM.EXPECT().CreateRole(gomock.Any()).Return(nil, roleExistsError), ) gomock.InOrder( @@ -140,7 +140,7 @@ func TestCreateTaskExecutionRole_ErrorOnCreateRoleFails(t *testing.T) { mocks := setupTestController(t) gomock.InOrder( - mocks.MockIAM.EXPECT().CreateOrFindRole(testRoleName, roleDescriptionString, assumeRolePolicyDocString).Return("", errors.New("something went wrong")), + mocks.MockIAM.EXPECT().CreateOrFindRole(testRoleName, roleDescriptionString, assumeRolePolicyDocString, nil).Return("", errors.New("something went wrong")), mocks.MockIAM.EXPECT().CreateRole(gomock.Any()).Return(nil, errors.New("something went wrong")), ) @@ -166,7 +166,7 @@ func TestCreateTaskExecutionRole_ErrorOnCreatePolicyFails(t *testing.T) { mocks := setupTestController(t) gomock.InOrder( - mocks.MockIAM.EXPECT().CreateOrFindRole(testRoleName, roleDescriptionString, assumeRolePolicyDocString).Return(*testRoleArn, nil), + mocks.MockIAM.EXPECT().CreateOrFindRole(testRoleName, roleDescriptionString, assumeRolePolicyDocString, nil).Return(*testRoleArn, nil), mocks.MockIAM.EXPECT().CreateRole(gomock.Any()).Return(&iam.CreateRoleOutput{Role: &iam.Role{Arn: testRoleArn}}, nil), ) gomock.InOrder( @@ -182,3 +182,69 @@ func TestCreateTaskExecutionRole_ErrorOnCreatePolicyFails(t *testing.T) { _, err := createTaskExecutionRole(testParams, mocks.MockIAM, mocks.MockKMS) assert.Error(t, err, "Expected error when CreatePolicy fails") } + +func TestCreateTaskExecutionRoleWithTags(t *testing.T) { + testRegistry := "myreg.test.io" + testRegCredARN := "arn:aws:secret/some-test-arn" + testRegKMSKey := "arn:aws:kms:key/67yt-756yth" + + testCreds := map[string]regcredio.CredsOutputEntry{ + testRegistry: regcredio.BuildOutputEntry(testRegCredARN, testRegKMSKey, []string{"test"}), + } + + testRoleName := "myNginxProjectRole" + + testPolicyArn := aws.String("arn:aws:iam::policy/" + testRoleName + "-policy") + testRoleArn := aws.String("arn:aws:iam::role/" + testRoleName) + + testParams := executionRoleParams{ + CredEntries: testCreds, + RoleName: testRoleName, + Region: "us-west-2", + Tags: map[string]*string{ + "Hey": aws.String("Jude"), + "Come": aws.String("Together"), + "Hello": aws.String("Goodbye"), + "Abbey": aws.String("Road"), + }, + } + + expectedTags := []*iam.Tag{ + &iam.Tag{ + Key: aws.String("Hey"), + Value: aws.String("Jude"), + }, + &iam.Tag{ + Key: aws.String("Come"), + Value: aws.String("Together"), + }, + &iam.Tag{ + Key: aws.String("Hello"), + Value: aws.String("Goodbye"), + }, + &iam.Tag{ + Key: aws.String("Abbey"), + Value: aws.String("Road"), + }, + } + + mocks := setupTestController(t) + gomock.InOrder( + mocks.MockIAM.EXPECT().CreateOrFindRole(testRoleName, roleDescriptionString, assumeRolePolicyDocString, gomock.Any()).Do(func(w, x, y, z interface{}) { + tags := z.([]*iam.Tag) + assert.ElementsMatch(t, tags, expectedTags, "Expected Tags to match") + }).Return(*testRoleArn, nil), + mocks.MockIAM.EXPECT().CreateRole(gomock.Any()).Return(&iam.CreateRoleOutput{Role: &iam.Role{Arn: testRoleArn}}, nil), + ) + gomock.InOrder( + // If KMSKeyID present, first thing to happen should be verifying its ARN + mocks.MockKMS.EXPECT().GetValidKeyARN(testRegKMSKey).Return(testRegKMSKey, nil), + mocks.MockIAM.EXPECT().CreatePolicy(gomock.Any()).Return(&iam.CreatePolicyOutput{Policy: &iam.Policy{Arn: testPolicyArn}}, nil), + mocks.MockIAM.EXPECT().AttachRolePolicy(getExecutionRolePolicyARN("us-west-2"), testRoleName).Return(nil, nil), + mocks.MockIAM.EXPECT().AttachRolePolicy(*testPolicyArn, testRoleName).Return(nil, nil), + ) + + policyCreateTime, err := createTaskExecutionRole(testParams, mocks.MockIAM, mocks.MockKMS) + assert.NoError(t, err, "Unexpected error when creating task execution role") + assert.NotNil(t, policyCreateTime, "Expected policy create time to be non-nil") +} diff --git a/ecs-cli/modules/cli/regcreds/regcreds_app.go b/ecs-cli/modules/cli/regcreds/regcreds_app.go index 4f0293e73..e155f366b 100644 --- a/ecs-cli/modules/cli/regcreds/regcreds_app.go +++ b/ecs-cli/modules/cli/regcreds/regcreds_app.go @@ -21,10 +21,13 @@ import ( "github.com/aws/amazon-ecs-cli/ecs-cli/modules/clients/aws/iam" "github.com/aws/amazon-ecs-cli/ecs-cli/modules/clients/aws/kms" secretsClient "github.com/aws/amazon-ecs-cli/ecs-cli/modules/clients/aws/secretsmanager" + "github.com/aws/amazon-ecs-cli/ecs-cli/modules/clients/aws/tagging" "github.com/aws/amazon-ecs-cli/ecs-cli/modules/commands/flags" "github.com/aws/amazon-ecs-cli/ecs-cli/modules/config" + "github.com/aws/amazon-ecs-cli/ecs-cli/modules/utils" "github.com/aws/amazon-ecs-cli/ecs-cli/modules/utils/regcredio" "github.com/aws/aws-sdk-go/aws" + taggingSDK "github.com/aws/aws-sdk-go/service/resourcegroupstaggingapi" "github.com/aws/aws-sdk-go/service/secretsmanager" "github.com/pkg/errors" log "github.com/sirupsen/logrus" @@ -85,6 +88,14 @@ func Up(c *cli.Context) { log.Fatal("Error executing 'up': ", err) } + var tags map[string]*string + if tagVal := c.String(flags.ResourceTagsFlag); tagVal != "" { + tags, err = utils.GetTagsMap(tagVal) + if err != nil { + log.Fatal("Error executing 'up': ", err) + } + } + var policyCreateTime *time.Time if !skipRole { region := commandConfig.Session.Config.Region @@ -93,6 +104,7 @@ func Up(c *cli.Context) { CredEntries: credentialOutput, RoleName: roleName, Region: *region, + Tags: tags, } policyCreateTime, err = createTaskExecutionRole(roleParams, iamClient, kmsClient) @@ -103,6 +115,14 @@ func Up(c *cli.Context) { log.Info("Skipping role creation.") } + if len(tags) > 0 { + taggingClient := tagging.NewTaggingClient(commandConfig) + err = tagRegistryCredentials(credentialOutput, tags, taggingClient) + if err != nil { + log.Fatal("Failed to tag resources: ", err) + } + } + // produce output file if !skipOutput { regcredio.GenerateCredsOutput(credentialOutput, roleName, outputDir, policyCreateTime) @@ -306,3 +326,26 @@ func validateOutputOptions(outputDir string, skipOutput bool) error { } return nil } + +func tagRegistryCredentials(creds map[string]regcredio.CredsOutputEntry, tags map[string]*string, taggingClient tagging.Client) error { + var arns []*string + + for _, credInfo := range creds { + arns = append(arns, aws.String(credInfo.CredentialARN)) + } + + input := &taggingSDK.TagResourcesInput{ + ResourceARNList: arns, + Tags: tags, + } + output, err := taggingClient.TagResources(input) + if err != nil { + return err + } + + for resource, info := range output.FailedResourcesMap { + return fmt.Errorf("Failed to tag resource %s; error=%s", resource, *info.ErrorMessage) + } + + return nil +} diff --git a/ecs-cli/modules/cli/regcreds/regcreds_app_test.go b/ecs-cli/modules/cli/regcreds/regcreds_app_test.go index 00a2965f0..559dbfa4c 100644 --- a/ecs-cli/modules/cli/regcreds/regcreds_app_test.go +++ b/ecs-cli/modules/cli/regcreds/regcreds_app_test.go @@ -21,9 +21,11 @@ import ( "github.com/aws/amazon-ecs-cli/ecs-cli/modules/clients/aws/iam/mock" "github.com/aws/amazon-ecs-cli/ecs-cli/modules/clients/aws/kms/mock" "github.com/aws/amazon-ecs-cli/ecs-cli/modules/clients/aws/secretsmanager/mock" + "github.com/aws/amazon-ecs-cli/ecs-cli/modules/clients/aws/tagging/mock" "github.com/aws/amazon-ecs-cli/ecs-cli/modules/utils/regcredio" "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/service/kms" + taggingSDK "github.com/aws/aws-sdk-go/service/resourcegroupstaggingapi" secretsmanager "github.com/aws/aws-sdk-go/service/secretsmanager" "github.com/golang/mock/gomock" "github.com/pkg/errors" @@ -216,6 +218,96 @@ func TestGetOrCreateRegistryCredentials_ErrorOnUpdate(t *testing.T) { assert.Error(t, err) } +func TestTagRegistryCredentials(t *testing.T) { + creds := map[string]regcredio.CredsOutputEntry{ + "the-who-registry.com": regcredio.CredsOutputEntry{ + CredentialARN: "arn:aws:secretsmanager:eu-west-1:111111111111:secret:path/whoareyou-1978", + }, + } + + tags := map[string]*string{ + "Baba": aws.String("O'riley"), + "Eminence": aws.String("Front"), + "My": aws.String("Generation"), + } + + ctrl := gomock.NewController(t) + + mockTagging := mock_tagging.NewMockClient(ctrl) + + gomock.InOrder( + mockTagging.EXPECT().TagResources(gomock.Any()).Do(func(x interface{}) { + input := x.(*taggingSDK.TagResourcesInput) + assert.Equal(t, tags, input.Tags, "Expected tags to match") + }).Return(&taggingSDK.TagResourcesOutput{}, nil), + ) + + err := tagRegistryCredentials(creds, tags, mockTagging) + assert.NoError(t, err, "Unexpected error calling tagRegistryCredentials") +} + +func TestTagRegistryCredentialsError(t *testing.T) { + creds := map[string]regcredio.CredsOutputEntry{ + "the-who-registry.com": regcredio.CredsOutputEntry{ + CredentialARN: "arn:aws:secretsmanager:eu-west-1:111111111111:secret:path/whoareyou-1978", + }, + } + + tags := map[string]*string{ + "Baba": aws.String("O'riley"), + "Eminence": aws.String("Front"), + "My": aws.String("Generation"), + } + + ctrl := gomock.NewController(t) + + mockTagging := mock_tagging.NewMockClient(ctrl) + + gomock.InOrder( + mockTagging.EXPECT().TagResources(gomock.Any()).Do(func(x interface{}) { + input := x.(*taggingSDK.TagResourcesInput) + assert.Equal(t, tags, input.Tags, "Expected tags to match") + }).Return(nil, fmt.Errorf("Some API error")), + ) + + err := tagRegistryCredentials(creds, tags, mockTagging) + assert.Error(t, err, "Expected error calling tagRegistryCredentials") +} + +func TestTagRegistryCredentialsFailedResources(t *testing.T) { + creds := map[string]regcredio.CredsOutputEntry{ + "the-who-registry.com": regcredio.CredsOutputEntry{ + CredentialARN: "arn:aws:secretsmanager:eu-west-1:111111111111:secret:path/whoareyou-1978", + }, + } + + tags := map[string]*string{ + "Baba": aws.String("O'riley"), + "Eminence": aws.String("Front"), + "My": aws.String("Generation"), + } + + ctrl := gomock.NewController(t) + + mockTagging := mock_tagging.NewMockClient(ctrl) + + gomock.InOrder( + mockTagging.EXPECT().TagResources(gomock.Any()).Do(func(x interface{}) { + input := x.(*taggingSDK.TagResourcesInput) + assert.Equal(t, tags, input.Tags, "Expected tags to match") + }).Return(&taggingSDK.TagResourcesOutput{ + FailedResourcesMap: map[string]*taggingSDK.FailureInfo{ + "arn:aws:secretsmanager:eu-west-1:111111111111:secret:path/whoareyou-1978": &taggingSDK.FailureInfo{ + ErrorMessage: aws.String("Auth Error: who are you"), + }, + }, + }, nil), + ) + + err := tagRegistryCredentials(creds, tags, mockTagging) + assert.Error(t, err, "Expected error calling tagRegistryCredentials") +} + func TestValidateCredsInput_ErrorEmptyCreds(t *testing.T) { emptyCredMap := make(map[string]regcredio.RegistryCredEntry) emptyCredsInput := regcredio.ECSRegCredsInput{ diff --git a/ecs-cli/modules/clients/aws/iam/client.go b/ecs-cli/modules/clients/aws/iam/client.go index c53f690d3..4fac9ecc1 100644 --- a/ecs-cli/modules/clients/aws/iam/client.go +++ b/ecs-cli/modules/clients/aws/iam/client.go @@ -27,7 +27,7 @@ type Client interface { AttachRolePolicy(policyArn, roleName string) (*iam.AttachRolePolicyOutput, error) CreateRole(iam.CreateRoleInput) (*iam.CreateRoleOutput, error) CreatePolicy(iam.CreatePolicyInput) (*iam.CreatePolicyOutput, error) - CreateOrFindRole(string, string, string) (string, error) + CreateOrFindRole(string, string, string, []*iam.Tag) (string, error) } type iamClient struct { @@ -81,12 +81,15 @@ func (c *iamClient) CreatePolicy(input iam.CreatePolicyInput) (*iam.CreatePolicy } // CreateOrFindRole returns a new role ARN or an empty string if role already exists -func (c *iamClient) CreateOrFindRole(roleName, roleDescription, assumeRolePolicyDoc string) (string, error) { +func (c *iamClient) CreateOrFindRole(roleName, roleDescription, assumeRolePolicyDoc string, tags []*iam.Tag) (string, error) { createRoleRequest := iam.CreateRoleInput{ AssumeRolePolicyDocument: aws.String(assumeRolePolicyDoc), Description: aws.String(roleDescription), RoleName: aws.String(roleName), } + if len(tags) > 0 { + createRoleRequest.Tags = tags + } roleResult, err := c.CreateRole(createRoleRequest) // if err is b/c role already exists, OK to continue if err != nil && !utils.EntityAlreadyExists(err) { diff --git a/ecs-cli/modules/clients/aws/iam/mock/client.go b/ecs-cli/modules/clients/aws/iam/mock/client.go index 3004e0b51..971f22011 100644 --- a/ecs-cli/modules/clients/aws/iam/mock/client.go +++ b/ecs-cli/modules/clients/aws/iam/mock/client.go @@ -61,16 +61,16 @@ func (mr *MockClientMockRecorder) AttachRolePolicy(arg0, arg1 interface{}) *gomo } // CreateOrFindRole mocks base method -func (m *MockClient) CreateOrFindRole(arg0, arg1, arg2 string) (string, error) { - ret := m.ctrl.Call(m, "CreateOrFindRole", arg0, arg1, arg2) +func (m *MockClient) CreateOrFindRole(arg0, arg1, arg2 string, arg3 []*iam.Tag) (string, error) { + ret := m.ctrl.Call(m, "CreateOrFindRole", arg0, arg1, arg2, arg3) ret0, _ := ret[0].(string) ret1, _ := ret[1].(error) return ret0, ret1 } // CreateOrFindRole indicates an expected call of CreateOrFindRole -func (mr *MockClientMockRecorder) CreateOrFindRole(arg0, arg1, arg2 interface{}) *gomock.Call { - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateOrFindRole", reflect.TypeOf((*MockClient)(nil).CreateOrFindRole), arg0, arg1, arg2) +func (mr *MockClientMockRecorder) CreateOrFindRole(arg0, arg1, arg2, arg3 interface{}) *gomock.Call { + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateOrFindRole", reflect.TypeOf((*MockClient)(nil).CreateOrFindRole), arg0, arg1, arg2, arg3) } // CreatePolicy mocks base method diff --git a/ecs-cli/modules/commands/regcreds/regcreds_command.go b/ecs-cli/modules/commands/regcreds/regcreds_command.go index 1ac54663e..fdba27da9 100644 --- a/ecs-cli/modules/commands/regcreds/regcreds_command.go +++ b/ecs-cli/modules/commands/regcreds/regcreds_command.go @@ -65,5 +65,9 @@ func regcredsUpFlags() []cli.Flag { Name: flags.OutputDirFlag, Usage: "[Optional] The directory where the output file should be created. If none specified, file will be created in the current working directory.", }, + cli.StringFlag{ + Name: flags.ResourceTagsFlag, + Usage: "[Optional] The AWS Resource tags to add to the Secrets Manager secrets and new IAM Role. Existing IAM Roles cannot be tagged.", + }, } }