зеркало из https://github.com/docker/compose-cli.git
introduce awsResources to replace CloudFormation parameters
Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com>
This commit is contained in:
Родитель
b30cd649b9
Коммит
7034254911
|
@ -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
|
||||
}
|
|
@ -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 {
|
||||
|
|
|
@ -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<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)
|
||||
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 <service>.<project>.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, ""))
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
33
ecs/ec2.go
33
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)
|
||||
|
|
12
ecs/ps.go
12
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
|
||||
}
|
||||
|
|
33
ecs/sdk.go
33
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) {
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
87
ecs/up.go
87
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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
2
ecs/x.go
2
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"
|
||||
|
|
Загрузка…
Ссылка в новой задаче