// Copyright 2015-2017 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 utils import ( "fmt" "github.com/aws/amazon-ecs-cli/ecs-cli/modules/cli/compose/adapter" "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/ecs" "github.com/pkg/errors" log "github.com/sirupsen/logrus" ) // TaskDefParams contains basic fields to build an ECS task definition type TaskDefParams struct { networkMode string taskRoleArn string cpu string memory string pidMode string ipcMode string containerDefs ContainerDefs executionRoleArn string } // ConvertTaskDefParams contains the inputs required to convert compose & ECS inputs into an ECS task definition type ConvertTaskDefParams struct { TaskDefName string TaskRoleArn string RequiredCompatibilites string Volumes *adapter.Volumes ContainerConfigs []adapter.ContainerConfig ECSParams *ECSParams ECSRegistryCreds *regcredio.ECSRegistryCredsOutput } // ConvertToTaskDefinition transforms the yaml configs to its ecs equivalent (task definition) func ConvertToTaskDefinition(params ConvertTaskDefParams) (*ecs.TaskDefinition, error) { if len(params.ContainerConfigs) == 0 { return nil, errors.New("cannot create a task definition with no containers; invalid service config") } // Instantiates zero values for fields on task def specified by ecs-params taskDefParams, err := convertTaskDefParams(params.ECSParams) if err != nil { return nil, err } // The task-role-arn flag takes precedence over a taskRoleArn value specified in ecs-params file. if params.TaskRoleArn == "" { params.TaskRoleArn = taskDefParams.taskRoleArn } // Create containerDefinitions containerDefinitions := []*ecs.ContainerDefinition{} for _, containerConfig := range params.ContainerConfigs { name := containerConfig.Name // Check if there are ecs-params specified for the container ecsContainerDef := &ContainerDef{Essential: true} if cd, ok := taskDefParams.containerDefs[name]; ok { ecsContainerDef = &cd } // Validate essential containers count := len(params.ContainerConfigs) if !hasEssential(taskDefParams.containerDefs, count) { return nil, errors.New("Task definition does not have any essential containers") } taskVals := taskLevelValues{ MemLimit: taskDefParams.memory, } containerDef, err := reconcileContainerDef(&containerConfig, ecsContainerDef, taskVals) if err != nil { return nil, err } containerDefinitions = append(containerDefinitions, containerDef) } ecsVolumes, err := convertToECSVolumes(params.Volumes, params.ECSParams) if err != nil { return nil, err } executionRoleArn := taskDefParams.executionRoleArn placementConstraints := convertToTaskDefinitionConstraints(params.ECSParams) // Check for and apply provided ecs-registry-creds values if params.ECSRegistryCreds != nil { err := addRegistryCredsToContainerDefs(containerDefinitions, params.ECSRegistryCreds.CredentialResources.ContainerCredentials) if err != nil { return nil, err } // if provided, add or replace existing executionRoleArn with value from cred file if params.ECSRegistryCreds.CredentialResources.TaskExecutionRole != "" { newExecutionRole := params.ECSRegistryCreds.CredentialResources.TaskExecutionRole if executionRoleArn != "" { // TODO: refactor 'showResourceOverrideMsg()' to take in override src and use here log.WithFields(log.Fields{ "option name": "task_execution_role", }).Infof("Using "+regcredio.ECSCredFileBaseName+" value as override (was %s but is now %s)", executionRoleArn, newExecutionRole) } else { log.WithFields(log.Fields{ "option name": "task_execution_role", }).Infof("Using "+regcredio.ECSCredFileBaseName+" value %s", newExecutionRole) } executionRoleArn = newExecutionRole } } // Note: this is later converted into an ecs.RegisterTaskDefinitionInput in entity_helper.go taskDefinition := &ecs.TaskDefinition{ Family: aws.String(params.TaskDefName), ContainerDefinitions: containerDefinitions, Volumes: ecsVolumes, TaskRoleArn: aws.String(params.TaskRoleArn), NetworkMode: aws.String(taskDefParams.networkMode), Cpu: aws.String(taskDefParams.cpu), Memory: aws.String(taskDefParams.memory), ExecutionRoleArn: aws.String(executionRoleArn), PlacementConstraints: placementConstraints, } // Set launch type if params.RequiredCompatibilites != "" { taskDefinition.RequiresCompatibilities = []*string{aws.String(params.RequiredCompatibilites)} } if taskDefParams.pidMode != "" { taskDefinition.SetPidMode(taskDefParams.pidMode) } if taskDefParams.ipcMode != "" { taskDefinition.SetIpcMode(taskDefParams.ipcMode) } return taskDefinition, nil } func resolveHealthCheck(serviceName string, healthCheck *ecs.HealthCheck, ecsParamsHealthCheck *HealthCheck) (*ecs.HealthCheck, error) { if ecsParamsHealthCheck != nil { healthCheckOverride, err := ecsParamsHealthCheck.ConvertToECSHealthCheck() if err != nil { return nil, err } if healthCheck != nil { healthCheck.Command = resolveStringSliceResourceOverride(serviceName, healthCheck.Command, healthCheckOverride.Command, "healthcheck command") healthCheck.Interval = resolveIntPointerResourceOverride(serviceName, healthCheck.Interval, healthCheckOverride.Interval, "healthcheck interval") healthCheck.Retries = resolveIntPointerResourceOverride(serviceName, healthCheck.Retries, healthCheckOverride.Retries, "healthcheck retries") healthCheck.Timeout = resolveIntPointerResourceOverride(serviceName, healthCheck.Timeout, healthCheckOverride.Timeout, "healthcheck timeout") healthCheck.StartPeriod = resolveIntPointerResourceOverride(serviceName, healthCheck.StartPeriod, healthCheckOverride.StartPeriod, "healthcheck start_period") } else { healthCheck = healthCheckOverride } } // validate healthcheck if healthCheck != nil && healthCheck.Validate() != nil { return healthCheck, fmt.Errorf("%s: test/command is a required field for container healthcheck", serviceName) } return healthCheck, nil } func resolveIntResourceOverride(serviceName string, composeVal, ecsParamsVal int64, option string) int64 { if composeVal > 0 && ecsParamsVal > 0 { showResourceOverrideMsg(serviceName, composeVal, ecsParamsVal, option) } if ecsParamsVal > 0 { return ecsParamsVal } return composeVal } func resolveIntPointerResourceOverride(serviceName string, composeVal, ecsParamsVal *int64, option string) *int64 { if composeVal != nil && ecsParamsVal != nil { showResourceOverrideMsg(serviceName, aws.Int64Value(composeVal), aws.Int64Value(ecsParamsVal), option) } if ecsParamsVal != nil { return ecsParamsVal } return composeVal } func resolveStringSliceResourceOverride(serviceName string, composeVal, ecsParamsVal []*string, option string) []*string { if len(composeVal) > 0 && len(ecsParamsVal) > 0 { log.WithFields(log.Fields{ "option name": option, "service name": serviceName, }).Infof("Using ecs-params value as override") } if len(ecsParamsVal) > 0 { return ecsParamsVal } return composeVal } func showResourceOverrideMsg(serviceName string, val int64, override int64, option string) { overrideMsg := "Using ecs-params value as override (was %v but is now %v)" log.WithFields(log.Fields{ "option name": option, "service name": serviceName, }).Infof(overrideMsg, val, override) } // convertToECSVolumes transforms the map of hostPaths to the format of ecs.Volume func convertToECSVolumes(hostPaths *adapter.Volumes, ecsParams *ECSParams) ([]*ecs.Volume, error) { output := []*ecs.Volume{} // volumes with a host path for hostPath, volName := range hostPaths.VolumeWithHost { ecsVolume := &ecs.Volume{ Name: aws.String(volName), Host: &ecs.HostVolumeProperties{ SourcePath: aws.String(hostPath), }} output = append(output, ecsVolume) } // volumes without host path (allowed to have Docker Volume Configuration) volumesWithoutHost, err := mergeVolumesWithoutHost(hostPaths.VolumeEmptyHost, ecsParams) if err != nil { return nil, err } output = append(output, volumesWithoutHost...) return output, nil } func convertToECSFirelensConfiguration(firelensConfiguration FirelensConfiguration) *ecs.FirelensConfiguration { ecsFirelensConfiguration := &ecs.FirelensConfiguration{ Type: aws.String(firelensConfiguration.Type), Options: aws.StringMap(firelensConfiguration.Options), } return ecsFirelensConfiguration } func convertToECSSecrets(secrets []Secret) []*ecs.Secret { var ecsSecrets []*ecs.Secret for _, secret := range secrets { s := &ecs.Secret{ ValueFrom: aws.String(secret.ValueFrom), Name: aws.String(secret.Name), } ecsSecrets = append(ecsSecrets, s) } return ecsSecrets } func convertToECSContainerDependency(dependencies []ContainerDependency) []*ecs.ContainerDependency { var ecsContainerDependencies []*ecs.ContainerDependency for _, dependency := range dependencies { d := &ecs.ContainerDependency{ Condition: aws.String(dependency.Condition), ContainerName: aws.String(dependency.ContainerName), } ecsContainerDependencies = append(ecsContainerDependencies, d) } return ecsContainerDependencies } func mergeVolumesWithoutHost(composeVolumes []string, ecsParams *ECSParams) ([]*ecs.Volume, error) { volumesWithoutHost := make(map[string]Volume) output := []*ecs.Volume{} for _, volName := range composeVolumes { volumesWithoutHost[volName] = Volume{} } if ecsParams != nil { for _, dockerVol := range ecsParams.TaskDefinition.DockerVolumes { if dockerVol.Name != "" { volumesWithoutHost[dockerVol.Name] = Volume{DockerVolumeConfig: dockerVol} } else { return nil, fmt.Errorf("Name is required when specifying a docker volume") } } for _, efsVol := range ecsParams.TaskDefinition.EFSVolumes { if efsVol.Name != "" { volumesWithoutHost[efsVol.Name] = Volume{EFSVolumeConfig: efsVol} } else { return nil, fmt.Errorf("Name is required when specifying an EFS volume") } } } var dVolCfg DockerVolume var efsVolCfg EFSVolume for volName, vol := range volumesWithoutHost { ecsVolume := &ecs.Volume{ Name: aws.String(volName), } dVolCfg = vol.DockerVolumeConfig efsVolCfg = vol.EFSVolumeConfig if dVolCfg.Name != "" { ecsVolume.DockerVolumeConfiguration = &ecs.DockerVolumeConfiguration{ Autoprovision: dVolCfg.Autoprovision, } if dVolCfg.Driver != nil { ecsVolume.DockerVolumeConfiguration.Driver = dVolCfg.Driver } if dVolCfg.Scope != nil { ecsVolume.DockerVolumeConfiguration.Scope = dVolCfg.Scope } if dVolCfg.DriverOptions != nil { ecsVolume.DockerVolumeConfiguration.DriverOpts = aws.StringMap(dVolCfg.DriverOptions) } if dVolCfg.Labels != nil { ecsVolume.DockerVolumeConfiguration.Labels = aws.StringMap(dVolCfg.Labels) } } if efsVolCfg.Name != "" { ecsVolume.EfsVolumeConfiguration = &ecs.EFSVolumeConfiguration{} if efsVolCfg.FileSystemID != nil { ecsVolume.EfsVolumeConfiguration.FileSystemId = efsVolCfg.FileSystemID } else { return nil, fmt.Errorf("file system id is required for efs volumes") } if efsVolCfg.RootDirectory != nil { ecsVolume.EfsVolumeConfiguration.RootDirectory = efsVolCfg.RootDirectory } var transitEncryptionRequired = false efsAuthCfg := &ecs.EFSAuthorizationConfig{} if efsVolCfg.IAM != nil { efsAuthCfg.Iam = efsVolCfg.IAM if *efsVolCfg.IAM == "ENABLED" { transitEncryptionRequired = true } } if efsVolCfg.AccessPointID != nil { efsAuthCfg.AccessPointId = efsVolCfg.AccessPointID transitEncryptionRequired = true } ecsVolume.EfsVolumeConfiguration.AuthorizationConfig = efsAuthCfg if efsVolCfg.TransitEncryption != nil { ecsVolume.EfsVolumeConfiguration.TransitEncryption = efsVolCfg.TransitEncryption } if transitEncryptionRequired && *efsVolCfg.TransitEncryption != "ENABLED" { return nil, fmt.Errorf("Transit encryption is required when using IAM access or an access point") } if efsVolCfg.TransitEncryptionPort != nil { ecsVolume.EfsVolumeConfiguration.TransitEncryptionPort = efsVolCfg.TransitEncryptionPort } } output = append(output, ecsVolume) } return output, nil } func hasEssential(ecsParamsContainerDefs ContainerDefs, count int) bool { // If the customer does not set the "essential" field on any container // definition, ECS will mark all containers in a TaskDefinition as // essential. Previously, since the customer could not pass in the // essential field, Task Definitions created through the CLI marked all // containers as essential. // Now that the essential field can be set by the customer via the // the ecs-params.yml file, we want to make sure that there is still at // least one essential container, i.e. that the customer does not // explicitly set all containers to be non-essential. nonEssentialCount := 0 for _, containerDef := range ecsParamsContainerDefs { if !containerDef.Essential { nonEssentialCount++ } } // 'count' is the total number of containers specified in the service config return nonEssentialCount != count } // Converts fields from ecsParams into the appropriate types for fields on an // ECS Task Definition func convertTaskDefParams(ecsParams *ECSParams) (params TaskDefParams, e error) { if ecsParams == nil { return params, nil } taskDef := ecsParams.TaskDefinition params.networkMode = taskDef.NetworkMode params.taskRoleArn = taskDef.TaskRoleArn params.containerDefs = taskDef.ContainerDefinitions params.cpu = taskDef.TaskSize.Cpu params.memory = taskDef.TaskSize.Memory params.executionRoleArn = taskDef.ExecutionRole params.ipcMode = taskDef.IPCMode params.pidMode = taskDef.PIDMode return params, nil } func addRegistryCredsToContainerDefs(containerDefs []*ecs.ContainerDefinition, containerCreds map[string]regcredio.CredsOutputEntry) error { credsMap, err := getContainersToCredsMap(containerCreds) if err != nil { return err } // set registry creds to applicable container definitions if len(credsMap) > 0 { for _, containerDef := range containerDefs { containerName := aws.StringValue(containerDef.Name) if foundCredParam := credsMap[containerName]; foundCredParam != "" { if containerDef.RepositoryCredentials != nil && aws.StringValue(containerDef.RepositoryCredentials.CredentialsParameter) != "" { log.WithFields(log.Fields{ "container name": containerName, "option name": "credentials_parameter", }).Infof("Using "+regcredio.ECSCredFileBaseName+" value as override (was %s but is now %s)", *containerDef.RepositoryCredentials.CredentialsParameter, foundCredParam) } else { log.WithFields(log.Fields{ "container name": containerName, "option name": "credentials_parameter", }).Infof("Using "+regcredio.ECSCredFileBaseName+" value %s", foundCredParam) } // set RepositoryCredentials to new value containerRepoCreds := ecs.RepositoryCredentials{ CredentialsParameter: aws.String(foundCredParam), } containerDef.RepositoryCredentials = &containerRepoCreds // remove container entry from cred map delete(credsMap, containerName) } } // if credMap contains container names not present in our container definitions, log a warning if len(credsMap) > 0 { unusedContainers := make([]string, 0, len(credsMap)) for container := range credsMap { unusedContainers = append(unusedContainers, container) } log.Warnf("Containers listed with registry credentials but not used: %v", unusedContainers) } } return nil } func getContainersToCredsMap(containerCreds map[string]regcredio.CredsOutputEntry) (map[string]string, error) { containerToCredMap := make(map[string]string) for registry, credEntry := range containerCreds { if credEntry.CredentialARN != "" && len(credEntry.ContainerNames) > 0 { credParam := credEntry.CredentialARN for _, containerName := range credEntry.ContainerNames { // if duplicate entries for a given container are found, return error if containerToCredMap[containerName] != "" { return nil, fmt.Errorf("Duplicate credential_parameter values found for container %s (%s and %s)", containerName, containerToCredMap[containerName], credParam) } containerToCredMap[containerName] = credParam } } else { log.Warnf("No containers found for registry %s", registry) } } return containerToCredMap, nil } func convertToTaskDefinitionConstraints(ecsParams *ECSParams) []*ecs.TaskDefinitionPlacementConstraint { if ecsParams == nil { return nil } placementConstraints := ecsParams.TaskDefinition.PlacementConstraints if len(placementConstraints) > 0 { tdPcs := make([]*ecs.TaskDefinitionPlacementConstraint, len(placementConstraints)) for i, pc := range placementConstraints { tdPcs[i] = &ecs.TaskDefinitionPlacementConstraint{ Type: aws.String(pc.Type), Expression: aws.String(pc.Expression), } } return tdPcs } return nil }