`parse` to return awsResources then convert into CF template

Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com>
This commit is contained in:
Nicolas De Loof 2020-09-29 08:31:11 +02:00
parent 7034254911
commit d5e0ec7aa6
No known key found for this signature in database
GPG Key ID: 9858809D6F8F6E7E
6 changed files with 116 additions and 102 deletions

View File

@ -32,7 +32,6 @@ import (
// awsResources hold the AWS component being used or created to support services definition // awsResources hold the AWS component being used or created to support services definition
type awsResources struct { type awsResources struct {
sdk sdk
vpc string vpc string
subnets []string subnets []string
cluster string cluster string
@ -58,101 +57,120 @@ func (r *awsResources) allSecurityGroups() []string {
} }
// parse look into compose project for configured resource to use, and check they are valid // 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 { func (b *ecsAPIService) parse(ctx context.Context, project *types.Project) (awsResources, error) {
return findProjectFnError(ctx, project, r := awsResources{}
r.parseClusterExtension, var err error
r.parseVPCExtension, r.cluster, err = b.parseClusterExtension(ctx, project)
r.parseLoadBalancerExtension, if err != nil {
r.parseSecurityGroupExtension, 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 (r *awsResources) parseClusterExtension(ctx context.Context, project *types.Project) error { func (b *ecsAPIService) parseClusterExtension(ctx context.Context, project *types.Project) (string, error) {
if x, ok := project.Extensions[extensionCluster]; ok { if x, ok := project.Extensions[extensionCluster]; ok {
cluster := x.(string) cluster := x.(string)
ok, err := r.sdk.ClusterExists(ctx, cluster) ok, err := b.SDK.ClusterExists(ctx, cluster)
if err != nil { if err != nil {
return err return "", err
} }
if !ok { if !ok {
return fmt.Errorf("cluster does not exist: %s", cluster) return "", fmt.Errorf("cluster does not exist: %s", cluster)
} }
r.cluster = cluster return cluster, nil
} }
return nil return "", nil
} }
func (r *awsResources) parseVPCExtension(ctx context.Context, project *types.Project) error { func (b *ecsAPIService) parseVPCExtension(ctx context.Context, project *types.Project) (string, []string, error) {
var vpc string
if x, ok := project.Extensions[extensionVPC]; ok { if x, ok := project.Extensions[extensionVPC]; ok {
vpc := x.(string) vpc = x.(string)
err := r.sdk.CheckVPC(ctx, vpc) err := b.SDK.CheckVPC(ctx, vpc)
if err != nil { if err != nil {
return err return "", nil, 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) } else {
defaultVPC, err := b.SDK.GetDefaultVPC(ctx)
if err != nil { if err != nil {
return err return "", nil, err
}
vpc = defaultVPC
}
subNets, err := b.SDK.GetSubNets(ctx, vpc)
if err != nil {
return "", nil, err
} }
if len(subNets) < 2 { if len(subNets) < 2 {
return fmt.Errorf("VPC %s should have at least 2 associated subnets in different availability zones", r.vpc) return "", nil, fmt.Errorf("VPC %s should have at least 2 associated subnets in different availability zones", vpc)
} }
r.subnets = subNets return vpc, subNets, nil
return nil
} }
func (r *awsResources) parseLoadBalancerExtension(ctx context.Context, project *types.Project) error { func (b *ecsAPIService) parseLoadBalancerExtension(ctx context.Context, project *types.Project) (string, string, error) {
if x, ok := project.Extensions[extensionLoadBalancer]; ok { if x, ok := project.Extensions[extensionLoadBalancer]; ok {
loadBalancer := x.(string) loadBalancer := x.(string)
loadBalancerType, err := r.sdk.LoadBalancerType(ctx, loadBalancer) loadBalancerType, err := b.SDK.LoadBalancerType(ctx, loadBalancer)
if err != nil { if err != nil {
return err return "", "", err
} }
required := getRequiredLoadBalancerType(project) required := getRequiredLoadBalancerType(project)
if loadBalancerType != required { if loadBalancerType != required {
return fmt.Errorf("load balancer %s is of type %s, project require a %s", loadBalancer, loadBalancerType, required) return "", "", fmt.Errorf("load balancer %s is of type %s, project require a %s", loadBalancer, loadBalancerType, required)
} }
r.loadBalancer = loadBalancer return loadBalancer, loadBalancerType, nil
r.loadBalancerType = loadBalancerType
} }
return nil return "", "", nil
} }
func (r *awsResources) parseSecurityGroupExtension(ctx context.Context, project *types.Project) error { func (b *ecsAPIService) parseSecurityGroupExtension(ctx context.Context, project *types.Project) (map[string]string, error) {
if r.securityGroups == nil { securityGroups := make(map[string]string, len(project.Networks))
r.securityGroups = make(map[string]string, len(project.Networks))
}
for name, net := range project.Networks { for name, net := range project.Networks {
var sg string
if net.External.External { if net.External.External {
r.securityGroups[name] = net.Name sg = net.Name
} }
if x, ok := net.Extensions[extensionSecurityGroup]; ok { 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.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) logrus.Debugf("Security Group for network %q set by user to %q", net.Name, x)
r.securityGroups[name] = x.(string) sg = x.(string)
} }
exists, err := b.SDK.SecurityGroupExists(ctx, sg)
if err != nil {
return nil, err
} }
return nil if !exists {
return nil, fmt.Errorf("security group %s doesn't exist", sg)
}
securityGroups[name] = sg
}
return securityGroups, nil
} }
// ensure all required resources pre-exists or are defined as cloudformation resources // ensureResources create required resources in template if not yet defined
func (r *awsResources) ensure(project *types.Project, template *cloudformation.Template) { func (b *ecsAPIService) ensureResources(resources *awsResources, project *types.Project, template *cloudformation.Template) {
r.ensureCluster(project, template) b.ensureCluster(resources, project, template)
r.ensureNetworks(project, template) b.ensureNetworks(resources, project, template)
r.ensureLoadBalancer(project, template) b.ensureLoadBalancer(resources, project, template)
} }
func (r *awsResources) ensureCluster(project *types.Project, template *cloudformation.Template) { func (b *ecsAPIService) ensureCluster(r *awsResources, project *types.Project, template *cloudformation.Template) {
if r.cluster != "" { if r.cluster != "" {
return return
} }
@ -163,7 +181,7 @@ func (r *awsResources) ensureCluster(project *types.Project, template *cloudform
r.cluster = cloudformation.Ref("Cluster") r.cluster = cloudformation.Ref("Cluster")
} }
func (r *awsResources) ensureNetworks(project *types.Project, template *cloudformation.Template) { func (b *ecsAPIService) ensureNetworks(r *awsResources, project *types.Project, template *cloudformation.Template) {
if r.securityGroups == nil { if r.securityGroups == nil {
r.securityGroups = make(map[string]string, len(project.Networks)) r.securityGroups = make(map[string]string, len(project.Networks))
} }
@ -179,7 +197,7 @@ func (r *awsResources) ensureNetworks(project *types.Project, template *cloudfor
ingress := securityGroup + "Ingress" ingress := securityGroup + "Ingress"
template.Resources[ingress] = &ec2.SecurityGroupIngress{ template.Resources[ingress] = &ec2.SecurityGroupIngress{
Description: fmt.Sprintf("Allow communication within network %s", name), Description: fmt.Sprintf("Allow communication within network %s", name),
IpProtocol: "-1", // all protocols IpProtocol: allProtocols,
GroupId: cloudformation.Ref(securityGroup), GroupId: cloudformation.Ref(securityGroup),
SourceSecurityGroupId: cloudformation.Ref(securityGroup), SourceSecurityGroupId: cloudformation.Ref(securityGroup),
} }
@ -188,7 +206,7 @@ func (r *awsResources) ensureNetworks(project *types.Project, template *cloudfor
} }
} }
func (r *awsResources) ensureLoadBalancer(project *types.Project, template *cloudformation.Template) { func (b *ecsAPIService) ensureLoadBalancer(r *awsResources, project *types.Project, template *cloudformation.Template) {
if r.loadBalancer != "" { if r.loadBalancer != "" {
return return
} }
@ -239,18 +257,6 @@ func portIsHTTP(it types.ServicePortConfig) bool {
return it.Target == 80 || it.Target == 443 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] // predicate[types.ServiceConfig]
type servicePredicate func(it types.ServiceConfig) bool type servicePredicate func(it types.ServiceConfig) bool

View File

@ -78,7 +78,6 @@ func getEcsAPIService(ecsCtx store.EcsContext) (*ecsAPIService, error) {
ctx: ecsCtx, ctx: ecsCtx,
Region: ecsCtx.Region, Region: ecsCtx.Region,
SDK: sdk, SDK: sdk,
resources: awsResources{sdk: sdk},
}, nil }, nil
} }
@ -86,7 +85,6 @@ type ecsAPIService struct {
ctx store.EcsContext ctx store.EcsContext
Region string Region string
SDK sdk SDK sdk
resources awsResources
} }
func (a *ecsAPIService) ContainerService() containers.Service { func (a *ecsAPIService) ContainerService() containers.Service {

View File

@ -43,12 +43,12 @@ func (b *ecsAPIService) Convert(ctx context.Context, project *types.Project) ([]
return nil, err return nil, err
} }
err = b.resources.parse(ctx, project) resources, err := b.parse(ctx, project)
if err != nil { if err != nil {
return nil, err return nil, err
} }
template, err := b.convert(project) template, err := b.convert(project, resources)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -64,7 +64,7 @@ func (b *ecsAPIService) Convert(ctx context.Context, project *types.Project) ([]
} }
} }
err = b.createCapacityProvider(ctx, project, template) err = b.createCapacityProvider(ctx, project, template, resources)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -73,9 +73,9 @@ 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, error) { //nolint:gocyclo func (b *ecsAPIService) convert(project *types.Project, resources awsResources) (*cloudformation.Template, error) {
template := cloudformation.NewTemplate() template := cloudformation.NewTemplate()
b.resources.ensure(project, template) b.ensureResources(&resources, project, template)
for name, secret := range project.Secrets { for name, secret := range project.Secrets {
err := b.createSecret(project, name, secret, template) err := b.createSecret(project, name, secret, template)
@ -87,7 +87,7 @@ func (b *ecsAPIService) convert(project *types.Project) (*cloudformation.Templat
b.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
b.createCloudMap(project, template) b.createCloudMap(project, template, resources.vpc)
for _, service := range project.Services { for _, service := range project.Services {
taskExecutionRole := b.createTaskExecutionRole(project, service, template) taskExecutionRole := b.createTaskExecutionRole(project, service, template)
@ -114,16 +114,16 @@ func (b *ecsAPIService) convert(project *types.Project) (*cloudformation.Templat
) )
for _, port := range service.Ports { for _, port := range service.Ports {
for net := range service.Networks { for net := range service.Networks {
b.createIngress(service, net, port, template) b.createIngress(service, net, port, template, resources)
} }
protocol := strings.ToUpper(port.Protocol) protocol := strings.ToUpper(port.Protocol)
if b.resources.loadBalancerType == elbv2.LoadBalancerTypeEnumApplication { if resources.loadBalancerType == elbv2.LoadBalancerTypeEnumApplication {
// we don't set Https as a certificate must be specified for HTTPS listeners // we don't set Https as a certificate must be specified for HTTPS listeners
protocol = elbv2.ProtocolEnumHttp protocol = elbv2.ProtocolEnumHttp
} }
targetGroupName := b.createTargetGroup(project, service, port, template, protocol) targetGroupName := b.createTargetGroup(project, service, port, template, protocol, resources.vpc)
listenerName := b.createListener(service, port, template, targetGroupName, b.resources.loadBalancer, protocol) listenerName := b.createListener(service, port, template, targetGroupName, resources.loadBalancer, protocol)
dependsOn = append(dependsOn, listenerName) dependsOn = append(dependsOn, listenerName)
serviceLB = append(serviceLB, ecs.Service_LoadBalancer{ serviceLB = append(serviceLB, ecs.Service_LoadBalancer{
ContainerName: service.Name, ContainerName: service.Name,
@ -157,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: b.resources.cluster, Cluster: resources.cluster,
DesiredCount: desiredCount, DesiredCount: desiredCount,
DeploymentController: &ecs.Service_DeploymentController{ DeploymentController: &ecs.Service_DeploymentController{
Type: ecsapi.DeploymentControllerTypeEcs, Type: ecsapi.DeploymentControllerTypeEcs,
@ -172,8 +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: b.resources.serviceSecurityGroups(service), SecurityGroups: resources.serviceSecurityGroups(service),
Subnets: b.resources.subnets, Subnets: resources.subnets,
}, },
}, },
PlatformVersion: platformVersion, PlatformVersion: platformVersion,
@ -187,16 +187,18 @@ func (b *ecsAPIService) convert(project *types.Project) (*cloudformation.Templat
return template, nil return template, nil
} }
func (b *ecsAPIService) createIngress(service types.ServiceConfig, net string, port types.ServicePortConfig, 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) protocol := strings.ToUpper(port.Protocol)
if protocol == "" { if protocol == "" {
protocol = "-1" protocol = allProtocols
} }
ingress := fmt.Sprintf("%s%dIngress", normalizeResourceName(net), port.Target) ingress := fmt.Sprintf("%s%dIngress", normalizeResourceName(net), port.Target)
template.Resources[ingress] = &ec2.SecurityGroupIngress{ template.Resources[ingress] = &ec2.SecurityGroupIngress{
CidrIp: "0.0.0.0/0", CidrIp: "0.0.0.0/0",
Description: fmt.Sprintf("%s:%d/%s on %s nextwork", service.Name, port.Target, port.Protocol, net), Description: fmt.Sprintf("%s:%d/%s on %s nextwork", service.Name, port.Target, port.Protocol, net),
GroupId: b.resources.securityGroups[net], GroupId: resources.securityGroups[net],
FromPort: int(port.Target), FromPort: int(port.Target),
IpProtocol: protocol, IpProtocol: protocol,
ToPort: int(port.Target), ToPort: int(port.Target),
@ -306,7 +308,7 @@ func (b *ecsAPIService) createListener(service types.ServiceConfig, port types.S
return listenerName return listenerName
} }
func (b *ecsAPIService) 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),
@ -319,7 +321,7 @@ func (b *ecsAPIService) createTargetGroup(project *types.Project, service types.
Protocol: protocol, Protocol: protocol,
Tags: projectTags(project), Tags: projectTags(project),
TargetType: elbv2.TargetTypeEnumIp, TargetType: elbv2.TargetTypeEnumIp,
VpcId: b.resources.vpc, VpcId: vpc,
} }
return targetGroupName return targetGroupName
} }
@ -390,11 +392,11 @@ func (b *ecsAPIService) createTaskRole(service types.ServiceConfig, template *cl
return taskRole return taskRole
} }
func (b *ecsAPIService) createCloudMap(project *types.Project, template *cloudformation.Template) { func (b *ecsAPIService) createCloudMap(project *types.Project, template *cloudformation.Template, vpc string) {
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: b.resources.vpc, Vpc: vpc,
} }
} }

View File

@ -321,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")
} }
@ -404,13 +404,11 @@ services:
} }
func convertResultAsString(t *testing.T, project *types.Project) string { func convertResultAsString(t *testing.T, project *types.Project) string {
backend := &ecsAPIService{ backend := &ecsAPIService{}
resources: awsResources{ template, err := backend.convert(project, awsResources{
vpc: "vpcID", vpc: "vpcID",
subnets: []string{"subnet1", "subnet2"}, subnets: []string{"subnet1", "subnet2"},
}, })
}
template, err := backend.convert(project)
assert.NilError(t, err) assert.NilError(t, err)
resultAsJSON, err := marshall(template) resultAsJSON, err := marshall(template)
assert.NilError(t, err) assert.NilError(t, err)
@ -430,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

@ -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, 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) {
@ -65,7 +65,7 @@ func (b *ecsAPIService) createCapacityProvider(ctx context.Context, project *typ
LaunchConfigurationName: cloudformation.Ref("LaunchConfiguration"), LaunchConfigurationName: cloudformation.Ref("LaunchConfiguration"),
MaxSize: "10", //TODO MaxSize: "10", //TODO
MinSize: "1", MinSize: "1",
VPCZoneIdentifier: b.resources.subnets, VPCZoneIdentifier: resources.subnets,
} }
userData := base64.StdEncoding.EncodeToString([]byte( userData := base64.StdEncoding.EncodeToString([]byte(
@ -74,7 +74,7 @@ func (b *ecsAPIService) createCapacityProvider(ctx context.Context, project *typ
template.Resources["LaunchConfiguration"] = &autoscaling.LaunchConfiguration{ template.Resources["LaunchConfiguration"] = &autoscaling.LaunchConfiguration{
ImageId: ami, ImageId: ami,
InstanceType: machineType, InstanceType: machineType,
SecurityGroups: b.resources.allSecurityGroups(), SecurityGroups: resources.allSecurityGroups(),
IamInstanceProfile: cloudformation.Ref("EC2InstanceProfile"), IamInstanceProfile: cloudformation.Ref("EC2InstanceProfile"),
UserData: userData, UserData: userData,
} }

View File

@ -704,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
}