// Copyright 2015-2018 Amazon.com, Inc. or its affiliates. All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"). You may // not use this file except in compliance with the License. A copy of the // License is located at // // http://aws.amazon.com/apache2.0/ // // or in the "license" file accompanying this file. This file is distributed // on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either // express or implied. See the License for the specific language governing // permissions and limitations under the License. package regcreds import ( "fmt" "strings" "time" "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" "github.com/urfave/cli" ) const ( maxContainersPerTaskDef = 10 ) // Up creates or updates registry credential secrets and an ECS task execution role needed to use them in a task def func Up(c *cli.Context) { args := c.Args() if len(args) != 1 { log.Fatal("Exactly 1 credential file is required. Found: ", len(args)) } // create clients commandConfig := getNewCommandConfig(c) smClient := secretsClient.NewSecretsManagerClient(commandConfig) kmsClient := kms.NewKMSClient(commandConfig) iamClient := iam.NewIAMClient(commandConfig) // validate provided values before creating any resources credsInput, err := regcredio.ReadCredsInput(args[0]) if err != nil { log.Fatal("Error executing 'up': ", err) } validatedRegCreds, err := validateCredsInput(*credsInput, kmsClient) if err != nil { log.Fatal("Error executing 'up': ", err) } roleName := c.String(flags.RoleNameFlag) skipRole := c.Bool(flags.NoRoleFlag) err = validateRoleDetails(roleName, skipRole) if err != nil { log.Fatal("Error executing 'up': ", err) } outputDir := c.String(flags.OutputDirFlag) skipOutput := c.Bool(flags.NoOutputFileFlag) err = validateOutputOptions(outputDir, skipOutput) if err != nil { log.Fatal("Error executing 'up': ", err) } // find or create secrets, role updateAllowed := c.Bool(flags.UpdateExistingSecretsFlag) credentialOutput, err := getOrCreateRegistryCredentials(validatedRegCreds, smClient, updateAllowed) if err != nil { 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 roleParams := executionRoleParams{ CredEntries: credentialOutput, RoleName: roleName, Region: *region, Tags: tags, } policyCreateTime, err = createTaskExecutionRole(roleParams, iamClient, kmsClient) if err != nil { log.Fatal("Error executing 'up': ", err) } } else { 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) } else { log.Info("Skipping generation of registry credentials output file.") } log.Info("\nIf your input file contains sensitive information, make sure that you delete it after use.") } func getOrCreateRegistryCredentials(entryMap regcredio.RegistryCreds, smClient secretsClient.SMClient, updateAllowed bool) (map[string]regcredio.CredsOutputEntry, error) { registryResults := make(map[string]regcredio.CredsOutputEntry) for registryName, credentialEntry := range entryMap { log.Infof("Processing credentials for registry %s...", registryName) arn := credentialEntry.SecretManagerARN var keyForSecret *string if arn == "" { newSecretARN, key, err := findOrCreateRegistrySecret(registryName, credentialEntry, smClient) if err != nil { return nil, err } arn = newSecretARN keyForSecret = &key } else if credentialEntry.HasCredPair() { if err := updateOrWarnForExistingSecret(credentialEntry, updateAllowed, smClient); err != nil { return nil, err } } else { log.Infof("Using existing secret %s.", arn) } if keyForSecret == nil { keyForSecret = &credentialEntry.KmsKeyID } registryResults[registryName] = regcredio.BuildOutputEntry(arn, *keyForSecret, credentialEntry.ContainerNames) } return registryResults, nil } // returns the ARN of a new or existing registry secret (and, if applicable, the KMS key associated with that secret) func findOrCreateRegistrySecret(registryName string, credEntry regcredio.RegistryCredEntry, smClient secretsClient.SMClient) (string, string, error) { secretName := generateECSResourceName(registryName) existingSecret, _ := smClient.DescribeSecret(*secretName) if existingSecret != nil { log.Infof("Existing credential secret found, using %s", *existingSecret.ARN) if existingSecret.KmsKeyId != nil { return *existingSecret.ARN, *existingSecret.KmsKeyId, nil } return *existingSecret.ARN, "", nil } secretString := generateSecretString(credEntry.Username, credEntry.Password) createSecretRequest := secretsmanager.CreateSecretInput{ Name: secretName, SecretString: secretString, Description: generateSecretDescription(registryName), } kmsKey := credEntry.KmsKeyID if kmsKey != "" { createSecretRequest.SetKmsKeyId(kmsKey) } output, err := smClient.CreateSecret(createSecretRequest) if err != nil { return "", "", err } log.Infof("New credential secret created: %s", *output.ARN) return *output.ARN, kmsKey, nil } func updateOrWarnForExistingSecret(credEntry regcredio.RegistryCredEntry, updateAllowed bool, smClient secretsClient.SMClient) error { secretARN := credEntry.SecretManagerARN if updateAllowed { updatedSecretString := generateSecretString(credEntry.Username, credEntry.Password) putSecretValueRequest := secretsmanager.PutSecretValueInput{ SecretId: aws.String(secretARN), SecretString: updatedSecretString, } _, err := smClient.PutSecretValue(putSecretValueRequest) if err != nil { return err } log.Infof("Updated existing secret %s with new value", secretARN) } else { log.Warnf("'username' and 'password' found but ignored for existing secret %s. To update existing secrets with new values, use '--update-existing-secrets' flag.", secretARN) } return nil } func validateCredsInput(input regcredio.ECSRegCredsInput, kmsClient kms.Client) (map[string]regcredio.RegistryCredEntry, error) { // TODO: validate version? inputRegCreds := input.RegistryCredentials if len(inputRegCreds) == 0 { return nil, errors.New("provided credentials must contain at least one registry") } if len(inputRegCreds) > maxContainersPerTaskDef { return nil, errors.New("no more than" + fmt.Sprint(maxContainersPerTaskDef) + "registry credential entries can be created at one time") } namedContainers := make(map[string]bool) outputRegCreds := make(map[string]regcredio.RegistryCredEntry) for registryName, credentialEntry := range inputRegCreds { if !credentialEntry.HasRequiredFields() { return nil, fmt.Errorf("missing required field(s) for registry %s; registry credentials should contain an existing secret ARN or username + password", registryName) } if len(credentialEntry.ContainerNames) > 0 { for _, container := range credentialEntry.ContainerNames { if namedContainers[container] { return nil, fmt.Errorf("container '%s' appears in more than one registry; container names must be unique across given registry credentials", container) } namedContainers[container] = true } } if len(credentialEntry.ContainerNames) == 0 { log.Warnf("No container names given for registry '%s'; output cannot be incorporated into a task definition when running 'compose' command", registryName) } if credentialEntry.SecretManagerARN != "" && !isARN(credentialEntry.SecretManagerARN) { return nil, fmt.Errorf("invalid secrets_manager_arn for registry %s", registryName) } // if key specified as ID or alias, validate & get ARN if credentialEntry.KmsKeyID != "" { keyARN, err := kmsClient.GetValidKeyARN(credentialEntry.KmsKeyID) if err != nil { return nil, err } credentialEntry.KmsKeyID = keyARN } // if both present, validate secret ARN & key are in same region if credentialEntry.SecretManagerARN != "" && credentialEntry.KmsKeyID != "" { secretRegion := strings.Split(credentialEntry.SecretManagerARN, ":")[3] keyRegion := strings.Split(credentialEntry.KmsKeyID, ":")[3] if secretRegion != keyRegion { return nil, fmt.Errorf("region of 'secrets_manager_arn'(%s) and 'kms_key_id'(%s) for registry %s do not match; secret and encryption key must be in same region", secretRegion, keyRegion, registryName) } } outputRegCreds[registryName] = credentialEntry } return outputRegCreds, nil } func getValidKeyARN(keyID string, kmsClient kms.Client) (string, error) { arn := "" if isARN(keyID) { arn = keyID } else { keyResult, err := kmsClient.DescribeKey(keyID) if err != nil { return "", err } keyMetadata := *keyResult.KeyMetadata arn = *keyMetadata.Arn } return arn, nil } func validateRoleDetails(roleName string, noRole bool) error { if noRole && roleName != "" { return fmt.Errorf("both role name ('%s') and '--no-role' specified; please specify either a role name or the '--no-role' flag", roleName) } if !noRole && roleName == "" { return errors.New("no value specified for '--role-name'; please specify either a role name or the '--no-role' flag") } return nil } func getNewCommandConfig(c *cli.Context) *config.CommandConfig { rdwr, err := config.NewReadWriter() if err != nil { log.Fatal("Error executing 'up': ", err) } commandConfig, err := config.NewCommandConfig(c, rdwr) if err != nil { log.Fatal("Error executing 'up': ", err) } return commandConfig } func validateOutputOptions(outputDir string, skipOutput bool) error { if outputDir != "" && skipOutput { return fmt.Errorf("Only one of '--"+flags.OutputDirFlag+"' (value '%s') and '--"+flags.NoOutputFileFlag+"' can be specified but both are present", outputDir) } 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 }