Merge pull request #638 from docker/clean

This commit is contained in:
Nicolas De loof 2020-09-29 12:08:31 +02:00 committed by GitHub
commit 3685cbbf8d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 612 additions and 638 deletions

284
ecs/awsResources.go Normal file
View File

@ -0,0 +1,284 @@
/*
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 {
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 (b *ecsAPIService) parse(ctx context.Context, project *types.Project) (awsResources, error) {
r := awsResources{}
var err error
r.cluster, err = b.parseClusterExtension(ctx, project)
if err != nil {
return r, err
}
r.vpc, r.subnets, err = b.parseVPCExtension(ctx, project)
if err != nil {
return r, err
}
r.loadBalancer, r.loadBalancerType, err = b.parseLoadBalancerExtension(ctx, project)
if err != nil {
return r, err
}
r.securityGroups, err = b.parseSecurityGroupExtension(ctx, project)
if err != nil {
return r, err
}
return r, nil
}
func (b *ecsAPIService) parseClusterExtension(ctx context.Context, project *types.Project) (string, error) {
if x, ok := project.Extensions[extensionCluster]; ok {
cluster := x.(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
}
func (b *ecsAPIService) parseVPCExtension(ctx context.Context, project *types.Project) (string, []string, error) {
var vpc string
if x, ok := project.Extensions[extensionVPC]; ok {
vpc = x.(string)
err := b.SDK.CheckVPC(ctx, vpc)
if err != nil {
return "", nil, err
}
} else {
defaultVPC, err := b.SDK.GetDefaultVPC(ctx)
if err != nil {
return "", nil, err
}
vpc = defaultVPC
}
subNets, err := b.SDK.GetSubNets(ctx, vpc)
if err != nil {
return "", nil, err
}
if len(subNets) < 2 {
return "", nil, fmt.Errorf("VPC %s should have at least 2 associated subnets in different availability zones", vpc)
}
return vpc, subNets, nil
}
func (b *ecsAPIService) parseLoadBalancerExtension(ctx context.Context, project *types.Project) (string, string, error) {
if x, ok := project.Extensions[extensionLoadBalancer]; ok {
loadBalancer := x.(string)
loadBalancerType, err := b.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)
}
return loadBalancer, loadBalancerType, nil
}
return "", "", nil
}
func (b *ecsAPIService) parseSecurityGroupExtension(ctx context.Context, project *types.Project) (map[string]string, error) {
securityGroups := make(map[string]string, len(project.Networks))
for name, net := range project.Networks {
var sg string
if net.External.External {
sg = 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)
sg = x.(string)
}
exists, err := b.SDK.SecurityGroupExists(ctx, sg)
if err != nil {
return nil, err
}
if !exists {
return nil, fmt.Errorf("security group %s doesn't exist", sg)
}
securityGroups[name] = sg
}
return securityGroups, nil
}
// ensureResources create required resources in template if not yet defined
func (b *ecsAPIService) ensureResources(resources *awsResources, project *types.Project, template *cloudformation.Template) {
b.ensureCluster(resources, project, template)
b.ensureNetworks(resources, project, template)
b.ensureLoadBalancer(resources, project, template)
}
func (b *ecsAPIService) ensureCluster(r *awsResources, 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 (b *ecsAPIService) ensureNetworks(r *awsResources, 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: allProtocols,
GroupId: cloudformation.Ref(securityGroup),
SourceSecurityGroupId: cloudformation.Ref(securityGroup),
}
r.securityGroups[name] = cloudformation.Ref(securityGroup)
}
}
func (b *ecsAPIService) ensureLoadBalancer(r *awsResources, 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
}
// 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
}

View File

@ -73,10 +73,11 @@ func getEcsAPIService(ecsCtx store.EcsContext) (*ecsAPIService, error) {
return nil, err return nil, err
} }
sdk := newSDK(sess)
return &ecsAPIService{ return &ecsAPIService{
ctx: ecsCtx, ctx: ecsCtx,
Region: ecsCtx.Region, Region: ecsCtx.Region,
SDK: newSDK(sess), SDK: sdk,
}, nil }, nil
} }

View File

@ -34,22 +34,21 @@ import (
"github.com/awslabs/goformation/v4/cloudformation/logs" "github.com/awslabs/goformation/v4/cloudformation/logs"
"github.com/awslabs/goformation/v4/cloudformation/secretsmanager" "github.com/awslabs/goformation/v4/cloudformation/secretsmanager"
cloudmap "github.com/awslabs/goformation/v4/cloudformation/servicediscovery" cloudmap "github.com/awslabs/goformation/v4/cloudformation/servicediscovery"
"github.com/compose-spec/compose-go/compatibility"
"github.com/compose-spec/compose-go/errdefs"
"github.com/compose-spec/compose-go/types" "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) { func (b *ecsAPIService) Convert(ctx context.Context, project *types.Project) ([]byte, error) {
template, networks, err := b.convert(project) err := b.checkCompatibility(project)
if err != nil {
return nil, err
}
resources, err := b.parse(ctx, project)
if err != nil {
return nil, err
}
template, err := b.convert(project, resources)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -58,43 +57,14 @@ func (b *ecsAPIService) Convert(ctx context.Context, project *types.Project) ([]
// as "source security group" use an arbitrary network attached to service(s) who mounts target volume // as "source security group" use an arbitrary network attached to service(s) who mounts target volume
for n, vol := range project.Volumes { for n, vol := range project.Volumes {
err := b.SDK.WithVolumeSecurityGroups(ctx, vol.Name, func(securityGroups []string) error { err := b.SDK.WithVolumeSecurityGroups(ctx, vol.Name, func(securityGroups []string) error {
target := securityGroups[0] return b.createNFSmountIngress(securityGroups, project, n, template)
for _, s := range project.Services {
for _, v := range s.Volumes {
if v.Source != n {
continue
}
var source string
for net := range s.Networks {
network := project.Networks[net]
if ext, ok := network.Extensions[extensionSecurityGroup]; ok {
source = ext.(string)
} else {
source = networkResourceName(project, net)
}
break
}
name := fmt.Sprintf("%sNFSMount%s", s.Name, n)
template.Resources[name] = &ec2.SecurityGroupIngress{
Description: fmt.Sprintf("Allow NFS mount for %s on %s", s.Name, n),
GroupId: target,
SourceSecurityGroupId: cloudformation.Ref(source),
IpProtocol: "tcp",
FromPort: 2049,
ToPort: 2049,
}
service := template.Resources[serviceResourceName(s.Name)].(*ecs.Service)
service.AWSCloudFormationDependsOn = append(service.AWSCloudFormationDependsOn, name)
}
}
return nil
}) })
if err != nil { if err != nil {
return nil, err return nil, err
} }
} }
err = b.createCapacityProvider(ctx, project, networks, template) err = b.createCapacityProvider(ctx, project, template, resources)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -103,103 +73,31 @@ func (b *ecsAPIService) Convert(ctx context.Context, project *types.Project) ([]
} }
// Convert a compose project into a CloudFormation template // 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, resources awsResources) (*cloudformation.Template, error) {
var checker compatibility.Checker = &fargateCompatibilityChecker{
compatibility.AllowList{
Supported: compatibleComposeAttributes,
},
}
compatibility.Check(project, checker)
for _, err := range checker.Errors() {
if errdefs.IsIncompatibleError(err) {
logrus.Error(err.Error())
} else {
logrus.Warn(err.Error())
}
}
if !compatibility.IsCompatible(checker) {
return nil, nil, fmt.Errorf("compose file is incompatible with Amazon ECS")
}
template := cloudformation.NewTemplate() template := cloudformation.NewTemplate()
template.Description = "CloudFormation template created by Docker for deploying applications on Amazon ECS" b.ensureResources(&resources, project, template)
template.Parameters[parameterClusterName] = cloudformation.Parameter{
Type: "String",
Description: "Name of the ECS cluster to deploy to (optional)",
}
template.Parameters[parameterVPCId] = cloudformation.Parameter{ for name, secret := range project.Secrets {
Type: "AWS::EC2::VPC::Id", err := b.createSecret(project, name, secret, template)
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<AWS::EC2::Subnet::Id>",
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)
if err != nil { 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 <service>.<project>.local // Private DNS namespace will allow DNS name for the services to be <service>.<project>.local
createCloudMap(project, template) b.createCloudMap(project, template, resources.vpc)
loadBalancerARN := createLoadBalancer(project, template)
for _, service := range project.Services { 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 { if err != nil {
return nil, nil, err return nil, err
} }
taskExecutionRole := createTaskExecutionRole(service, definition, template)
definition.ExecutionRoleArn = cloudformation.Ref(taskExecutionRole) definition.ExecutionRoleArn = cloudformation.Ref(taskExecutionRole)
taskRole := createTaskRole(service, template)
if taskRole != "" { if taskRole != "" {
definition.TaskRoleArn = cloudformation.Ref(taskRole) definition.TaskRoleArn = cloudformation.Ref(taskRole)
} }
@ -208,34 +106,30 @@ func (b *ecsAPIService) convert(project *types.Project) (*cloudformation.Templat
template.Resources[taskDefinition] = definition template.Resources[taskDefinition] = definition
var healthCheck *cloudmap.Service_HealthCheckConfig var healthCheck *cloudmap.Service_HealthCheckConfig
serviceRegistry := b.createServiceRegistry(service, template, healthCheck)
serviceRegistry := createServiceRegistry(service, template, healthCheck) var (
dependsOn []string
serviceSecurityGroups := []string{} serviceLB []ecs.Service_LoadBalancer
for net := range service.Networks { )
serviceSecurityGroups = append(serviceSecurityGroups, networks[net]) for _, port := range service.Ports {
} for net := range service.Networks {
b.createIngress(service, net, port, template, resources)
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),
})
}
} }
protocol := strings.ToUpper(port.Protocol)
if 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, resources.vpc)
listenerName := b.createListener(service, port, template, targetGroupName, 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 desiredCount := 1
@ -249,7 +143,7 @@ func (b *ecsAPIService) convert(project *types.Project) (*cloudformation.Templat
minPercent, maxPercent, err := computeRollingUpdateLimits(service) minPercent, maxPercent, err := computeRollingUpdateLimits(service)
if err != nil { if err != nil {
return nil, nil, err return nil, err
} }
assignPublicIP := ecsapi.AssignPublicIpEnabled assignPublicIP := ecsapi.AssignPublicIpEnabled
@ -263,7 +157,7 @@ func (b *ecsAPIService) convert(project *types.Project) (*cloudformation.Templat
template.Resources[serviceResourceName(service.Name)] = &ecs.Service{ template.Resources[serviceResourceName(service.Name)] = &ecs.Service{
AWSCloudFormationDependsOn: dependsOn, AWSCloudFormationDependsOn: dependsOn,
Cluster: cluster, Cluster: resources.cluster,
DesiredCount: desiredCount, DesiredCount: desiredCount,
DeploymentController: &ecs.Service_DeploymentController{ DeploymentController: &ecs.Service_DeploymentController{
Type: ecsapi.DeploymentControllerTypeEcs, Type: ecsapi.DeploymentControllerTypeEcs,
@ -278,11 +172,8 @@ func (b *ecsAPIService) convert(project *types.Project) (*cloudformation.Templat
NetworkConfiguration: &ecs.Service_NetworkConfiguration{ NetworkConfiguration: &ecs.Service_NetworkConfiguration{
AwsvpcConfiguration: &ecs.Service_AwsVpcConfiguration{ AwsvpcConfiguration: &ecs.Service_AwsVpcConfiguration{
AssignPublicIp: assignPublicIP, AssignPublicIp: assignPublicIP,
SecurityGroups: serviceSecurityGroups, SecurityGroups: resources.serviceSecurityGroups(service),
Subnets: []string{ Subnets: resources.subnets,
cloudformation.Ref(parameterSubnet1Id),
cloudformation.Ref(parameterSubnet2Id),
},
}, },
}, },
PlatformVersion: platformVersion, PlatformVersion: platformVersion,
@ -293,10 +184,48 @@ func (b *ecsAPIService) convert(project *types.Project) (*cloudformation.Templat
TaskDefinition: cloudformation.Ref(normalizeResourceName(taskDefinition)), TaskDefinition: cloudformation.Ref(normalizeResourceName(taskDefinition)),
} }
} }
return template, networks, nil return template, nil
} }
func createLogGroup(project *types.Project, template *cloudformation.Template) { const allProtocols = "-1"
func (b *ecsAPIService) createIngress(service types.ServiceConfig, net string, port types.ServicePortConfig, template *cloudformation.Template, resources awsResources) {
protocol := strings.ToUpper(port.Protocol)
if protocol == "" {
protocol = allProtocols
}
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: 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 retention := 0
if v, ok := project.Extensions[extensionRetention]; ok { if v, ok := project.Extensions[extensionRetention]; ok {
retention = v.(int) retention = v.(int)
@ -348,74 +277,9 @@ func computeRollingUpdateLimits(service types.ServiceConfig) (int, int, error) {
return minPercent, maxPercent, nil return minPercent, maxPercent, nil
} }
func getLoadBalancerType(project *types.Project) string { func (b *ecsAPIService) createListener(service types.ServiceConfig, port types.ServicePortConfig,
for _, service := range project.Services { template *cloudformation.Template,
for _, port := range service.Ports { targetGroupName string, loadBalancerARN string, protocol string) string {
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 {
listenerName := fmt.Sprintf( listenerName := fmt.Sprintf(
"%s%s%dListener", "%s%s%dListener",
normalizeResourceName(service.Name), normalizeResourceName(service.Name),
@ -444,7 +308,7 @@ func createListener(service types.ServiceConfig, port types.ServicePortConfig, t
return listenerName 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, vpc string) string {
targetGroupName := fmt.Sprintf( targetGroupName := fmt.Sprintf(
"%s%s%dTargetGroup", "%s%s%dTargetGroup",
normalizeResourceName(service.Name), normalizeResourceName(service.Name),
@ -457,12 +321,12 @@ func createTargetGroup(project *types.Project, service types.ServiceConfig, port
Protocol: protocol, Protocol: protocol,
Tags: projectTags(project), Tags: projectTags(project),
TargetType: elbv2.TargetTypeEnumIp, TargetType: elbv2.TargetTypeEnumIp,
VpcId: cloudformation.Ref(parameterVPCId), VpcId: vpc,
} }
return targetGroupName 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)) serviceRegistration := fmt.Sprintf("%sServiceDiscoveryEntry", normalizeResourceName(service.Name))
serviceRegistry := ecs.Service_ServiceRegistry{ serviceRegistry := ecs.Service_ServiceRegistry{
RegistryArn: cloudformation.GetAtt(serviceRegistration, "Arn"), RegistryArn: cloudformation.GetAtt(serviceRegistration, "Arn"),
@ -489,9 +353,9 @@ func createServiceRegistry(service types.ServiceConfig, template *cloudformation
return serviceRegistry return serviceRegistry
} }
func createTaskExecutionRole(service types.ServiceConfig, definition *ecs.TaskDefinition, 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)) taskExecutionRole := fmt.Sprintf("%sTaskExecutionRole", normalizeResourceName(service.Name))
policies := createPolicies(service, definition) policies := b.createPolicies(project, service)
template.Resources[taskExecutionRole] = &iam.Role{ template.Resources[taskExecutionRole] = &iam.Role{
AssumeRolePolicyDocument: ecsTaskAssumeRolePolicyDocument, AssumeRolePolicyDocument: ecsTaskAssumeRolePolicyDocument,
Policies: policies, Policies: policies,
@ -503,7 +367,7 @@ func createTaskExecutionRole(service types.ServiceConfig, definition *ecs.TaskDe
return taskExecutionRole 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)) taskRole := fmt.Sprintf("%sTaskRole", normalizeResourceName(service.Name))
rolePolicies := []iam.Role_Policy{} rolePolicies := []iam.Role_Policy{}
if roles, ok := service.Extensions[extensionRole]; ok { if roles, ok := service.Extensions[extensionRole]; ok {
@ -528,99 +392,21 @@ func createTaskRole(service types.ServiceConfig, template *cloudformation.Templa
return taskRole return taskRole
} }
func createCluster(project *types.Project, template *cloudformation.Template) string { func (b *ecsAPIService) createCloudMap(project *types.Project, template *cloudformation.Template, vpc 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) {
template.Resources["CloudMap"] = &cloudmap.PrivateDnsNamespace{ template.Resources["CloudMap"] = &cloudmap.PrivateDnsNamespace{
Description: fmt.Sprintf("Service Map for Docker Compose project %s", project.Name), Description: fmt.Sprintf("Service Map for Docker Compose project %s", project.Name),
Name: fmt.Sprintf("%s.local", project.Name), Name: fmt.Sprintf("%s.local", project.Name),
Vpc: cloudformation.Ref(parameterVPCId), Vpc: vpc,
} }
} }
func convertNetwork(project *types.Project, net types.NetworkConfig, vpc string, template *cloudformation.Template) string { func (b *ecsAPIService) createPolicies(project *types.Project, service types.ServiceConfig) []iam.Role_Policy {
if net.External.External { var arns []string
return net.Name if value, ok := service.Extensions[extensionPullCredentials]; ok {
arns = append(arns, value.(string))
} }
if sg, ok := net.Extensions[extensionSecurityGroup]; ok { for _, secret := range service.Secrets {
logrus.Warn("to use an existing security-group, set `network.external` and `network.name` in your compose file") arns = append(arns, project.Secrets[secret.Source].Name)
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(service types.ServiceConfig, taskDef *ecs.TaskDefinition) []iam.Role_Policy {
arns := []string{}
for _, container := range taskDef.ContainerDefinitions {
if container.RepositoryCredentials != nil {
arns = append(arns, container.RepositoryCredentials.CredentialsParameter)
}
if len(container.Secrets) > 0 {
for _, s := range container.Secrets {
arns = append(arns, s.ValueFrom)
}
}
} }
if len(arns) > 0 { if len(arns) > 0 {
return []iam.Role_Policy{ return []iam.Role_Policy{
@ -641,14 +427,14 @@ func createPolicies(service types.ServiceConfig, taskDef *ecs.TaskDefinition) []
return nil return nil
} }
func uniqueStrings(items []string) []string { func networkResourceName(network string) string {
keys := make(map[string]bool) return fmt.Sprintf("%sNetwork", normalizeResourceName(network))
unique := []string{} }
for _, item := range items {
if _, val := keys[item]; !val { func serviceResourceName(service string) string {
keys[item] = true return fmt.Sprintf("%sService", normalizeResourceName(service))
unique = append(unique, item) }
}
} func normalizeResourceName(s string) string {
return unique return strings.Title(regexp.MustCompile("[^a-zA-Z0-9]+").ReplaceAllString(s, ""))
} }

View File

@ -170,13 +170,13 @@ networks:
back-tier: back-tier:
internal: true internal: true
`) `)
assert.Check(t, template.Resources["TestPublicNetwork"] != nil) assert.Check(t, template.Resources["FronttierNetwork"] != nil)
assert.Check(t, template.Resources["TestBacktierNetwork"] != nil) assert.Check(t, template.Resources["BacktierNetwork"] != nil)
assert.Check(t, template.Resources["TestBacktierNetworkIngress"] != nil) assert.Check(t, template.Resources["BacktierNetworkIngress"] != nil)
i := template.Resources["TestPublicNetworkIngress"] i := template.Resources["FronttierNetworkIngress"]
assert.Check(t, i != nil) assert.Check(t, i != nil)
ingress := *i.(*ec2.SecurityGroupIngress) 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 image: nginx
ports: ports:
- 80:80 - 80:80
`,
`services:
test:
image: nginx
ports:
- target: 8080
protocol: http
`, `,
`services: `services:
test: test:
@ -205,7 +198,7 @@ func TestLoadBalancerTypeApplication(t *testing.T) {
} }
for _, y := range cases { for _, y := range cases {
template := convertYaml(t, y) template := convertYaml(t, y)
lb := template.Resources["TestLoadBalancer"] lb := template.Resources["LoadBalancer"]
assert.Check(t, lb != nil) assert.Check(t, lb != nil)
loadBalancer := *lb.(*elasticloadbalancingv2.LoadBalancer) loadBalancer := *lb.(*elasticloadbalancingv2.LoadBalancer)
assert.Check(t, len(loadBalancer.Name) <= 32) assert.Check(t, len(loadBalancer.Name) <= 32)
@ -328,7 +321,7 @@ services:
memory: 2043248M memory: 2043248M
`) `)
backend := &ecsAPIService{} backend := &ecsAPIService{}
_, _, err := backend.convert(model) _, err := backend.convert(model, awsResources{})
assert.ErrorContains(t, err, "the resources requested are not supported by ECS/Fargate") assert.ErrorContains(t, err, "the resources requested are not supported by ECS/Fargate")
} }
@ -341,7 +334,7 @@ services:
- 80:80 - 80:80
- 88:88 - 88:88
`) `)
lb := template.Resources["TestLoadBalancer"] lb := template.Resources["LoadBalancer"]
assert.Check(t, lb != nil) assert.Check(t, lb != nil)
loadBalancer := *lb.(*elasticloadbalancingv2.LoadBalancer) loadBalancer := *lb.(*elasticloadbalancingv2.LoadBalancer)
assert.Check(t, loadBalancer.Type == elbv2.LoadBalancerTypeEnumNetwork) assert.Check(t, loadBalancer.Type == elbv2.LoadBalancerTypeEnumNetwork)
@ -412,7 +405,10 @@ services:
func convertResultAsString(t *testing.T, project *types.Project) string { func convertResultAsString(t *testing.T, project *types.Project) string {
backend := &ecsAPIService{} backend := &ecsAPIService{}
template, _, err := backend.convert(project) template, err := backend.convert(project, awsResources{
vpc: "vpcID",
subnets: []string{"subnet1", "subnet2"},
})
assert.NilError(t, err) assert.NilError(t, err)
resultAsJSON, err := marshall(template) resultAsJSON, err := marshall(template)
assert.NilError(t, err) assert.NilError(t, err)
@ -432,7 +428,7 @@ func load(t *testing.T, paths ...string) *types.Project {
func convertYaml(t *testing.T, yaml string) *cloudformation.Template { func convertYaml(t *testing.T, yaml string) *cloudformation.Template {
project := loadConfig(t, yaml) project := loadConfig(t, yaml)
backend := &ecsAPIService{} backend := &ecsAPIService{}
template, _, err := backend.convert(project) template, err := backend.convert(project, awsResources{})
assert.NilError(t, err) assert.NilError(t, err)
return template return template
} }

View File

@ -17,10 +17,33 @@
package ecs package ecs
import ( import (
"fmt"
"github.com/compose-spec/compose-go/compatibility" "github.com/compose-spec/compose-go/compatibility"
"github.com/compose-spec/compose-go/errdefs"
"github.com/compose-spec/compose-go/types" "github.com/compose-spec/compose-go/types"
"github.com/sirupsen/logrus"
) )
func (b *ecsAPIService) checkCompatibility(project *types.Project) error {
var checker compatibility.Checker = &fargateCompatibilityChecker{
compatibility.AllowList{
Supported: compatibleComposeAttributes,
},
}
compatibility.Check(project, checker)
for _, err := range checker.Errors() {
if errdefs.IsIncompatibleError(err) {
return err
}
logrus.Warn(err.Error())
}
if !compatibility.IsCompatible(checker) {
return fmt.Errorf("compose file is incompatible with Amazon ECS")
}
return nil
}
type fargateCompatibilityChecker struct { type fargateCompatibilityChecker struct {
compatibility.AllowList compatibility.AllowList
} }

View File

@ -27,6 +27,7 @@ import (
"time" "time"
"github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/aws"
ecsapi "github.com/aws/aws-sdk-go/service/ecs" ecsapi "github.com/aws/aws-sdk-go/service/ecs"
"github.com/awslabs/goformation/v4/cloudformation" "github.com/awslabs/goformation/v4/cloudformation"
"github.com/awslabs/goformation/v4/cloudformation/ecs" "github.com/awslabs/goformation/v4/cloudformation/ecs"
@ -39,7 +40,7 @@ import (
const secretsInitContainerImage = "docker/ecs-secrets-sidecar" 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) cpu, mem, err := toLimits(service)
if err != nil { if err != nil {
return nil, err return nil, err
@ -532,11 +533,8 @@ func toHostEntryPtr(hosts types.HostsList) []ecs.TaskDefinition_HostEntry {
} }
func getRepoCredentials(service types.ServiceConfig) *ecs.TaskDefinition_RepositoryCredentials { func getRepoCredentials(service types.ServiceConfig) *ecs.TaskDefinition_RepositoryCredentials {
// extract registry and namespace string from image name if value, ok := service.Extensions[extensionPullCredentials]; ok {
for key, value := range service.Extensions { return &ecs.TaskDefinition_RepositoryCredentials{CredentialsParameter: value.(string)}
if key == extensionPullCredentials {
return &ecs.TaskDefinition_RepositoryCredentials{CredentialsParameter: value.(string)}
}
} }
return nil return nil
} }

View File

@ -28,7 +28,7 @@ import (
"github.com/compose-spec/compose-go/types" "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, resources awsResources) error {
var ec2 bool var ec2 bool
for _, s := range project.Services { for _, s := range project.Services {
if requireEC2(s) { if requireEC2(s) {
@ -51,11 +51,6 @@ func (b *ecsAPIService) createCapacityProvider(ctx context.Context, project *typ
return err return err
} }
var securityGroups []string
for _, r := range networks {
securityGroups = append(securityGroups, r)
}
template.Resources["CapacityProvider"] = &ecs.CapacityProvider{ template.Resources["CapacityProvider"] = &ecs.CapacityProvider{
AutoScalingGroupProvider: &ecs.CapacityProvider_AutoScalingGroupProvider{ AutoScalingGroupProvider: &ecs.CapacityProvider_AutoScalingGroupProvider{
AutoScalingGroupArn: cloudformation.Ref("AutoscalingGroup"), AutoScalingGroupArn: cloudformation.Ref("AutoscalingGroup"),
@ -63,36 +58,29 @@ func (b *ecsAPIService) createCapacityProvider(ctx context.Context, project *typ
TargetCapacity: 100, TargetCapacity: 100,
}, },
}, },
Tags: projectTags(project), Tags: projectTags(project),
AWSCloudFormationCondition: "CreateCluster",
} }
template.Resources["AutoscalingGroup"] = &autoscaling.AutoScalingGroup{ template.Resources["AutoscalingGroup"] = &autoscaling.AutoScalingGroup{
LaunchConfigurationName: cloudformation.Ref("LaunchConfiguration"), LaunchConfigurationName: cloudformation.Ref("LaunchConfiguration"),
MaxSize: "10", //TODO MaxSize: "10", //TODO
MinSize: "1", MinSize: "1",
VPCZoneIdentifier: []string{ VPCZoneIdentifier: resources.subnets,
cloudformation.Ref(parameterSubnet1Id),
cloudformation.Ref(parameterSubnet2Id),
},
AWSCloudFormationCondition: "CreateCluster",
} }
userData := base64.StdEncoding.EncodeToString([]byte( userData := base64.StdEncoding.EncodeToString([]byte(
fmt.Sprintf("#!/bin/bash\necho ECS_CLUSTER=%s >> /etc/ecs/ecs.config", project.Name))) fmt.Sprintf("#!/bin/bash\necho ECS_CLUSTER=%s >> /etc/ecs/ecs.config", project.Name)))
template.Resources["LaunchConfiguration"] = &autoscaling.LaunchConfiguration{ template.Resources["LaunchConfiguration"] = &autoscaling.LaunchConfiguration{
ImageId: ami, ImageId: ami,
InstanceType: machineType, InstanceType: machineType,
SecurityGroups: securityGroups, SecurityGroups: resources.allSecurityGroups(),
IamInstanceProfile: cloudformation.Ref("EC2InstanceProfile"), IamInstanceProfile: cloudformation.Ref("EC2InstanceProfile"),
UserData: userData, UserData: userData,
AWSCloudFormationCondition: "CreateCluster",
} }
template.Resources["EC2InstanceProfile"] = &iam.InstanceProfile{ template.Resources["EC2InstanceProfile"] = &iam.InstanceProfile{
Roles: []string{cloudformation.Ref("EC2InstanceRole")}, Roles: []string{cloudformation.Ref("EC2InstanceRole")},
AWSCloudFormationCondition: "CreateCluster",
} }
template.Resources["EC2InstanceRole"] = &iam.Role{ template.Resources["EC2InstanceRole"] = &iam.Role{
@ -100,8 +88,7 @@ func (b *ecsAPIService) createCapacityProvider(ctx context.Context, project *typ
ManagedPolicyArns: []string{ ManagedPolicyArns: []string{
ecsEC2InstanceRole, ecsEC2InstanceRole,
}, },
Tags: projectTags(project), Tags: projectTags(project),
AWSCloudFormationCondition: "CreateCluster",
} }
cluster := template.Resources["Cluster"].(*ecs.Cluster) cluster := template.Resources["Cluster"].(*ecs.Cluster)

View File

@ -25,18 +25,15 @@ import (
) )
func (b *ecsAPIService) Ps(ctx context.Context, project string) ([]compose.ServiceStatus, error) { 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) resources, err := b.SDK.ListStackResources(ctx, project)
if err != nil { if err != nil {
return nil, err return nil, err
} }
servicesARN := []string{} var (
cluster = project
servicesARN []string
)
for _, r := range resources { for _, r := range resources {
switch r.Type { switch r.Type {
case "AWS::ECS::Service": case "AWS::ECS::Service":
@ -45,6 +42,7 @@ func (b *ecsAPIService) Ps(ctx context.Context, project string) ([]compose.Servi
cluster = r.ARN cluster = r.ARN
} }
} }
if len(servicesARN) == 0 { if len(servicesARN) == 0 {
return nil, nil return nil, nil
} }

View File

@ -196,22 +196,13 @@ func (s sdk) StackExists(ctx context.Context, name string) (bool, error) {
return len(stacks.Stacks) > 0, nil 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") 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{ _, err := s.CF.CreateStackWithContext(ctx, &cloudformation.CreateStackInput{
OnFailure: aws.String("DELETE"), OnFailure: aws.String("DELETE"),
StackName: aws.String(name), StackName: aws.String(name),
TemplateBody: aws.String(string(template)), TemplateBody: aws.String(string(template)),
Parameters: param,
TimeoutInMinutes: nil, TimeoutInMinutes: nil,
Capabilities: []*string{ Capabilities: []*string{
aws.String(cloudformation.CapabilityCapabilityIam), aws.String(cloudformation.CapabilityCapabilityIam),
@ -226,24 +217,15 @@ func (s sdk) CreateStack(ctx context.Context, name string, template []byte, para
return err 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") 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")) update := fmt.Sprintf("Update%s", time.Now().Format("2006-01-02-15-04-05"))
changeset, err := s.CF.CreateChangeSetWithContext(ctx, &cloudformation.CreateChangeSetInput{ changeset, err := s.CF.CreateChangeSetWithContext(ctx, &cloudformation.CreateChangeSetInput{
ChangeSetName: aws.String(update), ChangeSetName: aws.String(update),
ChangeSetType: aws.String(cloudformation.ChangeSetTypeUpdate), ChangeSetType: aws.String(cloudformation.ChangeSetTypeUpdate),
StackName: aws.String(name), StackName: aws.String(name),
TemplateBody: aws.String(string(template)), TemplateBody: aws.String(string(template)),
Parameters: param,
Capabilities: []*string{ Capabilities: []*string{
aws.String(cloudformation.CapabilityCapabilityIam), aws.String(cloudformation.CapabilityCapabilityIam),
}, },
@ -647,15 +629,18 @@ func (s sdk) GetPublicIPs(ctx context.Context, interfaces ...string) (map[string
return publicIPs, nil return publicIPs, nil
} }
func (s sdk) LoadBalancerExists(ctx context.Context, arn string) (bool, error) { func (s sdk) LoadBalancerType(ctx context.Context, arn string) (string, error) {
logrus.Debug("CheckRequirements if PortPublisher exists: ", arn) logrus.Debug("Check if LoadBalancer exists: ", arn)
lbs, err := s.ELB.DescribeLoadBalancersWithContext(ctx, &elbv2.DescribeLoadBalancersInput{ lbs, err := s.ELB.DescribeLoadBalancersWithContext(ctx, &elbv2.DescribeLoadBalancersInput{
LoadBalancerArns: []*string{aws.String(arn)}, LoadBalancerArns: []*string{aws.String(arn)},
}) })
if err != nil { 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) { func (s sdk) GetLoadBalancerURL(ctx context.Context, arn string) (string, error) {
@ -719,3 +704,13 @@ func (s sdk) GetParameter(ctx context.Context, name string) (string, error) {
return ami.ImageID, nil return ami.ImageID, nil
} }
func (s sdk) SecurityGroupExists(ctx context.Context, sg string) (bool, error) {
desc, err := s.EC2.DescribeSecurityGroupsWithContext(ctx, &ec2.DescribeSecurityGroupsInput{
GroupIds: aws.StringSlice([]string{sg}),
})
if err != nil {
return false, err
}
return len(desc.SecurityGroups) > 0, nil
}

View File

@ -1,59 +1,15 @@
{ {
"AWSTemplateFormatVersion": "2010-09-09", "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": { "Resources": {
"CloudMap": { "CloudMap": {
"Properties": { "Properties": {
"Description": "Service Map for Docker Compose project TestSimpleConvert", "Description": "Service Map for Docker Compose project TestSimpleConvert",
"Name": "TestSimpleConvert.local", "Name": "TestSimpleConvert.local",
"Vpc": { "Vpc": "vpcID"
"Ref": "ParameterVPCId"
}
}, },
"Type": "AWS::ServiceDiscovery::PrivateDnsNamespace" "Type": "AWS::ServiceDiscovery::PrivateDnsNamespace"
}, },
"Cluster": { "Cluster": {
"Condition": "CreateCluster",
"Properties": { "Properties": {
"ClusterName": "TestSimpleConvert", "ClusterName": "TestSimpleConvert",
"Tags": [ "Tags": [
@ -65,6 +21,72 @@
}, },
"Type": "AWS::ECS::Cluster" "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": { "LogGroup": {
"Properties": { "Properties": {
"LogGroupName": "/docker-compose/TestSimpleConvert" "LogGroupName": "/docker-compose/TestSimpleConvert"
@ -77,15 +99,7 @@
], ],
"Properties": { "Properties": {
"Cluster": { "Cluster": {
"Fn::If": [ "Ref": "Cluster"
"CreateCluster",
{
"Ref": "Cluster"
},
{
"Ref": "ParameterClusterName"
}
]
}, },
"DeploymentConfiguration": { "DeploymentConfiguration": {
"MaximumPercent": 200, "MaximumPercent": 200,
@ -110,16 +124,12 @@
"AssignPublicIp": "ENABLED", "AssignPublicIp": "ENABLED",
"SecurityGroups": [ "SecurityGroups": [
{ {
"Ref": "TestSimpleConvertDefaultNetwork" "Ref": "DefaultNetwork"
} }
], ],
"Subnets": [ "Subnets": [
{ "subnet1",
"Ref": "ParameterSubnet1Id" "subnet2"
},
{
"Ref": "ParameterSubnet2Id"
}
] ]
} }
}, },
@ -191,15 +201,7 @@
} }
], ],
"LoadBalancerArn": { "LoadBalancerArn": {
"Fn::If": [ "Ref": "LoadBalancer"
"CreateLoadBalancer",
{
"Ref": "TestSimpleConvertLoadBalancer"
},
{
"Ref": "ParameterLoadBalancerARN"
}
]
}, },
"Port": 80, "Port": 80,
"Protocol": "HTTP" "Protocol": "HTTP"
@ -217,9 +219,7 @@
} }
], ],
"TargetType": "ip", "TargetType": "ip",
"VpcId": { "VpcId": "vpcID"
"Ref": "ParameterVPCId"
}
}, },
"Type": "AWS::ElasticLoadBalancingV2::TargetGroup" "Type": "AWS::ElasticLoadBalancingV2::TargetGroup"
}, },
@ -304,76 +304,6 @@
] ]
}, },
"Type": "AWS::IAM::Role" "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"
} }
} }
} }

View File

@ -32,42 +32,11 @@ func (b *ecsAPIService) Up(ctx context.Context, project *types.Project) error {
return err return err
} }
cluster, err := b.GetCluster(ctx, project)
if err != nil {
return err
}
template, err := b.Convert(ctx, project) template, err := b.Convert(ctx, project)
if err != nil { if err != nil {
return err 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) update, err := b.SDK.StackExists(ctx, project.Name)
if err != nil { if err != nil {
return err return err
@ -75,7 +44,7 @@ func (b *ecsAPIService) Up(ctx context.Context, project *types.Project) error {
operation := stackCreate operation := stackCreate
if update { if update {
operation = stackUpdate operation = stackUpdate
changeset, err := b.SDK.CreateChangeSet(ctx, project.Name, template, parameters) changeset, err := b.SDK.CreateChangeSet(ctx, project.Name, template)
if err != nil { if err != nil {
return err return err
} }
@ -84,7 +53,7 @@ func (b *ecsAPIService) Up(ctx context.Context, project *types.Project) error {
return err return err
} }
} else { } else {
err = b.SDK.CreateStack(ctx, project.Name, template, parameters) err = b.SDK.CreateStack(ctx, project.Name, template)
if err != nil { if err != nil {
return err return err
} }
@ -101,55 +70,3 @@ func (b *ecsAPIService) Up(ctx context.Context, project *types.Project) error {
err = b.WaitStackCompletion(ctx, project.Name, operation) err = b.WaitStackCompletion(ctx, project.Name, operation)
return err 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
}

59
ecs/volumes.go Normal file
View File

@ -0,0 +1,59 @@
/*
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 (
"fmt"
"github.com/awslabs/goformation/v4/cloudformation"
"github.com/awslabs/goformation/v4/cloudformation/ec2"
"github.com/awslabs/goformation/v4/cloudformation/ecs"
"github.com/compose-spec/compose-go/types"
)
func (b *ecsAPIService) createNFSmountIngress(securityGroups []string, project *types.Project, n string, template *cloudformation.Template) error {
target := securityGroups[0]
for _, s := range project.Services {
for _, v := range s.Volumes {
if v.Source != n {
continue
}
var source string
for net := range s.Networks {
network := project.Networks[net]
if ext, ok := network.Extensions[extensionSecurityGroup]; ok {
source = ext.(string)
} else {
source = networkResourceName(net)
}
break
}
name := fmt.Sprintf("%sNFSMount%s", s.Name, n)
template.Resources[name] = &ec2.SecurityGroupIngress{
Description: fmt.Sprintf("Allow NFS mount for %s on %s", s.Name, n),
GroupId: target,
SourceSecurityGroupId: cloudformation.Ref(source),
IpProtocol: "tcp",
FromPort: 2049,
ToPort: 2049,
}
service := template.Resources[serviceResourceName(s.Name)].(*ecs.Service)
service.AWSCloudFormationDependsOn = append(service.AWSCloudFormationDependsOn, name)
}
}
return nil
}

View File

@ -20,7 +20,7 @@ const (
extensionSecurityGroup = "x-aws-securitygroup" extensionSecurityGroup = "x-aws-securitygroup"
extensionVPC = "x-aws-vpc" extensionVPC = "x-aws-vpc"
extensionPullCredentials = "x-aws-pull_credentials" extensionPullCredentials = "x-aws-pull_credentials"
extensionLB = "x-aws-loadbalancer" extensionLoadBalancer = "x-aws-loadbalancer"
extensionProtocol = "x-aws-protocol" extensionProtocol = "x-aws-protocol"
extensionCluster = "x-aws-cluster" extensionCluster = "x-aws-cluster"
extensionKeys = "x-aws-keys" extensionKeys = "x-aws-keys"