From 7034254911ed4064c607d7a170f191fa6ee60836 Mon Sep 17 00:00:00 2001 From: Nicolas De Loof Date: Thu, 24 Sep 2020 16:11:08 +0200 Subject: [PATCH] introduce awsResources to replace CloudFormation parameters Signed-off-by: Nicolas De Loof --- ecs/awsResources.go | 278 +++++++++++++ ecs/backend.go | 15 +- ecs/cloudformation.go | 376 +++++------------- ecs/cloudformation_test.go | 34 +- ecs/convert.go | 3 +- ecs/ec2.go | 33 +- ecs/ps.go | 12 +- ecs/sdk.go | 33 +- .../simple-cloudformation-conversion.golden | 216 ++++------ ecs/up.go | 87 +--- ecs/volumes.go | 2 +- ecs/x.go | 2 +- 12 files changed, 510 insertions(+), 581 deletions(-) create mode 100644 ecs/awsResources.go diff --git a/ecs/awsResources.go b/ecs/awsResources.go new file mode 100644 index 000000000..d17473c05 --- /dev/null +++ b/ecs/awsResources.go @@ -0,0 +1,278 @@ +/* + Copyright 2020 Docker Compose CLI authors + + 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" + "fmt" + + "github.com/aws/aws-sdk-go/service/elbv2" + "github.com/awslabs/goformation/v4/cloudformation/ec2" + "github.com/awslabs/goformation/v4/cloudformation/elasticloadbalancingv2" + + "github.com/awslabs/goformation/v4/cloudformation" + "github.com/awslabs/goformation/v4/cloudformation/ecs" + "github.com/compose-spec/compose-go/types" + "github.com/sirupsen/logrus" +) + +// awsResources hold the AWS component being used or created to support services definition +type awsResources struct { + sdk sdk + vpc string + subnets []string + cluster string + loadBalancer string + loadBalancerType string + securityGroups map[string]string +} + +func (r *awsResources) serviceSecurityGroups(service types.ServiceConfig) []string { + var groups []string + for net := range service.Networks { + groups = append(groups, r.securityGroups[net]) + } + return groups +} + +func (r *awsResources) allSecurityGroups() []string { + var securityGroups []string + for _, r := range r.securityGroups { + securityGroups = append(securityGroups, r) + } + return securityGroups +} + +// parse look into compose project for configured resource to use, and check they are valid +func (r *awsResources) parse(ctx context.Context, project *types.Project) error { + return findProjectFnError(ctx, project, + r.parseClusterExtension, + r.parseVPCExtension, + r.parseLoadBalancerExtension, + r.parseSecurityGroupExtension, + ) +} + +func (r *awsResources) parseClusterExtension(ctx context.Context, project *types.Project) error { + if x, ok := project.Extensions[extensionCluster]; ok { + cluster := x.(string) + ok, err := r.sdk.ClusterExists(ctx, cluster) + if err != nil { + return err + } + if !ok { + return fmt.Errorf("cluster does not exist: %s", cluster) + } + r.cluster = cluster + } + return nil +} + +func (r *awsResources) parseVPCExtension(ctx context.Context, project *types.Project) error { + if x, ok := project.Extensions[extensionVPC]; ok { + vpc := x.(string) + err := r.sdk.CheckVPC(ctx, vpc) + if err != nil { + return err + } + r.vpc = vpc + } else { + defaultVPC, err := r.sdk.GetDefaultVPC(ctx) + if err != nil { + return err + } + r.vpc = defaultVPC + } + + subNets, err := r.sdk.GetSubNets(ctx, r.vpc) + if err != nil { + return err + } + if len(subNets) < 2 { + return fmt.Errorf("VPC %s should have at least 2 associated subnets in different availability zones", r.vpc) + } + r.subnets = subNets + return nil +} + +func (r *awsResources) parseLoadBalancerExtension(ctx context.Context, project *types.Project) error { + if x, ok := project.Extensions[extensionLoadBalancer]; ok { + loadBalancer := x.(string) + loadBalancerType, err := r.sdk.LoadBalancerType(ctx, loadBalancer) + if err != nil { + return err + } + + required := getRequiredLoadBalancerType(project) + if loadBalancerType != required { + return fmt.Errorf("load balancer %s is of type %s, project require a %s", loadBalancer, loadBalancerType, required) + } + + r.loadBalancer = loadBalancer + r.loadBalancerType = loadBalancerType + } + return nil +} + +func (r *awsResources) parseSecurityGroupExtension(ctx context.Context, project *types.Project) error { + if r.securityGroups == nil { + r.securityGroups = make(map[string]string, len(project.Networks)) + } + for name, net := range project.Networks { + if net.External.External { + r.securityGroups[name] = net.Name + } + if x, ok := net.Extensions[extensionSecurityGroup]; ok { + logrus.Warn("to use an existing security-group, use `network.external` and `network.name` in your compose file") + logrus.Debugf("Security Group for network %q set by user to %q", net.Name, x) + r.securityGroups[name] = x.(string) + } + } + return nil +} + +// ensure all required resources pre-exists or are defined as cloudformation resources +func (r *awsResources) ensure(project *types.Project, template *cloudformation.Template) { + r.ensureCluster(project, template) + r.ensureNetworks(project, template) + r.ensureLoadBalancer(project, template) +} + +func (r *awsResources) ensureCluster(project *types.Project, template *cloudformation.Template) { + if r.cluster != "" { + return + } + template.Resources["Cluster"] = &ecs.Cluster{ + ClusterName: project.Name, + Tags: projectTags(project), + } + r.cluster = cloudformation.Ref("Cluster") +} + +func (r *awsResources) ensureNetworks(project *types.Project, template *cloudformation.Template) { + if r.securityGroups == nil { + r.securityGroups = make(map[string]string, len(project.Networks)) + } + for name, net := range project.Networks { + securityGroup := networkResourceName(name) + template.Resources[securityGroup] = &ec2.SecurityGroup{ + GroupDescription: fmt.Sprintf("%s Security Group for %s network", project.Name, name), + GroupName: securityGroup, + VpcId: r.vpc, + Tags: networkTags(project, net), + } + + ingress := securityGroup + "Ingress" + template.Resources[ingress] = &ec2.SecurityGroupIngress{ + Description: fmt.Sprintf("Allow communication within network %s", name), + IpProtocol: "-1", // all protocols + GroupId: cloudformation.Ref(securityGroup), + SourceSecurityGroupId: cloudformation.Ref(securityGroup), + } + + r.securityGroups[name] = cloudformation.Ref(securityGroup) + } +} + +func (r *awsResources) ensureLoadBalancer(project *types.Project, template *cloudformation.Template) { + if r.loadBalancer != "" { + return + } + if allServices(project.Services, func(it types.ServiceConfig) bool { + return len(it.Ports) == 0 + }) { + logrus.Debug("Application does not expose any public port, so no need for a LoadBalancer") + return + } + + balancerType := getRequiredLoadBalancerType(project) + template.Resources["LoadBalancer"] = &elasticloadbalancingv2.LoadBalancer{ + Scheme: elbv2.LoadBalancerSchemeEnumInternetFacing, + SecurityGroups: r.getLoadBalancerSecurityGroups(project), + Subnets: r.subnets, + Tags: projectTags(project), + Type: balancerType, + } + r.loadBalancer = cloudformation.Ref("LoadBalancer") + r.loadBalancerType = balancerType +} + +func (r *awsResources) getLoadBalancerSecurityGroups(project *types.Project) []string { + securityGroups := []string{} + for name, network := range project.Networks { + if !network.Internal { + securityGroups = append(securityGroups, r.securityGroups[name]) + } + } + return securityGroups +} + +func getRequiredLoadBalancerType(project *types.Project) string { + loadBalancerType := elbv2.LoadBalancerTypeEnumNetwork + if allServices(project.Services, func(it types.ServiceConfig) bool { + return allPorts(it.Ports, portIsHTTP) + }) { + loadBalancerType = elbv2.LoadBalancerTypeEnumApplication + } + return loadBalancerType +} + +func portIsHTTP(it types.ServicePortConfig) bool { + if v, ok := it.Extensions[extensionProtocol]; ok { + protocol := v.(string) + return protocol == "http" || protocol == "https" + } + return it.Target == 80 || it.Target == 443 +} + +type projectFn func(ctx context.Context, project *types.Project) error + +func findProjectFnError(ctx context.Context, project *types.Project, funcs ...projectFn) error { + for _, fn := range funcs { + err := fn(ctx, project) + if err != nil { + return err + } + } + return nil +} + +// predicate[types.ServiceConfig] +type servicePredicate func(it types.ServiceConfig) bool + +// all[types.ServiceConfig] +func allServices(services types.Services, p servicePredicate) bool { + for _, s := range services { + if !p(s) { + return false + } + } + return true +} + +// predicate[types.ServicePortConfig] +type portPredicate func(it types.ServicePortConfig) bool + +// all[types.ServicePortConfig] +func allPorts(ports []types.ServicePortConfig, p portPredicate) bool { + for _, s := range ports { + if !p(s) { + return false + } + } + return true +} diff --git a/ecs/backend.go b/ecs/backend.go index 42e48bf46..a6f60b17b 100644 --- a/ecs/backend.go +++ b/ecs/backend.go @@ -73,17 +73,20 @@ func getEcsAPIService(ecsCtx store.EcsContext) (*ecsAPIService, error) { return nil, err } + sdk := newSDK(sess) return &ecsAPIService{ - ctx: ecsCtx, - Region: ecsCtx.Region, - SDK: newSDK(sess), + ctx: ecsCtx, + Region: ecsCtx.Region, + SDK: sdk, + resources: awsResources{sdk: sdk}, }, nil } type ecsAPIService struct { - ctx store.EcsContext - Region string - SDK sdk + ctx store.EcsContext + Region string + SDK sdk + resources awsResources } func (a *ecsAPIService) ContainerService() containers.Service { diff --git a/ecs/cloudformation.go b/ecs/cloudformation.go index cb2b6001c..d4a8714cb 100644 --- a/ecs/cloudformation.go +++ b/ecs/cloudformation.go @@ -35,15 +35,6 @@ import ( "github.com/awslabs/goformation/v4/cloudformation/secretsmanager" cloudmap "github.com/awslabs/goformation/v4/cloudformation/servicediscovery" "github.com/compose-spec/compose-go/types" - "github.com/sirupsen/logrus" -) - -const ( - parameterClusterName = "ParameterClusterName" - parameterVPCId = "ParameterVPCId" - parameterSubnet1Id = "ParameterSubnet1Id" - parameterSubnet2Id = "ParameterSubnet2Id" - parameterLoadBalancerARN = "ParameterLoadBalancerARN" ) func (b *ecsAPIService) Convert(ctx context.Context, project *types.Project) ([]byte, error) { @@ -52,7 +43,12 @@ func (b *ecsAPIService) Convert(ctx context.Context, project *types.Project) ([] return nil, err } - template, networks, err := b.convert(project) + err = b.resources.parse(ctx, project) + if err != nil { + return nil, err + } + + template, err := b.convert(project) if err != nil { return nil, err } @@ -68,7 +64,7 @@ func (b *ecsAPIService) Convert(ctx context.Context, project *types.Project) ([] } } - err = b.createCapacityProvider(ctx, project, networks, template) + err = b.createCapacityProvider(ctx, project, template) if err != nil { return nil, err } @@ -77,86 +73,31 @@ func (b *ecsAPIService) Convert(ctx context.Context, project *types.Project) ([] } // Convert a compose project into a CloudFormation template -func (b *ecsAPIService) convert(project *types.Project) (*cloudformation.Template, map[string]string, error) { //nolint:gocyclo +func (b *ecsAPIService) convert(project *types.Project) (*cloudformation.Template, error) { //nolint:gocyclo template := cloudformation.NewTemplate() - template.Description = "CloudFormation template created by Docker for deploying applications on Amazon ECS" - template.Parameters[parameterClusterName] = cloudformation.Parameter{ - Type: "String", - Description: "Name of the ECS cluster to deploy to (optional)", - } + b.resources.ensure(project, template) - template.Parameters[parameterVPCId] = cloudformation.Parameter{ - Type: "AWS::EC2::VPC::Id", - Description: "ID of the VPC", - } - - /* - FIXME can't set subnets: Ref("SubnetIds") see https://github.com/awslabs/goformation/issues/282 - template.Parameters["SubnetIds"] = cloudformation.Parameter{ - Type: "List", - Description: "The list of SubnetIds, for at least two Availability Zones in the region in your VPC", - } - */ - template.Parameters[parameterSubnet1Id] = cloudformation.Parameter{ - Type: "AWS::EC2::Subnet::Id", - Description: "SubnetId, for Availability Zone 1 in the region in your VPC", - } - template.Parameters[parameterSubnet2Id] = cloudformation.Parameter{ - Type: "AWS::EC2::Subnet::Id", - Description: "SubnetId, for Availability Zone 2 in the region in your VPC", - } - - template.Parameters[parameterLoadBalancerARN] = cloudformation.Parameter{ - Type: "String", - Description: "Name of the LoadBalancer to connect to (optional)", - } - - template.Conditions["CreateCluster"] = cloudformation.Equals("", cloudformation.Ref(parameterClusterName)) - - cluster := createCluster(project, template) - - networks := map[string]string{} - for _, net := range project.Networks { - networks[net.Name] = convertNetwork(project, net, cloudformation.Ref(parameterVPCId), template) - } - - for i, s := range project.Secrets { - if s.External.External { - continue - } - secret, err := ioutil.ReadFile(s.File) + for name, secret := range project.Secrets { + err := b.createSecret(project, name, secret, template) if err != nil { - return nil, nil, err + return nil, err } - - name := fmt.Sprintf("%sSecret", normalizeResourceName(s.Name)) - template.Resources[name] = &secretsmanager.Secret{ - Description: "", - SecretString: string(secret), - Tags: projectTags(project), - } - s.Name = cloudformation.Ref(name) - project.Secrets[i] = s } - createLogGroup(project, template) + b.createLogGroup(project, template) // Private DNS namespace will allow DNS name for the services to be ..local - createCloudMap(project, template) - - loadBalancerARN := createLoadBalancer(project, template) + b.createCloudMap(project, template) for _, service := range project.Services { + taskExecutionRole := b.createTaskExecutionRole(project, service, template) + taskRole := b.createTaskRole(service, template) - definition, err := convert(project, service) + definition, err := b.createTaskExecution(project, service) if err != nil { - return nil, nil, err + return nil, err } - - taskExecutionRole := createTaskExecutionRole(project, service, template) definition.ExecutionRoleArn = cloudformation.Ref(taskExecutionRole) - - taskRole := createTaskRole(service, template) if taskRole != "" { definition.TaskRoleArn = cloudformation.Ref(taskRole) } @@ -165,34 +106,30 @@ func (b *ecsAPIService) convert(project *types.Project) (*cloudformation.Templat template.Resources[taskDefinition] = definition var healthCheck *cloudmap.Service_HealthCheckConfig + serviceRegistry := b.createServiceRegistry(service, template, healthCheck) - serviceRegistry := createServiceRegistry(service, template, healthCheck) - - serviceSecurityGroups := []string{} - for net := range service.Networks { - serviceSecurityGroups = append(serviceSecurityGroups, networks[net]) - } - - dependsOn := []string{} - serviceLB := []ecs.Service_LoadBalancer{} - if len(service.Ports) > 0 { - for _, port := range service.Ports { - protocol := strings.ToUpper(port.Protocol) - if getLoadBalancerType(project) == elbv2.LoadBalancerTypeEnumApplication { - // we don't set Https as a certificate must be specified for HTTPS listeners - protocol = elbv2.ProtocolEnumHttp - } - if loadBalancerARN != "" { - targetGroupName := createTargetGroup(project, service, port, template, protocol) - listenerName := createListener(service, port, template, targetGroupName, loadBalancerARN, protocol) - dependsOn = append(dependsOn, listenerName) - serviceLB = append(serviceLB, ecs.Service_LoadBalancer{ - ContainerName: service.Name, - ContainerPort: int(port.Target), - TargetGroupArn: cloudformation.Ref(targetGroupName), - }) - } + var ( + dependsOn []string + serviceLB []ecs.Service_LoadBalancer + ) + for _, port := range service.Ports { + for net := range service.Networks { + b.createIngress(service, net, port, template) } + + protocol := strings.ToUpper(port.Protocol) + if b.resources.loadBalancerType == elbv2.LoadBalancerTypeEnumApplication { + // we don't set Https as a certificate must be specified for HTTPS listeners + protocol = elbv2.ProtocolEnumHttp + } + targetGroupName := b.createTargetGroup(project, service, port, template, protocol) + listenerName := b.createListener(service, port, template, targetGroupName, b.resources.loadBalancer, protocol) + dependsOn = append(dependsOn, listenerName) + serviceLB = append(serviceLB, ecs.Service_LoadBalancer{ + ContainerName: service.Name, + ContainerPort: int(port.Target), + TargetGroupArn: cloudformation.Ref(targetGroupName), + }) } desiredCount := 1 @@ -206,7 +143,7 @@ func (b *ecsAPIService) convert(project *types.Project) (*cloudformation.Templat minPercent, maxPercent, err := computeRollingUpdateLimits(service) if err != nil { - return nil, nil, err + return nil, err } assignPublicIP := ecsapi.AssignPublicIpEnabled @@ -220,7 +157,7 @@ func (b *ecsAPIService) convert(project *types.Project) (*cloudformation.Templat template.Resources[serviceResourceName(service.Name)] = &ecs.Service{ AWSCloudFormationDependsOn: dependsOn, - Cluster: cluster, + Cluster: b.resources.cluster, DesiredCount: desiredCount, DeploymentController: &ecs.Service_DeploymentController{ Type: ecsapi.DeploymentControllerTypeEcs, @@ -235,11 +172,8 @@ func (b *ecsAPIService) convert(project *types.Project) (*cloudformation.Templat NetworkConfiguration: &ecs.Service_NetworkConfiguration{ AwsvpcConfiguration: &ecs.Service_AwsVpcConfiguration{ AssignPublicIp: assignPublicIP, - SecurityGroups: serviceSecurityGroups, - Subnets: []string{ - cloudformation.Ref(parameterSubnet1Id), - cloudformation.Ref(parameterSubnet2Id), - }, + SecurityGroups: b.resources.serviceSecurityGroups(service), + Subnets: b.resources.subnets, }, }, PlatformVersion: platformVersion, @@ -250,10 +184,46 @@ func (b *ecsAPIService) convert(project *types.Project) (*cloudformation.Templat TaskDefinition: cloudformation.Ref(normalizeResourceName(taskDefinition)), } } - return template, networks, nil + return template, nil } -func createLogGroup(project *types.Project, template *cloudformation.Template) { +func (b *ecsAPIService) createIngress(service types.ServiceConfig, net string, port types.ServicePortConfig, template *cloudformation.Template) { + protocol := strings.ToUpper(port.Protocol) + if protocol == "" { + protocol = "-1" + } + ingress := fmt.Sprintf("%s%dIngress", normalizeResourceName(net), port.Target) + template.Resources[ingress] = &ec2.SecurityGroupIngress{ + CidrIp: "0.0.0.0/0", + Description: fmt.Sprintf("%s:%d/%s on %s nextwork", service.Name, port.Target, port.Protocol, net), + GroupId: b.resources.securityGroups[net], + FromPort: int(port.Target), + IpProtocol: protocol, + ToPort: int(port.Target), + } +} + +func (b *ecsAPIService) createSecret(project *types.Project, name string, s types.SecretConfig, template *cloudformation.Template) error { + if s.External.External { + return nil + } + sensitiveData, err := ioutil.ReadFile(s.File) + if err != nil { + return err + } + + resource := fmt.Sprintf("%sSecret", normalizeResourceName(s.Name)) + template.Resources[resource] = &secretsmanager.Secret{ + Description: fmt.Sprintf("Secret %s", s.Name), + SecretString: string(sensitiveData), + Tags: projectTags(project), + } + s.Name = cloudformation.Ref(resource) + project.Secrets[name] = s + return nil +} + +func (b *ecsAPIService) createLogGroup(project *types.Project, template *cloudformation.Template) { retention := 0 if v, ok := project.Extensions[extensionRetention]; ok { retention = v.(int) @@ -305,74 +275,9 @@ func computeRollingUpdateLimits(service types.ServiceConfig) (int, int, error) { return minPercent, maxPercent, nil } -func getLoadBalancerType(project *types.Project) string { - for _, service := range project.Services { - for _, port := range service.Ports { - protocol := port.Protocol - v, ok := port.Extensions[extensionProtocol] - if ok { - protocol = v.(string) - } - if protocol == "http" || protocol == "https" { - continue - } - if port.Published != 80 && port.Published != 443 { - return elbv2.LoadBalancerTypeEnumNetwork - } - } - } - return elbv2.LoadBalancerTypeEnumApplication -} - -func getLoadBalancerSecurityGroups(project *types.Project, template *cloudformation.Template) []string { - securityGroups := []string{} - for _, network := range project.Networks { - if !network.Internal { - net := convertNetwork(project, network, cloudformation.Ref(parameterVPCId), template) - securityGroups = append(securityGroups, net) - } - } - return uniqueStrings(securityGroups) -} - -func createLoadBalancer(project *types.Project, template *cloudformation.Template) string { - ports := 0 - for _, service := range project.Services { - ports += len(service.Ports) - } - if ports == 0 { - // Project do not expose any port (batch jobs?) - // So no need to create a PortPublisher - return "" - } - - // load balancer names are limited to 32 characters total - loadBalancerName := fmt.Sprintf("%.32s", fmt.Sprintf("%sLoadBalancer", strings.Title(project.Name))) - // Create PortPublisher if `ParameterLoadBalancerName` is not set - template.Conditions["CreateLoadBalancer"] = cloudformation.Equals("", cloudformation.Ref(parameterLoadBalancerARN)) - - loadBalancerType := getLoadBalancerType(project) - securityGroups := []string{} - if loadBalancerType == elbv2.LoadBalancerTypeEnumApplication { - securityGroups = getLoadBalancerSecurityGroups(project, template) - } - - template.Resources[loadBalancerName] = &elasticloadbalancingv2.LoadBalancer{ - Name: loadBalancerName, - Scheme: elbv2.LoadBalancerSchemeEnumInternetFacing, - SecurityGroups: securityGroups, - Subnets: []string{ - cloudformation.Ref(parameterSubnet1Id), - cloudformation.Ref(parameterSubnet2Id), - }, - Tags: projectTags(project), - Type: loadBalancerType, - AWSCloudFormationCondition: "CreateLoadBalancer", - } - return cloudformation.If("CreateLoadBalancer", cloudformation.Ref(loadBalancerName), cloudformation.Ref(parameterLoadBalancerARN)) -} - -func createListener(service types.ServiceConfig, port types.ServicePortConfig, template *cloudformation.Template, targetGroupName string, loadBalancerARN string, protocol string) string { +func (b *ecsAPIService) createListener(service types.ServiceConfig, port types.ServicePortConfig, + template *cloudformation.Template, + targetGroupName string, loadBalancerARN string, protocol string) string { listenerName := fmt.Sprintf( "%s%s%dListener", normalizeResourceName(service.Name), @@ -401,7 +306,7 @@ func createListener(service types.ServiceConfig, port types.ServicePortConfig, t return listenerName } -func createTargetGroup(project *types.Project, service types.ServiceConfig, port types.ServicePortConfig, template *cloudformation.Template, protocol string) string { +func (b *ecsAPIService) createTargetGroup(project *types.Project, service types.ServiceConfig, port types.ServicePortConfig, template *cloudformation.Template, protocol string) string { targetGroupName := fmt.Sprintf( "%s%s%dTargetGroup", normalizeResourceName(service.Name), @@ -414,12 +319,12 @@ func createTargetGroup(project *types.Project, service types.ServiceConfig, port Protocol: protocol, Tags: projectTags(project), TargetType: elbv2.TargetTypeEnumIp, - VpcId: cloudformation.Ref(parameterVPCId), + VpcId: b.resources.vpc, } return targetGroupName } -func createServiceRegistry(service types.ServiceConfig, template *cloudformation.Template, healthCheck *cloudmap.Service_HealthCheckConfig) ecs.Service_ServiceRegistry { +func (b *ecsAPIService) createServiceRegistry(service types.ServiceConfig, template *cloudformation.Template, healthCheck *cloudmap.Service_HealthCheckConfig) ecs.Service_ServiceRegistry { serviceRegistration := fmt.Sprintf("%sServiceDiscoveryEntry", normalizeResourceName(service.Name)) serviceRegistry := ecs.Service_ServiceRegistry{ RegistryArn: cloudformation.GetAtt(serviceRegistration, "Arn"), @@ -446,9 +351,9 @@ func createServiceRegistry(service types.ServiceConfig, template *cloudformation return serviceRegistry } -func createTaskExecutionRole(project *types.Project, service types.ServiceConfig, template *cloudformation.Template) string { +func (b *ecsAPIService) createTaskExecutionRole(project *types.Project, service types.ServiceConfig, template *cloudformation.Template) string { taskExecutionRole := fmt.Sprintf("%sTaskExecutionRole", normalizeResourceName(service.Name)) - policies := createPolicies(project, service) + policies := b.createPolicies(project, service) template.Resources[taskExecutionRole] = &iam.Role{ AssumeRolePolicyDocument: ecsTaskAssumeRolePolicyDocument, Policies: policies, @@ -460,7 +365,7 @@ func createTaskExecutionRole(project *types.Project, service types.ServiceConfig return taskExecutionRole } -func createTaskRole(service types.ServiceConfig, template *cloudformation.Template) string { +func (b *ecsAPIService) createTaskRole(service types.ServiceConfig, template *cloudformation.Template) string { taskRole := fmt.Sprintf("%sTaskRole", normalizeResourceName(service.Name)) rolePolicies := []iam.Role_Policy{} if roles, ok := service.Extensions[extensionRole]; ok { @@ -485,88 +390,15 @@ func createTaskRole(service types.ServiceConfig, template *cloudformation.Templa return taskRole } -func createCluster(project *types.Project, template *cloudformation.Template) string { - template.Resources["Cluster"] = &ecs.Cluster{ - ClusterName: project.Name, - Tags: projectTags(project), - AWSCloudFormationCondition: "CreateCluster", - } - cluster := cloudformation.If("CreateCluster", cloudformation.Ref("Cluster"), cloudformation.Ref(parameterClusterName)) - return cluster -} - -func createCloudMap(project *types.Project, template *cloudformation.Template) { +func (b *ecsAPIService) createCloudMap(project *types.Project, template *cloudformation.Template) { template.Resources["CloudMap"] = &cloudmap.PrivateDnsNamespace{ Description: fmt.Sprintf("Service Map for Docker Compose project %s", project.Name), Name: fmt.Sprintf("%s.local", project.Name), - Vpc: cloudformation.Ref(parameterVPCId), + Vpc: b.resources.vpc, } } -func convertNetwork(project *types.Project, net types.NetworkConfig, vpc string, template *cloudformation.Template) string { - if net.External.External { - return net.Name - } - if sg, ok := net.Extensions[extensionSecurityGroup]; ok { - logrus.Warn("to use an existing security-group, set `network.external` and `network.name` in your compose file") - logrus.Debugf("Security Group for network %q set by user to %q", net.Name, sg) - return sg.(string) - } - - var ingresses []ec2.SecurityGroup_Ingress - if !net.Internal { - for _, service := range project.Services { - if _, ok := service.Networks[net.Name]; ok { - for _, port := range service.Ports { - protocol := strings.ToUpper(port.Protocol) - if protocol == "" { - protocol = "-1" - } - ingresses = append(ingresses, ec2.SecurityGroup_Ingress{ - CidrIp: "0.0.0.0/0", - Description: fmt.Sprintf("%s:%d/%s", service.Name, port.Target, port.Protocol), - FromPort: int(port.Target), - IpProtocol: protocol, - ToPort: int(port.Target), - }) - } - } - } - } - - securityGroup := networkResourceName(project, net.Name) - template.Resources[securityGroup] = &ec2.SecurityGroup{ - GroupDescription: fmt.Sprintf("%s %s Security Group", project.Name, net.Name), - GroupName: securityGroup, - SecurityGroupIngress: ingresses, - VpcId: vpc, - Tags: networkTags(project, net), - } - - ingress := securityGroup + "Ingress" - template.Resources[ingress] = &ec2.SecurityGroupIngress{ - Description: fmt.Sprintf("Allow communication within network %s", net.Name), - IpProtocol: "-1", // all protocols - GroupId: cloudformation.Ref(securityGroup), - SourceSecurityGroupId: cloudformation.Ref(securityGroup), - } - - return cloudformation.Ref(securityGroup) -} - -func networkResourceName(project *types.Project, network string) string { - return fmt.Sprintf("%s%sNetwork", normalizeResourceName(project.Name), normalizeResourceName(network)) -} - -func serviceResourceName(service string) string { - return fmt.Sprintf("%sService", normalizeResourceName(service)) -} - -func normalizeResourceName(s string) string { - return strings.Title(regexp.MustCompile("[^a-zA-Z0-9]+").ReplaceAllString(s, "")) -} - -func createPolicies(project *types.Project, service types.ServiceConfig) []iam.Role_Policy { +func (b *ecsAPIService) createPolicies(project *types.Project, service types.ServiceConfig) []iam.Role_Policy { var arns []string if value, ok := service.Extensions[extensionPullCredentials]; ok { arns = append(arns, value.(string)) @@ -593,14 +425,14 @@ func createPolicies(project *types.Project, service types.ServiceConfig) []iam.R return nil } -func uniqueStrings(items []string) []string { - keys := make(map[string]bool) - unique := []string{} - for _, item := range items { - if _, val := keys[item]; !val { - keys[item] = true - unique = append(unique, item) - } - } - return unique +func networkResourceName(network string) string { + return fmt.Sprintf("%sNetwork", normalizeResourceName(network)) +} + +func serviceResourceName(service string) string { + return fmt.Sprintf("%sService", normalizeResourceName(service)) +} + +func normalizeResourceName(s string) string { + return strings.Title(regexp.MustCompile("[^a-zA-Z0-9]+").ReplaceAllString(s, "")) } diff --git a/ecs/cloudformation_test.go b/ecs/cloudformation_test.go index d2d55ce7f..b07658a4b 100644 --- a/ecs/cloudformation_test.go +++ b/ecs/cloudformation_test.go @@ -170,13 +170,13 @@ networks: back-tier: internal: true `) - assert.Check(t, template.Resources["TestPublicNetwork"] != nil) - assert.Check(t, template.Resources["TestBacktierNetwork"] != nil) - assert.Check(t, template.Resources["TestBacktierNetworkIngress"] != nil) - i := template.Resources["TestPublicNetworkIngress"] + assert.Check(t, template.Resources["FronttierNetwork"] != nil) + assert.Check(t, template.Resources["BacktierNetwork"] != nil) + assert.Check(t, template.Resources["BacktierNetworkIngress"] != nil) + i := template.Resources["FronttierNetworkIngress"] assert.Check(t, i != nil) ingress := *i.(*ec2.SecurityGroupIngress) - assert.Check(t, ingress.SourceSecurityGroupId == cloudformation.Ref("TestPublicNetwork")) + assert.Check(t, ingress.SourceSecurityGroupId == cloudformation.Ref("FronttierNetwork")) } @@ -187,13 +187,6 @@ func TestLoadBalancerTypeApplication(t *testing.T) { image: nginx ports: - 80:80 -`, - `services: - test: - image: nginx - ports: - - target: 8080 - protocol: http `, `services: test: @@ -205,7 +198,7 @@ func TestLoadBalancerTypeApplication(t *testing.T) { } for _, y := range cases { template := convertYaml(t, y) - lb := template.Resources["TestLoadBalancer"] + lb := template.Resources["LoadBalancer"] assert.Check(t, lb != nil) loadBalancer := *lb.(*elasticloadbalancingv2.LoadBalancer) assert.Check(t, len(loadBalancer.Name) <= 32) @@ -328,7 +321,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") } @@ -341,7 +334,7 @@ services: - 80:80 - 88:88 `) - lb := template.Resources["TestLoadBalancer"] + lb := template.Resources["LoadBalancer"] assert.Check(t, lb != nil) loadBalancer := *lb.(*elasticloadbalancingv2.LoadBalancer) assert.Check(t, loadBalancer.Type == elbv2.LoadBalancerTypeEnumNetwork) @@ -411,8 +404,13 @@ services: } func convertResultAsString(t *testing.T, project *types.Project) string { - backend := &ecsAPIService{} - template, _, err := backend.convert(project) + backend := &ecsAPIService{ + resources: awsResources{ + vpc: "vpcID", + subnets: []string{"subnet1", "subnet2"}, + }, + } + template, err := backend.convert(project) assert.NilError(t, err) resultAsJSON, err := marshall(template) assert.NilError(t, err) @@ -432,7 +430,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 7e7f3070a..3b0636fb7 100644 --- a/ecs/convert.go +++ b/ecs/convert.go @@ -27,6 +27,7 @@ import ( "time" "github.com/aws/aws-sdk-go/aws" + ecsapi "github.com/aws/aws-sdk-go/service/ecs" "github.com/awslabs/goformation/v4/cloudformation" "github.com/awslabs/goformation/v4/cloudformation/ecs" @@ -39,7 +40,7 @@ import ( const secretsInitContainerImage = "docker/ecs-secrets-sidecar" -func convert(project *types.Project, service types.ServiceConfig) (*ecs.TaskDefinition, error) { +func (b *ecsAPIService) createTaskExecution(project *types.Project, service types.ServiceConfig) (*ecs.TaskDefinition, error) { cpu, mem, err := toLimits(service) if err != nil { return nil, err diff --git a/ecs/ec2.go b/ecs/ec2.go index c7d8d6819..ef3d28633 100644 --- a/ecs/ec2.go +++ b/ecs/ec2.go @@ -28,7 +28,7 @@ import ( "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 { +func (b *ecsAPIService) createCapacityProvider(ctx context.Context, project *types.Project, template *cloudformation.Template) error { var ec2 bool for _, s := range project.Services { if requireEC2(s) { @@ -51,11 +51,6 @@ func (b *ecsAPIService) createCapacityProvider(ctx context.Context, project *typ return err } - var securityGroups []string - for _, r := range networks { - securityGroups = append(securityGroups, r) - } - template.Resources["CapacityProvider"] = &ecs.CapacityProvider{ AutoScalingGroupProvider: &ecs.CapacityProvider_AutoScalingGroupProvider{ AutoScalingGroupArn: cloudformation.Ref("AutoscalingGroup"), @@ -63,36 +58,29 @@ func (b *ecsAPIService) createCapacityProvider(ctx context.Context, project *typ TargetCapacity: 100, }, }, - Tags: projectTags(project), - AWSCloudFormationCondition: "CreateCluster", + Tags: projectTags(project), } template.Resources["AutoscalingGroup"] = &autoscaling.AutoScalingGroup{ LaunchConfigurationName: cloudformation.Ref("LaunchConfiguration"), MaxSize: "10", //TODO MinSize: "1", - VPCZoneIdentifier: []string{ - cloudformation.Ref(parameterSubnet1Id), - cloudformation.Ref(parameterSubnet2Id), - }, - AWSCloudFormationCondition: "CreateCluster", + VPCZoneIdentifier: b.resources.subnets, } 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", + ImageId: ami, + InstanceType: machineType, + SecurityGroups: b.resources.allSecurityGroups(), + IamInstanceProfile: cloudformation.Ref("EC2InstanceProfile"), + UserData: userData, } template.Resources["EC2InstanceProfile"] = &iam.InstanceProfile{ - Roles: []string{cloudformation.Ref("EC2InstanceRole")}, - AWSCloudFormationCondition: "CreateCluster", + Roles: []string{cloudformation.Ref("EC2InstanceRole")}, } template.Resources["EC2InstanceRole"] = &iam.Role{ @@ -100,8 +88,7 @@ func (b *ecsAPIService) createCapacityProvider(ctx context.Context, project *typ ManagedPolicyArns: []string{ ecsEC2InstanceRole, }, - Tags: projectTags(project), - AWSCloudFormationCondition: "CreateCluster", + Tags: projectTags(project), } cluster := template.Resources["Cluster"].(*ecs.Cluster) diff --git a/ecs/ps.go b/ecs/ps.go index 318326c41..a54b7dbfc 100644 --- a/ecs/ps.go +++ b/ecs/ps.go @@ -25,18 +25,15 @@ import ( ) func (b *ecsAPIService) Ps(ctx context.Context, project string) ([]compose.ServiceStatus, error) { - parameters, err := b.SDK.ListStackParameters(ctx, project) - if err != nil { - return nil, err - } - cluster := parameters[parameterClusterName] - resources, err := b.SDK.ListStackResources(ctx, project) if err != nil { return nil, err } - servicesARN := []string{} + var ( + cluster = project + servicesARN []string + ) for _, r := range resources { switch r.Type { case "AWS::ECS::Service": @@ -45,6 +42,7 @@ func (b *ecsAPIService) Ps(ctx context.Context, project string) ([]compose.Servi cluster = r.ARN } } + if len(servicesARN) == 0 { return nil, nil } diff --git a/ecs/sdk.go b/ecs/sdk.go index 906e2d427..13dbb63cd 100644 --- a/ecs/sdk.go +++ b/ecs/sdk.go @@ -196,22 +196,13 @@ func (s sdk) StackExists(ctx context.Context, name string) (bool, error) { return len(stacks.Stacks) > 0, nil } -func (s sdk) CreateStack(ctx context.Context, name string, template []byte, parameters map[string]string) error { +func (s sdk) CreateStack(ctx context.Context, name string, template []byte) error { logrus.Debug("Create CloudFormation stack") - param := []*cloudformation.Parameter{} - for name, value := range parameters { - param = append(param, &cloudformation.Parameter{ - ParameterKey: aws.String(name), - ParameterValue: aws.String(value), - }) - } - _, err := s.CF.CreateStackWithContext(ctx, &cloudformation.CreateStackInput{ OnFailure: aws.String("DELETE"), StackName: aws.String(name), TemplateBody: aws.String(string(template)), - Parameters: param, TimeoutInMinutes: nil, Capabilities: []*string{ aws.String(cloudformation.CapabilityCapabilityIam), @@ -226,24 +217,15 @@ func (s sdk) CreateStack(ctx context.Context, name string, template []byte, para return err } -func (s sdk) CreateChangeSet(ctx context.Context, name string, template []byte, parameters map[string]string) (string, error) { +func (s sdk) CreateChangeSet(ctx context.Context, name string, template []byte) (string, error) { logrus.Debug("Create CloudFormation Changeset") - param := []*cloudformation.Parameter{} - for name := range parameters { - param = append(param, &cloudformation.Parameter{ - ParameterKey: aws.String(name), - UsePreviousValue: aws.Bool(true), - }) - } - update := fmt.Sprintf("Update%s", time.Now().Format("2006-01-02-15-04-05")) changeset, err := s.CF.CreateChangeSetWithContext(ctx, &cloudformation.CreateChangeSetInput{ ChangeSetName: aws.String(update), ChangeSetType: aws.String(cloudformation.ChangeSetTypeUpdate), StackName: aws.String(name), TemplateBody: aws.String(string(template)), - Parameters: param, Capabilities: []*string{ aws.String(cloudformation.CapabilityCapabilityIam), }, @@ -647,15 +629,18 @@ func (s sdk) GetPublicIPs(ctx context.Context, interfaces ...string) (map[string return publicIPs, nil } -func (s sdk) LoadBalancerExists(ctx context.Context, arn string) (bool, error) { - logrus.Debug("CheckRequirements if PortPublisher exists: ", arn) +func (s sdk) LoadBalancerType(ctx context.Context, arn string) (string, error) { + logrus.Debug("Check if LoadBalancer exists: ", arn) lbs, err := s.ELB.DescribeLoadBalancersWithContext(ctx, &elbv2.DescribeLoadBalancersInput{ LoadBalancerArns: []*string{aws.String(arn)}, }) if err != nil { - return false, err + return "", err } - return len(lbs.LoadBalancers) > 0, nil + if len(lbs.LoadBalancers) == 0 { + return "", fmt.Errorf("load balancer does not exist: %s", arn) + } + return aws.StringValue(lbs.LoadBalancers[0].Type), nil } func (s sdk) GetLoadBalancerURL(ctx context.Context, arn string) (string, error) { diff --git a/ecs/testdata/simple/simple-cloudformation-conversion.golden b/ecs/testdata/simple/simple-cloudformation-conversion.golden index c977241c4..56ec12f06 100644 --- a/ecs/testdata/simple/simple-cloudformation-conversion.golden +++ b/ecs/testdata/simple/simple-cloudformation-conversion.golden @@ -1,59 +1,15 @@ { "AWSTemplateFormatVersion": "2010-09-09", - "Conditions": { - "CreateCluster": { - "Fn::Equals": [ - "", - { - "Ref": "ParameterClusterName" - } - ] - }, - "CreateLoadBalancer": { - "Fn::Equals": [ - "", - { - "Ref": "ParameterLoadBalancerARN" - } - ] - } - }, - "Description": "CloudFormation template created by Docker for deploying applications on Amazon ECS", - "Parameters": { - "ParameterClusterName": { - "Description": "Name of the ECS cluster to deploy to (optional)", - "Type": "String" - }, - "ParameterLoadBalancerARN": { - "Description": "Name of the LoadBalancer to connect to (optional)", - "Type": "String" - }, - "ParameterSubnet1Id": { - "Description": "SubnetId, for Availability Zone 1 in the region in your VPC", - "Type": "AWS::EC2::Subnet::Id" - }, - "ParameterSubnet2Id": { - "Description": "SubnetId, for Availability Zone 2 in the region in your VPC", - "Type": "AWS::EC2::Subnet::Id" - }, - "ParameterVPCId": { - "Description": "ID of the VPC", - "Type": "AWS::EC2::VPC::Id" - } - }, "Resources": { "CloudMap": { "Properties": { "Description": "Service Map for Docker Compose project TestSimpleConvert", "Name": "TestSimpleConvert.local", - "Vpc": { - "Ref": "ParameterVPCId" - } + "Vpc": "vpcID" }, "Type": "AWS::ServiceDiscovery::PrivateDnsNamespace" }, "Cluster": { - "Condition": "CreateCluster", "Properties": { "ClusterName": "TestSimpleConvert", "Tags": [ @@ -65,6 +21,72 @@ }, "Type": "AWS::ECS::Cluster" }, + "Default80Ingress": { + "Properties": { + "CidrIp": "0.0.0.0/0", + "Description": "simple:80/tcp on default nextwork", + "FromPort": 80, + "GroupId": { + "Ref": "DefaultNetwork" + }, + "IpProtocol": "TCP", + "ToPort": 80 + }, + "Type": "AWS::EC2::SecurityGroupIngress" + }, + "DefaultNetwork": { + "Properties": { + "GroupDescription": "TestSimpleConvert Security Group for default network", + "GroupName": "DefaultNetwork", + "Tags": [ + { + "Key": "com.docker.compose.project", + "Value": "TestSimpleConvert" + }, + { + "Key": "com.docker.compose.network", + "Value": "default" + } + ], + "VpcId": "vpcID" + }, + "Type": "AWS::EC2::SecurityGroup" + }, + "DefaultNetworkIngress": { + "Properties": { + "Description": "Allow communication within network default", + "GroupId": { + "Ref": "DefaultNetwork" + }, + "IpProtocol": "-1", + "SourceSecurityGroupId": { + "Ref": "DefaultNetwork" + } + }, + "Type": "AWS::EC2::SecurityGroupIngress" + }, + "LoadBalancer": { + "Properties": { + "Scheme": "internet-facing", + "SecurityGroups": [ + { + "Ref": "DefaultNetwork" + } + ], + "Subnets": [ + "subnet1", + "subnet2" + ], + "Tags": [ + { + "Key": "com.docker.compose.project", + "Value": "TestSimpleConvert" + } + ], + "Type": "application" + }, + "Type": "AWS::ElasticLoadBalancingV2::LoadBalancer" + }, "LogGroup": { "Properties": { "LogGroupName": "/docker-compose/TestSimpleConvert" @@ -77,15 +99,7 @@ ], "Properties": { "Cluster": { - "Fn::If": [ - "CreateCluster", - { - "Ref": "Cluster" - }, - { - "Ref": "ParameterClusterName" - } - ] + "Ref": "Cluster" }, "DeploymentConfiguration": { "MaximumPercent": 200, @@ -110,16 +124,12 @@ "AssignPublicIp": "ENABLED", "SecurityGroups": [ { - "Ref": "TestSimpleConvertDefaultNetwork" + "Ref": "DefaultNetwork" } ], "Subnets": [ - { - "Ref": "ParameterSubnet1Id" - }, - { - "Ref": "ParameterSubnet2Id" - } + "subnet1", + "subnet2" ] } }, @@ -191,15 +201,7 @@ } ], "LoadBalancerArn": { - "Fn::If": [ - "CreateLoadBalancer", - { - "Ref": "TestSimpleConvertLoadBalancer" - }, - { - "Ref": "ParameterLoadBalancerARN" - } - ] + "Ref": "LoadBalancer" }, "Port": 80, "Protocol": "HTTP" @@ -217,9 +219,7 @@ } ], "TargetType": "ip", - "VpcId": { - "Ref": "ParameterVPCId" - } + "VpcId": "vpcID" }, "Type": "AWS::ElasticLoadBalancingV2::TargetGroup" }, @@ -304,76 +304,6 @@ ] }, "Type": "AWS::IAM::Role" - }, - "TestSimpleConvertDefaultNetwork": { - "Properties": { - "GroupDescription": "TestSimpleConvert default Security Group", - "GroupName": "TestSimpleConvertDefaultNetwork", - "SecurityGroupIngress": [ - { - "CidrIp": "0.0.0.0/0", - "Description": "simple:80/tcp", - "FromPort": 80, - "IpProtocol": "TCP", - "ToPort": 80 - } - ], - "Tags": [ - { - "Key": "com.docker.compose.project", - "Value": "TestSimpleConvert" - }, - { - "Key": "com.docker.compose.network", - "Value": "default" - } - ], - "VpcId": { - "Ref": "ParameterVPCId" - } - }, - "Type": "AWS::EC2::SecurityGroup" - }, - "TestSimpleConvertDefaultNetworkIngress": { - "Properties": { - "Description": "Allow communication within network default", - "GroupId": { - "Ref": "TestSimpleConvertDefaultNetwork" - }, - "IpProtocol": "-1", - "SourceSecurityGroupId": { - "Ref": "TestSimpleConvertDefaultNetwork" - } - }, - "Type": "AWS::EC2::SecurityGroupIngress" - }, - "TestSimpleConvertLoadBalancer": { - "Condition": "CreateLoadBalancer", - "Properties": { - "Name": "TestSimpleConvertLoadBalancer", - "Scheme": "internet-facing", - "SecurityGroups": [ - { - "Ref": "TestSimpleConvertDefaultNetwork" - } - ], - "Subnets": [ - { - "Ref": "ParameterSubnet1Id" - }, - { - "Ref": "ParameterSubnet2Id" - } - ], - "Tags": [ - { - "Key": "com.docker.compose.project", - "Value": "TestSimpleConvert" - } - ], - "Type": "application" - }, - "Type": "AWS::ElasticLoadBalancingV2::LoadBalancer" } } } diff --git a/ecs/up.go b/ecs/up.go index 93ee67883..f959a1949 100644 --- a/ecs/up.go +++ b/ecs/up.go @@ -32,42 +32,11 @@ func (b *ecsAPIService) Up(ctx context.Context, project *types.Project) error { return err } - cluster, err := b.GetCluster(ctx, project) - if err != nil { - return err - } - template, err := b.Convert(ctx, project) if err != nil { return err } - vpc, err := b.GetVPC(ctx, project) - if err != nil { - return err - } - - subNets, err := b.SDK.GetSubNets(ctx, vpc) - if err != nil { - return err - } - if len(subNets) < 2 { - return fmt.Errorf("VPC %s should have at least 2 associated subnets in different availability zones", vpc) - } - - lb, err := b.GetLoadBalancer(ctx, project) - if err != nil { - return err - } - - parameters := map[string]string{ - parameterClusterName: cluster, - parameterVPCId: vpc, - parameterSubnet1Id: subNets[0], - parameterSubnet2Id: subNets[1], - parameterLoadBalancerARN: lb, - } - update, err := b.SDK.StackExists(ctx, project.Name) if err != nil { return err @@ -75,7 +44,7 @@ func (b *ecsAPIService) Up(ctx context.Context, project *types.Project) error { operation := stackCreate if update { operation = stackUpdate - changeset, err := b.SDK.CreateChangeSet(ctx, project.Name, template, parameters) + changeset, err := b.SDK.CreateChangeSet(ctx, project.Name, template) if err != nil { return err } @@ -84,7 +53,7 @@ func (b *ecsAPIService) Up(ctx context.Context, project *types.Project) error { return err } } else { - err = b.SDK.CreateStack(ctx, project.Name, template, parameters) + err = b.SDK.CreateStack(ctx, project.Name, template) if err != nil { return err } @@ -101,55 +70,3 @@ func (b *ecsAPIService) Up(ctx context.Context, project *types.Project) error { err = b.WaitStackCompletion(ctx, project.Name, operation) return err } - -func (b ecsAPIService) GetVPC(ctx context.Context, project *types.Project) (string, error) { - var vpcID string - //check compose file for custom VPC selected - if vpc, ok := project.Extensions[extensionVPC]; ok { - vpcID = vpc.(string) - } else { - defaultVPC, err := b.SDK.GetDefaultVPC(ctx) - if err != nil { - return "", err - } - vpcID = defaultVPC - } - - err := b.SDK.CheckVPC(ctx, vpcID) - if err != nil { - return "", err - } - return vpcID, nil -} - -func (b ecsAPIService) GetLoadBalancer(ctx context.Context, project *types.Project) (string, error) { - //check compose file for custom VPC selected - if ext, ok := project.Extensions[extensionLB]; ok { - lb := ext.(string) - ok, err := b.SDK.LoadBalancerExists(ctx, lb) - if err != nil { - return "", err - } - if !ok { - return "", fmt.Errorf("load balancer does not exist: %s", lb) - } - return lb, nil - } - return "", nil -} - -func (b ecsAPIService) GetCluster(ctx context.Context, project *types.Project) (string, error) { - //check compose file for custom VPC selected - if ext, ok := project.Extensions[extensionCluster]; ok { - cluster := ext.(string) - ok, err := b.SDK.ClusterExists(ctx, cluster) - if err != nil { - return "", err - } - if !ok { - return "", fmt.Errorf("cluster does not exist: %s", cluster) - } - return cluster, nil - } - return "", nil -} diff --git a/ecs/volumes.go b/ecs/volumes.go index 23f17add8..d3f246216 100644 --- a/ecs/volumes.go +++ b/ecs/volumes.go @@ -38,7 +38,7 @@ func (b *ecsAPIService) createNFSmountIngress(securityGroups []string, project * if ext, ok := network.Extensions[extensionSecurityGroup]; ok { source = ext.(string) } else { - source = networkResourceName(project, net) + source = networkResourceName(net) } break } diff --git a/ecs/x.go b/ecs/x.go index 4f417dcd6..9b93436c2 100644 --- a/ecs/x.go +++ b/ecs/x.go @@ -20,7 +20,7 @@ const ( extensionSecurityGroup = "x-aws-securitygroup" extensionVPC = "x-aws-vpc" extensionPullCredentials = "x-aws-pull_credentials" - extensionLB = "x-aws-loadbalancer" + extensionLoadBalancer = "x-aws-loadbalancer" extensionProtocol = "x-aws-protocol" extensionCluster = "x-aws-cluster" extensionKeys = "x-aws-keys"