From 10b8fabaaefcd978f88ddf6dfb80523929fee708 Mon Sep 17 00:00:00 2001
From: Nicolas De Loof <nicolas.deloof@gmail.com>
Date: Tue, 29 Sep 2020 15:11:31 +0200
Subject: [PATCH] Allow user to set ami/machine by Deploy.Placement.Constraint

Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com>
---
 ecs/backend.go             | 14 +++++------
 ecs/cloudformation_test.go |  6 ++---
 ecs/compatibility.go       |  2 ++
 ecs/ec2.go                 | 50 +++++++++++++++++++++++++++++++-------
 ecs/ec2_test.go            | 49 +++++++++++++++++++++++++++++++++++++
 5 files changed, 101 insertions(+), 20 deletions(-)
 create mode 100644 ecs/ec2_test.go

diff --git a/ecs/backend.go b/ecs/backend.go
index ab5b3ddec..7e015db95 100644
--- a/ecs/backend.go
+++ b/ecs/backend.go
@@ -88,23 +88,23 @@ type ecsAPIService struct {
 	aws    API
 }
 
-func (a *ecsAPIService) ContainerService() containers.Service {
+func (b *ecsAPIService) ContainerService() containers.Service {
 	return nil
 }
 
-func (a *ecsAPIService) ComposeService() compose.Service {
-	return a
+func (b *ecsAPIService) ComposeService() compose.Service {
+	return b
 }
 
-func (a *ecsAPIService) SecretsService() secrets.Service {
-	return a
+func (b *ecsAPIService) SecretsService() secrets.Service {
+	return b
 }
 
-func (a *ecsAPIService) VolumeService() volumes.Service {
+func (b *ecsAPIService) VolumeService() volumes.Service {
 	return nil
 }
 
-func (a *ecsAPIService) ResourceService() resources.Service {
+func (b *ecsAPIService) ResourceService() resources.Service {
 	return nil
 }
 
diff --git a/ecs/cloudformation_test.go b/ecs/cloudformation_test.go
index 06176822c..ff060292c 100644
--- a/ecs/cloudformation_test.go
+++ b/ecs/cloudformation_test.go
@@ -23,21 +23,19 @@ import (
 	"reflect"
 	"testing"
 
-	"github.com/awslabs/goformation/v4/cloudformation/efs"
-
-	"github.com/golang/mock/gomock"
-
 	"github.com/docker/compose-cli/api/compose"
 
 	"github.com/aws/aws-sdk-go/service/elbv2"
 	"github.com/awslabs/goformation/v4/cloudformation"
 	"github.com/awslabs/goformation/v4/cloudformation/ec2"
 	"github.com/awslabs/goformation/v4/cloudformation/ecs"
+	"github.com/awslabs/goformation/v4/cloudformation/efs"
 	"github.com/awslabs/goformation/v4/cloudformation/elasticloadbalancingv2"
 	"github.com/awslabs/goformation/v4/cloudformation/iam"
 	"github.com/awslabs/goformation/v4/cloudformation/logs"
 	"github.com/compose-spec/compose-go/loader"
 	"github.com/compose-spec/compose-go/types"
+	"github.com/golang/mock/gomock"
 	"gotest.tools/v3/assert"
 	"gotest.tools/v3/golden"
 )
diff --git a/ecs/compatibility.go b/ecs/compatibility.go
index 4c42fc434..eb663a6ce 100644
--- a/ecs/compatibility.go
+++ b/ecs/compatibility.go
@@ -54,6 +54,8 @@ var compatibleComposeAttributes = []string{
 	"services.cap_drop",
 	"services.depends_on",
 	"services.deploy",
+	"services.deploy.placement",
+	"services.deploy.placement.constraints",
 	"services.deploy.replicas",
 	"services.deploy.resources.limits",
 	"services.deploy.resources.limits.cpus",
diff --git a/ecs/ec2.go b/ecs/ec2.go
index fae76ed51..7438598ae 100644
--- a/ecs/ec2.go
+++ b/ecs/ec2.go
@@ -20,6 +20,7 @@ import (
 	"context"
 	"encoding/base64"
 	"fmt"
+	"strings"
 
 	"github.com/awslabs/goformation/v4/cloudformation"
 	"github.com/awslabs/goformation/v4/cloudformation/autoscaling"
@@ -28,11 +29,22 @@ import (
 	"github.com/compose-spec/compose-go/types"
 )
 
+const (
+	placementConstraintAMI     = "node.ami == "
+	placementConstraintMachine = "node.machine == "
+)
+
 func (b *ecsAPIService) createCapacityProvider(ctx context.Context, project *types.Project, template *cloudformation.Template, resources awsResources) error {
-	var ec2 bool
-	for _, s := range project.Services {
-		if requireEC2(s) {
+	var (
+		ec2         bool
+		ami         string
+		machineType string
+	)
+	for _, service := range project.Services {
+		if requireEC2(service) {
 			ec2 = true
+			// TODO once we can assign a service to a CapacityProvider, we could run this _per service_
+			ami, machineType = getUserDefinedMachine(service)
 			break
 		}
 	}
@@ -41,14 +53,20 @@ func (b *ecsAPIService) createCapacityProvider(ctx context.Context, project *typ
 		return nil
 	}
 
-	ami, err := b.aws.GetParameter(ctx, "/aws/service/ecs/optimized-ami/amazon-linux-2/gpu/recommended")
-	if err != nil {
-		return err
+	if ami == "" {
+		recommended, err := b.aws.GetParameter(ctx, "/aws/service/ecs/optimized-ami/amazon-linux-2/gpu/recommended")
+		if err != nil {
+			return err
+		}
+		ami = recommended
 	}
 
-	machineType, err := guessMachineType(project)
-	if err != nil {
-		return err
+	if machineType == "" {
+		t, err := guessMachineType(project)
+		if err != nil {
+			return err
+		}
+		machineType = t
 	}
 
 	template.Resources["CapacityProvider"] = &ecs.CapacityProvider{
@@ -98,3 +116,17 @@ func (b *ecsAPIService) createCapacityProvider(ctx context.Context, project *typ
 
 	return nil
 }
+
+func getUserDefinedMachine(s types.ServiceConfig) (ami string, machineType string) {
+	if s.Deploy != nil {
+		for _, s := range s.Deploy.Placement.Constraints {
+			if strings.HasPrefix(s, placementConstraintAMI) {
+				ami = s[len(placementConstraintAMI):]
+			}
+			if strings.HasPrefix(s, placementConstraintMachine) {
+				machineType = s[len(placementConstraintMachine):]
+			}
+		}
+	}
+	return ami, machineType
+}
diff --git a/ecs/ec2_test.go b/ecs/ec2_test.go
new file mode 100644
index 000000000..db476c73d
--- /dev/null
+++ b/ecs/ec2_test.go
@@ -0,0 +1,49 @@
+/*
+   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 (
+	"testing"
+
+	"github.com/awslabs/goformation/v4/cloudformation/autoscaling"
+	"gotest.tools/v3/assert"
+)
+
+func TestUserDefinedAMI(t *testing.T) {
+	template := convertYaml(t, `
+services:
+  test:
+    image: "image"
+    deploy:
+      placement:
+        constraints:
+          - "node.ami == ami123456789"
+          - "node.machine == t0.femto"
+      resources:
+        # devices:
+        #   - capabilities: ["gpu"]
+        reservations:
+          memory: 8Gb
+          generic_resources:
+            - discrete_resource_spec:
+                kind: gpus
+                value: 1                    
+`, useDefaultVPC)
+	lc := template.Resources["LaunchConfiguration"].(*autoscaling.LaunchConfiguration)
+	assert.Check(t, lc.ImageId == "ami123456789")
+	assert.Check(t, lc.InstanceType == "t0.femto")
+}