diff --git a/ecs/cloudformation.go b/ecs/cloudformation.go index 0dc489768..16c330741 100644 --- a/ecs/cloudformation.go +++ b/ecs/cloudformation.go @@ -23,8 +23,6 @@ import ( "regexp" "strings" - "github.com/docker/compose-cli/api/compose" - ecsapi "github.com/aws/aws-sdk-go/service/ecs" "github.com/aws/aws-sdk-go/service/elbv2" cloudmapapi "github.com/aws/aws-sdk-go/service/servicediscovery" @@ -36,7 +34,6 @@ import ( "github.com/awslabs/goformation/v4/cloudformation/logs" "github.com/awslabs/goformation/v4/cloudformation/secretsmanager" cloudmap "github.com/awslabs/goformation/v4/cloudformation/servicediscovery" - "github.com/awslabs/goformation/v4/cloudformation/tags" "github.com/compose-spec/compose-go/compatibility" "github.com/compose-spec/compose-go/errdefs" "github.com/compose-spec/compose-go/types" @@ -52,7 +49,7 @@ const ( ) func (b *ecsAPIService) Convert(ctx context.Context, project *types.Project) ([]byte, error) { - template, err := b.convert(project) + template, networks, err := b.convert(project) if err != nil { return nil, err } @@ -97,11 +94,16 @@ func (b *ecsAPIService) Convert(ctx context.Context, project *types.Project) ([] } } + err = b.createCapacityProvider(ctx, project, networks, template) + if err != nil { + return nil, err + } + return marshall(template) } // Convert a compose project into a CloudFormation template -func (b *ecsAPIService) convert(project *types.Project) (*cloudformation.Template, error) { //nolint:gocyclo +func (b *ecsAPIService) convert(project *types.Project) (*cloudformation.Template, map[string]string, error) { //nolint:gocyclo var checker compatibility.Checker = &fargateCompatibilityChecker{ compatibility.AllowList{ Supported: compatibleComposeAttributes, @@ -116,7 +118,7 @@ func (b *ecsAPIService) convert(project *types.Project) (*cloudformation.Templat } } if !compatibility.IsCompatible(checker) { - return nil, fmt.Errorf("compose file is incompatible with Amazon ECS") + return nil, nil, fmt.Errorf("compose file is incompatible with Amazon ECS") } template := cloudformation.NewTemplate() @@ -152,7 +154,6 @@ func (b *ecsAPIService) convert(project *types.Project) (*cloudformation.Templat Description: "Name of the LoadBalancer to connect to (optional)", } - // Createmount.nfs4: Connection timed out : unsuccessful EFS utils command execution; code: 32 Cluster is `ParameterClusterName` parameter is not set template.Conditions["CreateCluster"] = cloudformation.Equals("", cloudformation.Ref(parameterClusterName)) cluster := createCluster(project, template) @@ -168,19 +169,14 @@ func (b *ecsAPIService) convert(project *types.Project) (*cloudformation.Templat } secret, err := ioutil.ReadFile(s.File) if err != nil { - return nil, err + return nil, nil, err } name := fmt.Sprintf("%sSecret", normalizeResourceName(s.Name)) template.Resources[name] = &secretsmanager.Secret{ Description: "", SecretString: string(secret), - Tags: []tags.Tag{ - { - Key: compose.ProjectTag, - Value: project.Name, - }, - }, + Tags: projectTags(project), } s.Name = cloudformation.Ref(name) project.Secrets[i] = s @@ -197,7 +193,7 @@ func (b *ecsAPIService) convert(project *types.Project) (*cloudformation.Templat definition, err := convert(project, service) if err != nil { - return nil, err + return nil, nil, err } taskExecutionRole := createTaskExecutionRole(service, definition, template) @@ -255,7 +251,14 @@ func (b *ecsAPIService) convert(project *types.Project) (*cloudformation.Templat minPercent, maxPercent, err := computeRollingUpdateLimits(service) if err != nil { - return nil, err + return nil, nil, err + } + + launchType := ecsapi.LaunchTypeFargate + platformVersion := "1.4.0" // LATEST which is set to 1.3.0 (?) which doesn’t allow efs volumes. + if requireEC2(service) { + launchType = ecsapi.LaunchTypeEc2 + platformVersion = "" // The platform version must be null when specifying an EC2 launch type } template.Resources[serviceResourceName(service.Name)] = &ecs.Service{ @@ -269,11 +272,12 @@ func (b *ecsAPIService) convert(project *types.Project) (*cloudformation.Templat MaximumPercent: maxPercent, MinimumHealthyPercent: minPercent, }, - LaunchType: ecsapi.LaunchTypeFargate, + LaunchType: launchType, + // TODO we miss support for https://github.com/aws/containers-roadmap/issues/631 to select a capacity provider LoadBalancers: serviceLB, NetworkConfiguration: &ecs.Service_NetworkConfiguration{ AwsvpcConfiguration: &ecs.Service_AwsVpcConfiguration{ - AssignPublicIp: ecsapi.AssignPublicIpEnabled, + AssignPublicIp: ecsapi.AssignPublicIpDisabled, SecurityGroups: serviceSecurityGroups, Subnets: []string{ cloudformation.Ref(parameterSubnet1Id), @@ -281,24 +285,15 @@ func (b *ecsAPIService) convert(project *types.Project) (*cloudformation.Templat }, }, }, - PlatformVersion: "1.4.0", // LATEST which is set to 1.3.0 (?) which doesn’t allow efs volumes. + PlatformVersion: platformVersion, PropagateTags: ecsapi.PropagateTagsService, SchedulingStrategy: ecsapi.SchedulingStrategyReplica, ServiceRegistries: []ecs.Service_ServiceRegistry{serviceRegistry}, - Tags: []tags.Tag{ - { - Key: compose.ProjectTag, - Value: project.Name, - }, - { - Key: compose.ServiceTag, - Value: service.Name, - }, - }, - TaskDefinition: cloudformation.Ref(normalizeResourceName(taskDefinition)), + Tags: serviceTags(project, service), + TaskDefinition: cloudformation.Ref(normalizeResourceName(taskDefinition)), } } - return template, nil + return template, networks, nil } func createLogGroup(project *types.Project, template *cloudformation.Template) { @@ -413,12 +408,7 @@ func createLoadBalancer(project *types.Project, template *cloudformation.Templat cloudformation.Ref(parameterSubnet1Id), cloudformation.Ref(parameterSubnet2Id), }, - Tags: []tags.Tag{ - { - Key: compose.ProjectTag, - Value: project.Name, - }, - }, + Tags: projectTags(project), Type: loadBalancerType, AWSCloudFormationCondition: "CreateLoadBalancer", } @@ -462,14 +452,9 @@ func createTargetGroup(project *types.Project, service types.ServiceConfig, port port.Published, ) template.Resources[targetGroupName] = &elasticloadbalancingv2.TargetGroup{ - Port: int(port.Target), - Protocol: protocol, - Tags: []tags.Tag{ - { - Key: compose.ProjectTag, - Value: project.Name, - }, - }, + Port: int(port.Target), + Protocol: protocol, + Tags: projectTags(project), VpcId: cloudformation.Ref(parameterVPCId), TargetType: elbv2.TargetTypeEnumIp, } @@ -507,7 +492,7 @@ func createTaskExecutionRole(service types.ServiceConfig, definition *ecs.TaskDe taskExecutionRole := fmt.Sprintf("%sTaskExecutionRole", normalizeResourceName(service.Name)) policies := createPolicies(service, definition) template.Resources[taskExecutionRole] = &iam.Role{ - AssumeRolePolicyDocument: assumeRolePolicyDocument, + AssumeRolePolicyDocument: ecsTaskAssumeRolePolicyDocument, Policies: policies, ManagedPolicyArns: []string{ ecsTaskExecutionPolicy, @@ -535,7 +520,7 @@ func createTaskRole(service types.ServiceConfig, template *cloudformation.Templa return "" } template.Resources[taskRole] = &iam.Role{ - AssumeRolePolicyDocument: assumeRolePolicyDocument, + AssumeRolePolicyDocument: ecsTaskAssumeRolePolicyDocument, Policies: rolePolicies, ManagedPolicyArns: managedPolicies, } @@ -544,13 +529,8 @@ func createTaskRole(service types.ServiceConfig, template *cloudformation.Templa func createCluster(project *types.Project, template *cloudformation.Template) string { template.Resources["Cluster"] = &ecs.Cluster{ - ClusterName: project.Name, - Tags: []tags.Tag{ - { - Key: compose.ProjectTag, - Value: project.Name, - }, - }, + ClusterName: project.Name, + Tags: projectTags(project), AWSCloudFormationCondition: "CreateCluster", } cluster := cloudformation.If("CreateCluster", cloudformation.Ref("Cluster"), cloudformation.Ref(parameterClusterName)) @@ -598,16 +578,7 @@ func convertNetwork(project *types.Project, net types.NetworkConfig, vpc string, GroupName: securityGroup, SecurityGroupIngress: ingresses, VpcId: vpc, - Tags: []tags.Tag{ - { - Key: compose.ProjectTag, - Value: project.Name, - }, - { - Key: compose.NetworkTag, - Value: net.Name, - }, - }, + Tags: networkTags(project, net), } ingress := securityGroup + "Ingress" diff --git a/ecs/cloudformation_test.go b/ecs/cloudformation_test.go index fd2f087d4..c21508151 100644 --- a/ecs/cloudformation_test.go +++ b/ecs/cloudformation_test.go @@ -290,7 +290,7 @@ services: memory: 2043248M `) backend := &ecsAPIService{} - _, err := backend.convert(model) + _, _, err := backend.convert(model) assert.ErrorContains(t, err, "the resources requested are not supported by ECS/Fargate") } @@ -374,7 +374,7 @@ services: func convertResultAsString(t *testing.T, project *types.Project) string { backend := &ecsAPIService{} - template, err := backend.convert(project) + template, _, err := backend.convert(project) assert.NilError(t, err) resultAsJSON, err := marshall(template) assert.NilError(t, err) @@ -394,7 +394,7 @@ func load(t *testing.T, paths ...string) *types.Project { func convertYaml(t *testing.T, yaml string) *cloudformation.Template { project := loadConfig(t, yaml) backend := &ecsAPIService{} - template, err := backend.convert(project) + template, _, err := backend.convert(project) assert.NilError(t, err) return template } diff --git a/ecs/convert.go b/ecs/convert.go index ca32d00f0..4ceb50389 100644 --- a/ecs/convert.go +++ b/ecs/convert.go @@ -102,6 +102,11 @@ func convert(project *types.Project, service types.ServiceConfig) (*ecs.TaskDefi return nil, err } + var reservations *types.Resource + if service.Deploy != nil && service.Deploy.Resources.Reservations != nil { + reservations = service.Deploy.Resources.Reservations + } + containers := append(initContainers, ecs.TaskDefinition_ContainerDefinition{ Command: service.Command, DisableNetworking: service.NetworkMode == "none", @@ -129,7 +134,7 @@ func convert(project *types.Project, service types.ServiceConfig) (*ecs.TaskDefi PseudoTerminal: service.Tty, ReadonlyRootFilesystem: service.ReadOnly, RepositoryCredentials: credential, - ResourceRequirements: nil, + ResourceRequirements: toTaskResourceRequirements(reservations), StartTimeout: 0, StopTimeout: durationToInt(service.StopGracePeriod), SystemControls: toSystemControls(service.Sysctls), @@ -139,21 +144,44 @@ func convert(project *types.Project, service types.ServiceConfig) (*ecs.TaskDefi WorkingDirectory: service.WorkingDir, }) + launchType := ecsapi.LaunchTypeFargate + if requireEC2(service) { + launchType = ecsapi.LaunchTypeEc2 + } + return &ecs.TaskDefinition{ - ContainerDefinitions: containers, - Cpu: cpu, - Family: fmt.Sprintf("%s-%s", project.Name, service.Name), - IpcMode: service.Ipc, - Memory: mem, - NetworkMode: ecsapi.NetworkModeAwsvpc, // FIXME could be set by service.NetworkMode, Fargate only supports network mode ‘awsvpc’. - PidMode: service.Pid, - PlacementConstraints: toPlacementConstraints(service.Deploy), - ProxyConfiguration: nil, - RequiresCompatibilities: []string{ecsapi.LaunchTypeFargate}, - Volumes: volumes, + ContainerDefinitions: containers, + Cpu: cpu, + Family: fmt.Sprintf("%s-%s", project.Name, service.Name), + IpcMode: service.Ipc, + Memory: mem, + NetworkMode: ecsapi.NetworkModeAwsvpc, // FIXME could be set by service.NetworkMode, Fargate only supports network mode ‘awsvpc’. + PidMode: service.Pid, + PlacementConstraints: toPlacementConstraints(service.Deploy), + ProxyConfiguration: nil, + RequiresCompatibilities: []string{ + launchType, + }, + Volumes: volumes, }, nil } +func toTaskResourceRequirements(reservations *types.Resource) []ecs.TaskDefinition_ResourceRequirement { + if reservations == nil { + return nil + } + var requirements []ecs.TaskDefinition_ResourceRequirement + for _, r := range reservations.GenericResources { + if r.DiscreteResourceSpec.Kind == "gpus" { + requirements = append(requirements, ecs.TaskDefinition_ResourceRequirement{ + Type: ecsapi.ResourceTypeGpu, + Value: fmt.Sprint(r.DiscreteResourceSpec.Value), + }) + } + } + return requirements +} + func createSecretsSideCar(project *types.Project, service types.ServiceConfig, logConfiguration *ecs.TaskDefinition_LogConfiguration) ( ecs.TaskDefinition_Volume, ecs.TaskDefinition_MountPoint, @@ -295,7 +323,7 @@ func toSystemControls(sysctls types.Mapping) []ecs.TaskDefinition_SystemControl const miB = 1024 * 1024 func toLimits(service types.ServiceConfig) (string, string, error) { - // All possible cpu/mem values for Fargate + // All possible CPU/mem values for Fargate cpuToMem := map[int64][]types.UnitBytes{ 256: {512, 1024, 2048}, 512: {1024, 2048, 3072, 4096}, @@ -490,3 +518,20 @@ func getRepoCredentials(service types.ServiceConfig) *ecs.TaskDefinition_Reposit } return nil } + +func requireEC2(s types.ServiceConfig) bool { + return gpuRequirements(s) > 0 +} + +func gpuRequirements(s types.ServiceConfig) int64 { + if deploy := s.Deploy; deploy != nil { + if reservations := deploy.Resources.Reservations; reservations != nil { + for _, resource := range reservations.GenericResources { + if resource.DiscreteResourceSpec.Kind == "gpus" { + return resource.DiscreteResourceSpec.Value + } + } + } + } + return 0 +} diff --git a/ecs/ec2.go b/ecs/ec2.go new file mode 100644 index 000000000..cdaf58fd6 --- /dev/null +++ b/ecs/ec2.go @@ -0,0 +1,110 @@ +/* + Copyright 2020 Docker, Inc. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License 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 ecs + +import ( + "context" + "encoding/base64" + "fmt" + + "github.com/awslabs/goformation/v4/cloudformation" + "github.com/awslabs/goformation/v4/cloudformation/autoscaling" + "github.com/awslabs/goformation/v4/cloudformation/ecs" + "github.com/awslabs/goformation/v4/cloudformation/iam" + "github.com/compose-spec/compose-go/types" +) + +func (b *ecsAPIService) createCapacityProvider(ctx context.Context, project *types.Project, networks map[string]string, template *cloudformation.Template) error { + var ec2 bool + for _, s := range project.Services { + if requireEC2(s) { + ec2 = true + break + } + } + + if !ec2 { + return nil + } + + ami, err := b.SDK.GetParameter(ctx, "/aws/service/ecs/optimized-ami/amazon-linux-2/gpu/recommended") + if err != nil { + return err + } + + machineType := "g4dn.xlarge" // FIXME https://github.com/docker/compose-cli/pull/628 + + var securityGroups []string + for _, r := range networks { + securityGroups = append(securityGroups, r) + } + + template.Resources["CapacityProvider"] = &ecs.CapacityProvider{ + AutoScalingGroupProvider: &ecs.CapacityProvider_AutoScalingGroupProvider{ + AutoScalingGroupArn: cloudformation.Ref("AutoscalingGroup"), + ManagedScaling: &ecs.CapacityProvider_ManagedScaling{ + TargetCapacity: 100, + }, + }, + Tags: projectTags(project), + AWSCloudFormationCondition: "CreateCluster", + } + + template.Resources["AutoscalingGroup"] = &autoscaling.AutoScalingGroup{ + LaunchConfigurationName: cloudformation.Ref("LaunchConfiguration"), + MaxSize: "10", //TODO + MinSize: "1", + VPCZoneIdentifier: []string{ + cloudformation.Ref(parameterSubnet1Id), + cloudformation.Ref(parameterSubnet2Id), + }, + AWSCloudFormationCondition: "CreateCluster", + } + + userData := base64.StdEncoding.EncodeToString([]byte( + fmt.Sprintf("#!/bin/bash\necho ECS_CLUSTER=%s >> /etc/ecs/ecs.config", project.Name))) + + template.Resources["LaunchConfiguration"] = &autoscaling.LaunchConfiguration{ + ImageId: ami, + InstanceType: machineType, + SecurityGroups: securityGroups, + IamInstanceProfile: cloudformation.Ref("EC2InstanceProfile"), + UserData: userData, + AWSCloudFormationCondition: "CreateCluster", + } + + template.Resources["EC2InstanceProfile"] = &iam.InstanceProfile{ + Roles: []string{cloudformation.Ref("EC2InstanceRole")}, + AWSCloudFormationCondition: "CreateCluster", + } + + template.Resources["EC2InstanceRole"] = &iam.Role{ + AssumeRolePolicyDocument: ec2InstanceAssumeRolePolicyDocument, + ManagedPolicyArns: []string{ + ecsEC2InstanceRole, + }, + Tags: projectTags(project), + AWSCloudFormationCondition: "CreateCluster", + } + + cluster := template.Resources["Cluster"].(*ecs.Cluster) + cluster.CapacityProviders = []string{ + cloudformation.Ref("CapacityProvider"), + } + + return nil +} diff --git a/ecs/gpu.go b/ecs/gpu.go index c4427d440..a05e0f219 100644 --- a/ecs/gpu.go +++ b/ecs/gpu.go @@ -34,23 +34,47 @@ type machine struct { type family []machine -var p3family = family{ +var gpufamily = family{ { - id: "p3.2xlarge", - cpus: 8, - memory: 64 * units.GiB, - gpus: 2, + id: "g4dn.xlarge", + cpus: 4, + memory: 16 * units.GiB, + gpus: 1, }, { - id: "p3.8xlarge", + id: "g4dn.2xlarge", + cpus: 8, + memory: 32 * units.GiB, + gpus: 1, + }, + { + id: "g4dn.4xlarge", + cpus: 16, + memory: 64 * units.GiB, + gpus: 1, + }, + { + id: "g4dn.8xlarge", cpus: 32, - memory: 244 * units.GiB, + memory: 128 * units.GiB, + gpus: 1, + }, + { + id: "g4dn.12xlarge", + cpus: 48, + memory: 192 * units.GiB, gpus: 4, }, { - id: "p3.16xlarge", + id: "g4dn.16xlarge", cpus: 64, - memory: 488 * units.GiB, + memory: 256 * units.GiB, + gpus: 1, + }, + { + id: "g4dn.metal", + cpus: 96, + memory: 384 * units.GiB, gpus: 8, }, } @@ -82,7 +106,7 @@ func guessMachineType(project *types.Project) (string, error) { return "", err } - instanceType, err := p3family. + instanceType, err := gpufamily. filter(func(m machine) bool { return m.memory >= requirements.memory }). @@ -92,7 +116,7 @@ func guessMachineType(project *types.Project) (string, error) { filter(func(m machine) bool { return m.gpus >= requirements.gpus }). - firstOrError("none of the Amazon EC2 P3 instance types meet the requirements for memory:%d cpu:%f gpus:%d", requirements.memory, requirements.cpus, requirements.gpus) + firstOrError("none of the Amazon EC2 G4 instance types meet the requirements for memory:%d cpu:%f gpus:%d", requirements.memory, requirements.cpus, requirements.gpus) if err != nil { return "", err } diff --git a/ecs/gpu_test.go b/ecs/gpu_test.go index c4705e0c0..c952633bf 100644 --- a/ecs/gpu_test.go +++ b/ecs/gpu_test.go @@ -41,7 +41,7 @@ services: kind: gpus value: 1 `, - want: "p3.2xlarge", + want: "g4dn.xlarge", wantErr: false, }, { @@ -58,7 +58,7 @@ services: kind: gpus value: 4 `, - want: "p3.8xlarge", + want: "g4dn.12xlarge", wantErr: false, }, { @@ -76,7 +76,7 @@ services: kind: gpus value: 2 `, - want: "p3.16xlarge", + want: "g4dn.metal", wantErr: false, }, { @@ -95,7 +95,7 @@ services: kind: gpus value: 2 `, - want: "p3.8xlarge", + want: "g4dn.12xlarge", wantErr: false, }, } diff --git a/ecs/iam.go b/ecs/iam.go index b91ad6bb7..3a5174823 100644 --- a/ecs/iam.go +++ b/ecs/iam.go @@ -19,13 +19,14 @@ package ecs const ( ecsTaskExecutionPolicy = "arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy" ecrReadOnlyPolicy = "arn:aws:iam::aws:policy/AmazonEC2ContainerRegistryReadOnly" + ecsEC2InstanceRole = "arn:aws:iam::aws:policy/service-role/AmazonEC2ContainerServiceforEC2Role" actionGetSecretValue = "secretsmanager:GetSecretValue" actionGetParameters = "ssm:GetParameters" actionDecrypt = "kms:Decrypt" ) -var assumeRolePolicyDocument = PolicyDocument{ +var ecsTaskAssumeRolePolicyDocument = PolicyDocument{ Version: "2012-10-17", // https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_elements_version.html Statement: []PolicyStatement{ { @@ -38,6 +39,19 @@ var assumeRolePolicyDocument = PolicyDocument{ }, } +var ec2InstanceAssumeRolePolicyDocument = PolicyDocument{ + Version: "2012-10-17", // https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_elements_version.html + Statement: []PolicyStatement{ + { + Effect: "Allow", + Principal: PolicyPrincipal{ + Service: "ec2.amazonaws.com", + }, + Action: []string{"sts:AssumeRole"}, + }, + }, +} + // PolicyDocument describes an IAM policy document // could alternatively depend on https://github.com/kubernetes-sigs/cluster-api-provider-aws/blob/master/cmd/clusterawsadm/api/iam/v1alpha1/types.go type PolicyDocument struct { diff --git a/ecs/sdk.go b/ecs/sdk.go index 9b7db23f8..135ec20d7 100644 --- a/ecs/sdk.go +++ b/ecs/sdk.go @@ -18,10 +18,14 @@ package ecs import ( "context" + "encoding/json" "fmt" "strings" "time" + "github.com/aws/aws-sdk-go/service/ssm" + "github.com/aws/aws-sdk-go/service/ssm/ssmiface" + "github.com/docker/compose-cli/api/compose" "github.com/docker/compose-cli/api/secrets" @@ -56,6 +60,7 @@ type sdk struct { IAM iamiface.IAMAPI CF cloudformationiface.CloudFormationAPI SM secretsmanageriface.SecretsManagerAPI + SSM ssmiface.SSMAPI } func newSDK(sess *session.Session) sdk { @@ -71,6 +76,7 @@ func newSDK(sess *session.Session) sdk { IAM: iam.New(sess), CF: cloudformation.New(sess), SM: secretsmanager.New(sess), + SSM: ssm.New(sess), } } @@ -86,7 +92,7 @@ func (s sdk) CheckRequirements(ctx context.Context, region string) error { if *serviceLongArnFormat != "enabled" { return fmt.Errorf("this tool requires the \"new ARN resource ID format\".\n"+ "Check https://%s.console.aws.amazon.com/ecs/home#/settings\n"+ - "Learn more: https://aws.amazon.com/blogs/compute/migrating-your-amazon-ecs-deployment-to-the-new-arn-and-resource-id-format-2", region) + "Learn more: https://aws.amazon.com/blogs/compute/migrating-your-amazon-ecs-deployment-to-the-new-arn-and-resource-ID-format-2", region) } return nil } @@ -182,7 +188,7 @@ func (s sdk) StackExists(ctx context.Context, name string) (bool, error) { StackName: aws.String(name), }) if err != nil { - if strings.HasPrefix(err.Error(), fmt.Sprintf("ValidationError: Stack with id %s does not exist", name)) { + if strings.HasPrefix(err.Error(), fmt.Sprintf("ValidationError: Stack with ID %s does not exist", name)) { return false, nil } return false, nil @@ -688,3 +694,28 @@ func (s sdk) WithVolumeSecurityGroups(ctx context.Context, id string, fn func(se } return nil } + +func (s sdk) GetParameter(ctx context.Context, name string) (string, error) { + parameter, err := s.SSM.GetParameterWithContext(ctx, &ssm.GetParameterInput{ + Name: aws.String(name), + }) + if err != nil { + return "", err + } + + value := *parameter.Parameter.Value + var ami struct { + SchemaVersion int `json:"schema_version"` + ImageName string `json:"image_name"` + ImageID string `json:"image_id"` + OS string `json:"os"` + ECSRuntimeVersion string `json:"ecs_runtime_verion"` + ECSAgentVersion string `json:"ecs_agent_version"` + } + err = json.Unmarshal([]byte(value), &ami) + if err != nil { + return "", err + } + + return ami.ImageID, nil +} diff --git a/ecs/tags.go b/ecs/tags.go new file mode 100644 index 000000000..74c03158e --- /dev/null +++ b/ecs/tags.go @@ -0,0 +1,58 @@ +/* + Copyright 2020 Docker, Inc. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License 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 ecs + +import ( + "github.com/awslabs/goformation/v4/cloudformation/tags" + "github.com/compose-spec/compose-go/types" + "github.com/docker/compose-cli/api/compose" +) + +func projectTags(project *types.Project) []tags.Tag { + return []tags.Tag{ + { + Key: compose.ProjectTag, + Value: project.Name, + }, + } +} + +func serviceTags(project *types.Project, service types.ServiceConfig) []tags.Tag { + return []tags.Tag{ + { + Key: compose.ProjectTag, + Value: project.Name, + }, + { + Key: compose.ServiceTag, + Value: service.Name, + }, + } +} + +func networkTags(project *types.Project, net types.NetworkConfig) []tags.Tag { + return []tags.Tag{ + { + Key: compose.ProjectTag, + Value: project.Name, + }, + { + Key: compose.NetworkTag, + Value: net.Name, + }, + } +} diff --git a/ecs/testdata/simple/simple-cloudformation-conversion.golden b/ecs/testdata/simple/simple-cloudformation-conversion.golden index c977241c4..cbda1f5a7 100644 --- a/ecs/testdata/simple/simple-cloudformation-conversion.golden +++ b/ecs/testdata/simple/simple-cloudformation-conversion.golden @@ -107,7 +107,7 @@ ], "NetworkConfiguration": { "AwsvpcConfiguration": { - "AssignPublicIp": "ENABLED", + "AssignPublicIp": "DISABLED", "SecurityGroups": [ { "Ref": "TestSimpleConvertDefaultNetwork"