diff --git a/aci/compose.go b/aci/compose.go index f40818a19..d8792fe32 100644 --- a/aci/compose.go +++ b/aci/compose.go @@ -129,6 +129,6 @@ func (cs *aciComposeService) Logs(ctx context.Context, project string, w io.Writ return errdefs.ErrNotImplemented } -func (cs *aciComposeService) Convert(ctx context.Context, project *types.Project) ([]byte, error) { +func (cs *aciComposeService) Convert(ctx context.Context, project *types.Project, format string) ([]byte, error) { return nil, errdefs.ErrNotImplemented } diff --git a/api/client/compose.go b/api/client/compose.go index 05e2e106d..33014ec6f 100644 --- a/api/client/compose.go +++ b/api/client/compose.go @@ -55,6 +55,6 @@ func (c *composeService) List(context.Context, string) ([]compose.Stack, error) } // Convert translate compose model into backend's native format -func (c *composeService) Convert(context.Context, *types.Project) ([]byte, error) { +func (c *composeService) Convert(context.Context, *types.Project, string) ([]byte, error) { return nil, errdefs.ErrNotImplemented } diff --git a/api/compose/api.go b/api/compose/api.go index 0d4c49839..22ddb9d1f 100644 --- a/api/compose/api.go +++ b/api/compose/api.go @@ -36,7 +36,7 @@ type Service interface { // List executes the equivalent to a `docker stack ls` List(ctx context.Context, projectName string) ([]Stack, error) // Convert translate compose model into backend's native format - Convert(ctx context.Context, project *types.Project) ([]byte, error) + Convert(ctx context.Context, project *types.Project, format string) ([]byte, error) } // PortPublisher hold status about published port diff --git a/cli/cmd/compose/convert.go b/cli/cmd/compose/convert.go index 224256d7a..aa9d62ac0 100644 --- a/cli/cmd/compose/convert.go +++ b/cli/cmd/compose/convert.go @@ -39,6 +39,7 @@ func convertCommand() *cobra.Command { convertCmd.Flags().StringVar(&opts.WorkingDir, "workdir", "", "Work dir") convertCmd.Flags().StringArrayVarP(&opts.ConfigPaths, "file", "f", []string{}, "Compose configuration files") convertCmd.Flags().StringArrayVarP(&opts.Environment, "environment", "e", []string{}, "Environment variables") + convertCmd.Flags().StringVar(&opts.Format, "format", "yaml", "Format the output. Values: [yaml | json]") return convertCmd } @@ -60,7 +61,7 @@ func runConvert(ctx context.Context, opts composeOptions) error { return err } - json, err = c.ComposeService().Convert(ctx, project) + json, err = c.ComposeService().Convert(ctx, project, opts.Format) if err != nil { return err } diff --git a/ecs/cloudformation.go b/ecs/cloudformation.go index 2fded8700..a28c48f9a 100644 --- a/ecs/cloudformation.go +++ b/ecs/cloudformation.go @@ -37,13 +37,13 @@ import ( "github.com/compose-spec/compose-go/types" ) -func (b *ecsAPIService) Convert(ctx context.Context, project *types.Project) ([]byte, error) { +func (b *ecsAPIService) Convert(ctx context.Context, project *types.Project, format string) ([]byte, error) { template, err := b.convert(ctx, project) if err != nil { return nil, err } - return marshall(template) + return marshall(template, format) } func (b *ecsAPIService) convert(ctx context.Context, project *types.Project) (*cloudformation.Template, error) { diff --git a/ecs/cloudformation_test.go b/ecs/cloudformation_test.go index 920f7fcf3..c8d92c943 100644 --- a/ecs/cloudformation_test.go +++ b/ecs/cloudformation_test.go @@ -44,7 +44,7 @@ func TestSimpleConvert(t *testing.T) { bytes, err := ioutil.ReadFile("testdata/input/simple-single-service.yaml") assert.NilError(t, err) template := convertYaml(t, string(bytes), useDefaultVPC) - resultAsJSON, err := marshall(template) + resultAsJSON, err := marshall(template, "yaml") assert.NilError(t, err) result := fmt.Sprintf("%s\n", string(resultAsJSON)) expected := "simple/simple-cloudformation-conversion.golden" diff --git a/ecs/local/compose.go b/ecs/local/compose.go index c6abbbb56..148f9f08f 100644 --- a/ecs/local/compose.go +++ b/ecs/local/compose.go @@ -57,7 +57,7 @@ func (e ecsLocalSimulation) Up(ctx context.Context, project *types.Project, deta return fmt.Errorf("ECS simulation mode require Docker-compose 1.27, found %s", version) } - converted, err := e.Convert(ctx, project) + converted, err := e.Convert(ctx, project, "yaml") if err != nil { return err } @@ -69,7 +69,7 @@ func (e ecsLocalSimulation) Up(ctx context.Context, project *types.Project, deta return cmd.Run() } -func (e ecsLocalSimulation) Convert(ctx context.Context, project *types.Project) ([]byte, error) { +func (e ecsLocalSimulation) Convert(ctx context.Context, project *types.Project, format string) ([]byte, error) { project.Networks["credentials_network"] = types.NetworkConfig{ Driver: "bridge", Ipam: types.IPAMConfig{ diff --git a/ecs/marshall.go b/ecs/marshall.go index f37654e95..2a70b040a 100644 --- a/ecs/marshall.go +++ b/ecs/marshall.go @@ -22,29 +22,50 @@ import ( "strings" "github.com/awslabs/goformation/v4/cloudformation" + "github.com/sanathkr/go-yaml" ) -func marshall(template *cloudformation.Template) ([]byte, error) { - raw, err := template.JSON() +func marshall(template *cloudformation.Template, format string) ([]byte, error) { + var ( + source func() ([]byte, error) + marshal func(in interface{}) ([]byte, error) + unmarshal func(in []byte, out interface{}) error + ) + switch format { + case "yaml": + source = template.YAML + marshal = yaml.Marshal + unmarshal = yaml.Unmarshal + case "json": + source = template.JSON + marshal = func(in interface{}) ([]byte, error) { + return json.MarshalIndent(in, "", " ") + } + unmarshal = json.Unmarshal + default: + return nil, fmt.Errorf("unsupported format %q", format) + } + + raw, err := source() if err != nil { return nil, err } var unmarshalled interface{} - if err := json.Unmarshal(raw, &unmarshalled); err != nil { + if err := unmarshal(raw, &unmarshalled); err != nil { return nil, fmt.Errorf("invalid JSON: %s", err) } - if input, ok := unmarshalled.(map[string]interface{}); ok { + if input, ok := unmarshalled.(map[interface{}]interface{}); ok { if resources, ok := input["Resources"]; ok { - for _, uresource := range resources.(map[string]interface{}) { - if resource, ok := uresource.(map[string]interface{}); ok { + for _, uresource := range resources.(map[interface{}]interface{}) { + if resource, ok := uresource.(map[interface{}]interface{}); ok { if resource["Type"] == "AWS::ECS::TaskDefinition" { - properties := resource["Properties"].(map[string]interface{}) + properties := resource["Properties"].(map[interface{}]interface{}) for _, def := range properties["ContainerDefinitions"].([]interface{}) { - containerDefinition := def.(map[string]interface{}) + containerDefinition := def.(map[interface{}]interface{}) if strings.HasSuffix(containerDefinition["Name"].(string), "_InitContainer") { - containerDefinition["Essential"] = "false" + containerDefinition["Essential"] = false } } } @@ -53,9 +74,5 @@ func marshall(template *cloudformation.Template) ([]byte, error) { } } - raw, err = json.MarshalIndent(unmarshalled, "", " ") - if err != nil { - return nil, fmt.Errorf("invalid JSON: %s", err) - } - return raw, err + return marshal(unmarshalled) } diff --git a/ecs/testdata/simple/simple-cloudformation-conversion.golden b/ecs/testdata/simple/simple-cloudformation-conversion.golden index d064cdd81..6882bf795 100644 --- a/ecs/testdata/simple/simple-cloudformation-conversion.golden +++ b/ecs/testdata/simple/simple-cloudformation-conversion.golden @@ -1,332 +1,208 @@ -{ - "AWSTemplateFormatVersion": "2010-09-09", - "Resources": { - "CloudMap": { - "Properties": { - "Description": "Service Map for Docker Compose project TestSimpleConvert", - "Name": "TestSimpleConvert.local", - "Vpc": "vpc-123" - }, - "Type": "AWS::ServiceDiscovery::PrivateDnsNamespace" - }, - "Cluster": { - "Properties": { - "ClusterName": "TestSimpleConvert", - "Tags": [ - { - "Key": "com.docker.compose.project", - "Value": "TestSimpleConvert" - } - ] - }, - "Type": "AWS::ECS::Cluster" - }, - "Default80Ingress": { - "Properties": { - "CidrIp": "0.0.0.0/0", - "Description": "simple:80/tcp on default network", - "FromPort": 80, - "GroupId": { - "Ref": "DefaultNetwork" - }, - "IpProtocol": "TCP", - "ToPort": 80 - }, - "Type": "AWS::EC2::SecurityGroupIngress" - }, - "DefaultNetwork": { - "Properties": { - "GroupDescription": "TestSimpleConvert Security Group for default network", - "Tags": [ - { - "Key": "com.docker.compose.project", - "Value": "TestSimpleConvert" - }, - { - "Key": "com.docker.compose.network", - "Value": "default" - } - ], - "VpcId": "vpc-123" - }, - "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" - }, - "Type": "AWS::Logs::LogGroup" - }, - "SimpleService": { - "DependsOn": [ - "SimpleTCP80Listener" - ], - "Properties": { - "Cluster": { - "Fn::GetAtt": [ - "Cluster", - "Arn" - ] - }, - "DeploymentConfiguration": { - "MaximumPercent": 200, - "MinimumHealthyPercent": 100 - }, - "DeploymentController": { - "Type": "ECS" - }, - "DesiredCount": 1, - "LaunchType": "FARGATE", - "LoadBalancers": [ - { - "ContainerName": "simple", - "ContainerPort": 80, - "TargetGroupArn": { - "Ref": "SimpleTCP80TargetGroup" - } - } - ], - "NetworkConfiguration": { - "AwsvpcConfiguration": { - "AssignPublicIp": "ENABLED", - "SecurityGroups": [ - { - "Ref": "DefaultNetwork" - } - ], - "Subnets": [ - "subnet1", - "subnet2" - ] - } - }, - "PlatformVersion": "1.4.0", - "PropagateTags": "SERVICE", - "SchedulingStrategy": "REPLICA", - "ServiceRegistries": [ - { - "RegistryArn": { - "Fn::GetAtt": [ - "SimpleServiceDiscoveryEntry", - "Arn" - ] - } - } - ], - "Tags": [ - { - "Key": "com.docker.compose.project", - "Value": "TestSimpleConvert" - }, - { - "Key": "com.docker.compose.service", - "Value": "simple" - } - ], - "TaskDefinition": { - "Ref": "SimpleTaskDefinition" - } - }, - "Type": "AWS::ECS::Service" - }, - "SimpleServiceDiscoveryEntry": { - "Properties": { - "Description": "\"simple\" service discovery entry in Cloud Map", - "DnsConfig": { - "DnsRecords": [ - { - "TTL": 60, - "Type": "A" - } - ], - "RoutingPolicy": "MULTIVALUE" - }, - "HealthCheckCustomConfig": { - "FailureThreshold": 1 - }, - "Name": "simple", - "NamespaceId": { - "Ref": "CloudMap" - } - }, - "Type": "AWS::ServiceDiscovery::Service" - }, - "SimpleTCP80Listener": { - "Properties": { - "DefaultActions": [ - { - "ForwardConfig": { - "TargetGroups": [ - { - "TargetGroupArn": { - "Ref": "SimpleTCP80TargetGroup" - } - } - ] - }, - "Type": "forward" - } - ], - "LoadBalancerArn": { - "Ref": "LoadBalancer" - }, - "Port": 80, - "Protocol": "HTTP" - }, - "Type": "AWS::ElasticLoadBalancingV2::Listener" - }, - "SimpleTCP80TargetGroup": { - "Properties": { - "Port": 80, - "Protocol": "HTTP", - "Tags": [ - { - "Key": "com.docker.compose.project", - "Value": "TestSimpleConvert" - } - ], - "TargetType": "ip", - "VpcId": "vpc-123" - }, - "Type": "AWS::ElasticLoadBalancingV2::TargetGroup" - }, - "SimpleTaskDefinition": { - "Properties": { - "ContainerDefinitions": [ - { - "Command": [ - ".compute.internal", - "TestSimpleConvert.local" - ], - "Essential": "false", - "Image": "docker/ecs-searchdomain-sidecar:1.0", - "LogConfiguration": { - "LogDriver": "awslogs", - "Options": { - "awslogs-group": { - "Ref": "LogGroup" - }, - "awslogs-region": { - "Ref": "AWS::Region" - }, - "awslogs-stream-prefix": "TestSimpleConvert" - } - }, - "Name": "Simple_ResolvConf_InitContainer" - }, - { - "DependsOn": [ - { - "Condition": "SUCCESS", - "ContainerName": "Simple_ResolvConf_InitContainer" - } - ], - "Essential": true, - "Image": "nginx", - "LinuxParameters": {}, - "LogConfiguration": { - "LogDriver": "awslogs", - "Options": { - "awslogs-group": { - "Ref": "LogGroup" - }, - "awslogs-region": { - "Ref": "AWS::Region" - }, - "awslogs-stream-prefix": "TestSimpleConvert" - } - }, - "Name": "simple", - "PortMappings": [ - { - "ContainerPort": 80, - "HostPort": 80, - "Protocol": "tcp" - } - ] - } - ], - "Cpu": "256", - "ExecutionRoleArn": { - "Ref": "SimpleTaskExecutionRole" - }, - "Family": "TestSimpleConvert-simple", - "Memory": "512", - "NetworkMode": "awsvpc", - "RequiresCompatibilities": [ - "FARGATE" - ] - }, - "Type": "AWS::ECS::TaskDefinition" - }, - "SimpleTaskExecutionRole": { - "Properties": { - "AssumeRolePolicyDocument": { - "Statement": [ - { - "Action": [ - "sts:AssumeRole" - ], - "Condition": {}, - "Effect": "Allow", - "Principal": { - "Service": "ecs-tasks.amazonaws.com" - } - } - ], - "Version": "2012-10-17" - }, - "ManagedPolicyArns": [ - "arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy", - "arn:aws:iam::aws:policy/AmazonEC2ContainerRegistryReadOnly" - ], - "Tags": [ - { - "Key": "com.docker.compose.project", - "Value": "TestSimpleConvert" - }, - { - "Key": "com.docker.compose.service", - "Value": "simple" - } - ] - }, - "Type": "AWS::IAM::Role" - } - } -} +AWSTemplateFormatVersion: 2010-09-09 +Resources: + CloudMap: + Properties: + Description: Service Map for Docker Compose project TestSimpleConvert + Name: TestSimpleConvert.local + Vpc: vpc-123 + Type: AWS::ServiceDiscovery::PrivateDnsNamespace + Cluster: + Properties: + ClusterName: TestSimpleConvert + Tags: + - Key: com.docker.compose.project + Value: TestSimpleConvert + Type: AWS::ECS::Cluster + Default80Ingress: + Properties: + CidrIp: 0.0.0.0/0 + Description: simple:80/tcp on default network + FromPort: 80 + GroupId: + Ref: DefaultNetwork + IpProtocol: TCP + ToPort: 80 + Type: AWS::EC2::SecurityGroupIngress + DefaultNetwork: + Properties: + GroupDescription: TestSimpleConvert Security Group for default network + Tags: + - Key: com.docker.compose.project + Value: TestSimpleConvert + - Key: com.docker.compose.network + Value: default + VpcId: vpc-123 + 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 + Type: AWS::Logs::LogGroup + SimpleService: + DependsOn: + - SimpleTCP80Listener + Properties: + Cluster: + Fn::GetAtt: + - Cluster + - Arn + DeploymentConfiguration: + MaximumPercent: 200 + MinimumHealthyPercent: 100 + DeploymentController: + Type: ECS + DesiredCount: 1 + LaunchType: FARGATE + LoadBalancers: + - ContainerName: simple + ContainerPort: 80 + TargetGroupArn: + Ref: SimpleTCP80TargetGroup + NetworkConfiguration: + AwsvpcConfiguration: + AssignPublicIp: ENABLED + SecurityGroups: + - Ref: DefaultNetwork + Subnets: + - subnet1 + - subnet2 + PlatformVersion: 1.4.0 + PropagateTags: SERVICE + SchedulingStrategy: REPLICA + ServiceRegistries: + - RegistryArn: + Fn::GetAtt: + - SimpleServiceDiscoveryEntry + - Arn + Tags: + - Key: com.docker.compose.project + Value: TestSimpleConvert + - Key: com.docker.compose.service + Value: simple + TaskDefinition: + Ref: SimpleTaskDefinition + Type: AWS::ECS::Service + SimpleServiceDiscoveryEntry: + Properties: + Description: '"simple" service discovery entry in Cloud Map' + DnsConfig: + DnsRecords: + - TTL: 60 + Type: A + RoutingPolicy: MULTIVALUE + HealthCheckCustomConfig: + FailureThreshold: 1 + Name: simple + NamespaceId: + Ref: CloudMap + Type: AWS::ServiceDiscovery::Service + SimpleTCP80Listener: + Properties: + DefaultActions: + - ForwardConfig: + TargetGroups: + - TargetGroupArn: + Ref: SimpleTCP80TargetGroup + Type: forward + LoadBalancerArn: + Ref: LoadBalancer + Port: 80 + Protocol: HTTP + Type: AWS::ElasticLoadBalancingV2::Listener + SimpleTCP80TargetGroup: + Properties: + Port: 80 + Protocol: HTTP + Tags: + - Key: com.docker.compose.project + Value: TestSimpleConvert + TargetType: ip + VpcId: vpc-123 + Type: AWS::ElasticLoadBalancingV2::TargetGroup + SimpleTaskDefinition: + Properties: + ContainerDefinitions: + - Command: + - .compute.internal + - TestSimpleConvert.local + Essential: false + Image: docker/ecs-searchdomain-sidecar:1.0 + LogConfiguration: + LogDriver: awslogs + Options: + awslogs-group: + Ref: LogGroup + awslogs-region: + Ref: AWS::Region + awslogs-stream-prefix: TestSimpleConvert + Name: Simple_ResolvConf_InitContainer + - DependsOn: + - Condition: SUCCESS + ContainerName: Simple_ResolvConf_InitContainer + Essential: true + Image: nginx + LinuxParameters: {} + LogConfiguration: + LogDriver: awslogs + Options: + awslogs-group: + Ref: LogGroup + awslogs-region: + Ref: AWS::Region + awslogs-stream-prefix: TestSimpleConvert + Name: simple + PortMappings: + - ContainerPort: 80 + HostPort: 80 + Protocol: tcp + Cpu: "256" + ExecutionRoleArn: + Ref: SimpleTaskExecutionRole + Family: TestSimpleConvert-simple + Memory: "512" + NetworkMode: awsvpc + RequiresCompatibilities: + - FARGATE + Type: AWS::ECS::TaskDefinition + SimpleTaskExecutionRole: + Properties: + AssumeRolePolicyDocument: + Statement: + - Action: + - sts:AssumeRole + Condition: {} + Effect: Allow + Principal: + Service: ecs-tasks.amazonaws.com + Version: 2012-10-17 + ManagedPolicyArns: + - arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy + - arn:aws:iam::aws:policy/AmazonEC2ContainerRegistryReadOnly + Tags: + - Key: com.docker.compose.project + Value: TestSimpleConvert + - Key: com.docker.compose.service + Value: simple + Type: AWS::IAM::Role + diff --git a/ecs/up.go b/ecs/up.go index b3065ba7b..308683e8a 100644 --- a/ecs/up.go +++ b/ecs/up.go @@ -32,7 +32,7 @@ func (b *ecsAPIService) Up(ctx context.Context, project *types.Project, detach b return err } - template, err := b.Convert(ctx, project) + template, err := b.Convert(ctx, project, "yaml") if err != nil { return err } diff --git a/example/backend.go b/example/backend.go index 8e8f1ede7..a02ed9d0a 100644 --- a/example/backend.go +++ b/example/backend.go @@ -158,6 +158,6 @@ func (cs *composeService) Logs(ctx context.Context, project string, w io.Writer) return errdefs.ErrNotImplemented } -func (cs *composeService) Convert(ctx context.Context, project *types.Project) ([]byte, error) { +func (cs *composeService) Convert(ctx context.Context, project *types.Project, format string) ([]byte, error) { return nil, errdefs.ErrNotImplemented }