diff --git a/clouddriver-ecs/clouddriver-ecs.gradle b/clouddriver-ecs/clouddriver-ecs.gradle index e7303fd27fe..c9ac4733d17 100644 --- a/clouddriver-ecs/clouddriver-ecs.gradle +++ b/clouddriver-ecs/clouddriver-ecs.gradle @@ -1,5 +1,6 @@ dependencies { implementation project(":cats:cats-core") + implementation project(":clouddriver-artifacts") implementation project(":clouddriver-aws") implementation project(":clouddriver-core") implementation project(":clouddriver-security") @@ -15,6 +16,7 @@ dependencies { implementation "com.netflix.spectator:spectator-api" implementation "com.netflix.spinnaker.fiat:fiat-api:$fiatVersion" implementation "com.netflix.spinnaker.fiat:fiat-core:$fiatVersion" + implementation "com.netflix.spinnaker.kork:kork-artifacts" implementation "com.netflix.spinnaker.kork:kork-exceptions" implementation "com.squareup.okhttp:okhttp" implementation "com.squareup.okhttp:okhttp-apache" diff --git a/clouddriver-ecs/src/main/java/com/netflix/spinnaker/clouddriver/ecs/deploy/description/CreateServerGroupDescription.java b/clouddriver-ecs/src/main/java/com/netflix/spinnaker/clouddriver/ecs/deploy/description/CreateServerGroupDescription.java index f8746b90e11..4f4f003a7f5 100644 --- a/clouddriver-ecs/src/main/java/com/netflix/spinnaker/clouddriver/ecs/deploy/description/CreateServerGroupDescription.java +++ b/clouddriver-ecs/src/main/java/com/netflix/spinnaker/clouddriver/ecs/deploy/description/CreateServerGroupDescription.java @@ -19,8 +19,10 @@ import com.amazonaws.services.ecs.model.PlacementConstraint; import com.amazonaws.services.ecs.model.PlacementStrategy; import com.netflix.spinnaker.clouddriver.model.ServerGroup; +import com.netflix.spinnaker.kork.artifacts.model.Artifact; import java.util.List; import java.util.Map; +import javax.annotation.Nullable; import lombok.Data; import lombok.EqualsAndHashCode; @@ -35,13 +37,13 @@ public class CreateServerGroupDescription extends AbstractECSDescription { String portProtocol; - Integer computeUnits; - Integer reservedMemory; + @Nullable Integer computeUnits; + @Nullable Integer reservedMemory; Map environmentVariables; Map tags; - String dockerImageAddress; + @Nullable String dockerImageAddress; String dockerImageCredentialsSecret; ServerGroup.Capacity capacity; @@ -67,6 +69,12 @@ public class CreateServerGroupDescription extends AbstractECSDescription { List serviceDiscoveryAssociations; + boolean useTaskDefinitionArtifact; + Artifact resolvedTaskDefinitionArtifact; + String taskDefinitionArtifactAccount; + Map containerToImageMap; + String loadBalancedContainer; + @Override public String getRegion() { // CreateServerGroupDescription does not contain a region. Instead it has AvailabilityZones diff --git a/clouddriver-ecs/src/main/java/com/netflix/spinnaker/clouddriver/ecs/deploy/ops/CreateServerGroupAtomicOperation.java b/clouddriver-ecs/src/main/java/com/netflix/spinnaker/clouddriver/ecs/deploy/ops/CreateServerGroupAtomicOperation.java index f73b6653d75..eb14e1afdb0 100644 --- a/clouddriver-ecs/src/main/java/com/netflix/spinnaker/clouddriver/ecs/deploy/ops/CreateServerGroupAtomicOperation.java +++ b/clouddriver-ecs/src/main/java/com/netflix/spinnaker/clouddriver/ecs/deploy/ops/CreateServerGroupAtomicOperation.java @@ -28,6 +28,8 @@ import com.amazonaws.services.identitymanagement.model.GetRoleRequest; import com.amazonaws.services.identitymanagement.model.GetRoleResult; import com.amazonaws.services.identitymanagement.model.Role; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.netflix.spinnaker.clouddriver.artifacts.ArtifactDownloader; import com.netflix.spinnaker.clouddriver.aws.security.AmazonCredentials; import com.netflix.spinnaker.clouddriver.aws.security.AssumeRoleAmazonCredentials; import com.netflix.spinnaker.clouddriver.aws.security.NetflixAmazonCredentials; @@ -42,8 +44,11 @@ import com.netflix.spinnaker.clouddriver.ecs.services.SecurityGroupSelector; import com.netflix.spinnaker.clouddriver.ecs.services.SubnetSelector; import com.netflix.spinnaker.clouddriver.helpers.OperationPoller; +import com.netflix.spinnaker.kork.artifacts.model.Artifact; +import java.io.*; import java.util.*; import java.util.stream.Collectors; +import org.apache.commons.io.IOUtils; import org.apache.commons.lang3.StringUtils; import org.springframework.beans.factory.annotation.Autowired; @@ -68,6 +73,10 @@ public class CreateServerGroupAtomicOperation @Autowired SecurityGroupSelector securityGroupSelector; + @Autowired ArtifactDownloader artifactDownloader; + + @Autowired ObjectMapper mapper; + public CreateServerGroupAtomicOperation(CreateServerGroupDescription description) { super(description, "CREATE_ECS_SERVER_GROUP"); } @@ -123,8 +132,14 @@ public DeploymentResult operate(List priorOutputs) { protected TaskDefinition registerTaskDefinition( AmazonECS ecs, String ecsServiceRole, String newServerGroupName) { - RegisterTaskDefinitionRequest request = - makeTaskDefinitionRequest(ecsServiceRole, newServerGroupName); + + RegisterTaskDefinitionRequest request; + + if (description.isUseTaskDefinitionArtifact()) { + request = makeTaskDefinitionRequestFromArtifact(ecsServiceRole, newServerGroupName); + } else { + request = makeTaskDefinitionRequest(ecsServiceRole, newServerGroupName); + } RegisterTaskDefinitionResult registerTaskDefinitionResult = ecs.registerTaskDefinition(request); @@ -144,12 +159,7 @@ protected RegisterTaskDefinitionRequest makeTaskDefinitionRequest( } } - containerEnvironment.add( - new KeyValuePair().withName("SERVER_GROUP").withValue(newServerGroupName)); - containerEnvironment.add( - new KeyValuePair().withName("CLOUD_STACK").withValue(description.getStack())); - containerEnvironment.add( - new KeyValuePair().withName("CLOUD_DETAIL").withValue(description.getFreeFormDetails())); + containerEnvironment = setSpinnakerEnvVars(containerEnvironment, newServerGroupName); ContainerDefinition containerDefinition = new ContainerDefinition() @@ -213,15 +223,7 @@ protected RegisterTaskDefinitionRequest makeTaskDefinitionRequest( labelsMap.putAll(description.getDockerLabels()); } - if (description.getStack() != null) { - labelsMap.put(DOCKER_LABEL_KEY_STACK, description.getStack()); - } - - if (description.getFreeFormDetails() != null) { - labelsMap.put(DOCKER_LABEL_KEY_DETAIL, description.getFreeFormDetails()); - } - - labelsMap.put(DOCKER_LABEL_KEY_SERVERGROUP, newServerGroupName); + labelsMap = setSpinnakerDockerLabels(labelsMap, newServerGroupName); containerDefinition.withDockerLabels(labelsMap); @@ -263,6 +265,106 @@ protected RegisterTaskDefinitionRequest makeTaskDefinitionRequest( return request; } + protected RegisterTaskDefinitionRequest makeTaskDefinitionRequestFromArtifact( + String ecsServiceRole, String newServerGroupName) { + + File artifactFile = + downloadTaskDefinitionArtifact(description.getResolvedTaskDefinitionArtifact()); + + RegisterTaskDefinitionRequest requestTemplate; + try { + requestTemplate = mapper.readValue(artifactFile, RegisterTaskDefinitionRequest.class); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + + String templateMode = requestTemplate.getNetworkMode(); + if (templateMode != null + && !templateMode.isEmpty() + && !templateMode.equals(description.getNetworkMode())) { + throw new IllegalArgumentException( + "Task definition networkMode does not match server group value. Found '" + + templateMode + + "' but expected '" + + description.getNetworkMode() + + "'"); + } + + List containers = requestTemplate.getContainerDefinitions(); + if (containers.size() == 0) { + throw new IllegalArgumentException( + "Provided task definition does not contain any container definitions."); + } + + description + .getContainerToImageMap() + .forEach( + (k, v) -> { + // check if taskDefTemplate contains matching container + List matches = + containers.stream() + .filter(x -> x.getName().equals(k)) + .collect(Collectors.toList()); + + if (matches.size() != 1) { + throw new IllegalArgumentException( + "Invalid number of matching containers found for mapping '" + + k + + "'. Have " + + matches.size() + + " but expected 1."); + } + + // interpolate container mappings + matches.get(0).setImage(v); + }); + + containers.forEach( + (c) -> { + Collection updatedEnv = + setSpinnakerEnvVars(c.getEnvironment(), newServerGroupName); + c.setEnvironment(updatedEnv); + + Map updatedLabels = + setSpinnakerDockerLabels(c.getDockerLabels(), newServerGroupName); + c.setDockerLabels(updatedLabels); + }); + + if (FARGATE_LAUNCH_TYPE.equals(description.getLaunchType())) { + String templateExecutionRole = requestTemplate.getExecutionRoleArn(); + + if (templateExecutionRole == null || templateExecutionRole.isEmpty()) { + requestTemplate.setExecutionRoleArn(ecsServiceRole); + } + } + requestTemplate.setFamily(EcsServerGroupNameResolver.getEcsFamilyName(newServerGroupName)); + + return requestTemplate; + } + + private File downloadTaskDefinitionArtifact(Artifact taskDefArtifact) { + File file = null; + if (taskDefArtifact.getArtifactAccount() == null + || taskDefArtifact.getArtifactAccount().isEmpty() + && description.getTaskDefinitionArtifactAccount() != null + && !description.getTaskDefinitionArtifactAccount().isEmpty()) { + taskDefArtifact.setArtifactAccount(description.getTaskDefinitionArtifactAccount()); + } + try { + InputStream artifactInput = artifactDownloader.download(taskDefArtifact); + file = File.createTempFile(UUID.randomUUID().toString(), null); + FileOutputStream fileOutputStream = new FileOutputStream(file); + IOUtils.copy(artifactInput, fileOutputStream); + fileOutputStream.close(); + } catch (IOException e) { + if (file != null) { + file.delete(); + } + throw new UncheckedIOException(e); + } + return file; + } + private Service createService( AmazonECS ecs, TaskDefinition taskDefinition, @@ -552,7 +654,12 @@ private Collection retrieveLoadBalancers(String containerName) { Collection loadBalancers = new LinkedList<>(); if (description.getTargetGroup() != null && !description.getTargetGroup().isEmpty()) { LoadBalancer loadBalancer = new LoadBalancer(); - loadBalancer.setContainerName(containerName); + String containerToUse = + description.getLoadBalancedContainer() != null + && !description.getLoadBalancedContainer().isEmpty() + ? description.getLoadBalancedContainer() + : containerName; + loadBalancer.setContainerName(containerToUse); loadBalancer.setContainerPort(description.getContainerPort()); AmazonElasticLoadBalancing loadBalancingV2 = getAmazonElasticLoadBalancingClient(); @@ -623,6 +730,38 @@ private String getServerGroupName(Service service) { return getRegion() + ":" + service.getServiceName(); } + private Collection setSpinnakerEnvVars( + Collection targetEnv, String newServerGroupName) { + + targetEnv.add(new KeyValuePair().withName("SERVER_GROUP").withValue(newServerGroupName)); + targetEnv.add(new KeyValuePair().withName("CLOUD_STACK").withValue(description.getStack())); + targetEnv.add( + new KeyValuePair().withName("CLOUD_DETAIL").withValue(description.getFreeFormDetails())); + + return targetEnv; + } + + private Map setSpinnakerDockerLabels( + Map targetMap, String newServerGroupName) { + + Map newLabels = new HashMap<>(); + if (targetMap != null) { + newLabels.putAll(targetMap); + } + + if (description.getStack() != null) { + newLabels.put(DOCKER_LABEL_KEY_STACK, description.getStack()); + } + + if (description.getFreeFormDetails() != null) { + newLabels.put(DOCKER_LABEL_KEY_DETAIL, description.getFreeFormDetails()); + } + + newLabels.put(DOCKER_LABEL_KEY_SERVERGROUP, newServerGroupName); + + return newLabels; + } + @Override protected String getRegion() { // CreateServerGroupDescription does not contain a region. Instead it has AvailabilityZones diff --git a/clouddriver-ecs/src/main/java/com/netflix/spinnaker/clouddriver/ecs/deploy/validators/EcsCreateServerGroupDescriptionValidator.java b/clouddriver-ecs/src/main/java/com/netflix/spinnaker/clouddriver/ecs/deploy/validators/EcsCreateServerGroupDescriptionValidator.java index 2d2c65828d3..4375f44e196 100644 --- a/clouddriver-ecs/src/main/java/com/netflix/spinnaker/clouddriver/ecs/deploy/validators/EcsCreateServerGroupDescriptionValidator.java +++ b/clouddriver-ecs/src/main/java/com/netflix/spinnaker/clouddriver/ecs/deploy/validators/EcsCreateServerGroupDescriptionValidator.java @@ -102,8 +102,26 @@ public void validate(List priorDescriptions, Object description, Errors errors) rejectValue(errors, "ecsClusterName", "not.nullable"); } - if (createServerGroupDescription.getDockerImageAddress() == null) { - rejectValue(errors, "dockerImageAddress", "not.nullable"); + if (!createServerGroupDescription.isUseTaskDefinitionArtifact()) { + if (createServerGroupDescription.getDockerImageAddress() == null) { + rejectValue(errors, "dockerImageAddress", "not.nullable"); + } + + if (createServerGroupDescription.getComputeUnits() != null) { + if (createServerGroupDescription.getComputeUnits() < 0) { + rejectValue(errors, "computeUnits", "invalid"); + } + } else { + rejectValue(errors, "computeUnits", "not.nullable"); + } + + if (createServerGroupDescription.getReservedMemory() != null) { + if (createServerGroupDescription.getReservedMemory() < 0) { + rejectValue(errors, "reservedMemory", "invalid"); + } + } else { + rejectValue(errors, "reservedMemory", "not.nullable"); + } } if (createServerGroupDescription.getContainerPort() != null) { @@ -116,22 +134,6 @@ public void validate(List priorDescriptions, Object description, Errors errors) rejectValue(errors, "containerPort", "not.nullable"); } - if (createServerGroupDescription.getComputeUnits() != null) { - if (createServerGroupDescription.getComputeUnits() < 0) { - rejectValue(errors, "computeUnits", "invalid"); - } - } else { - rejectValue(errors, "computeUnits", "not.nullable"); - } - - if (createServerGroupDescription.getReservedMemory() != null) { - if (createServerGroupDescription.getReservedMemory() < 0) { - rejectValue(errors, "reservedMemory", "invalid"); - } - } else { - rejectValue(errors, "reservedMemory", "not.nullable"); - } - // Verify that the environment variables set by the user do not contain reserved values if (createServerGroupDescription.getEnvironmentVariables() != null) { if (!Collections.disjoint( diff --git a/clouddriver-ecs/src/main/java/com/netflix/spinnaker/clouddriver/ecs/services/ContainerInformationService.java b/clouddriver-ecs/src/main/java/com/netflix/spinnaker/clouddriver/ecs/services/ContainerInformationService.java index 555eec17835..1f3c696129e 100644 --- a/clouddriver-ecs/src/main/java/com/netflix/spinnaker/clouddriver/ecs/services/ContainerInformationService.java +++ b/clouddriver-ecs/src/main/java/com/netflix/spinnaker/clouddriver/ecs/services/ContainerInformationService.java @@ -18,6 +18,7 @@ import com.amazonaws.services.ec2.model.Instance; import com.amazonaws.services.ecs.model.LoadBalancer; +import com.amazonaws.services.ecs.model.NetworkBinding; import com.netflix.spinnaker.clouddriver.ecs.cache.Keys; import com.netflix.spinnaker.clouddriver.ecs.cache.client.ContainerInstanceCacheClient; import com.netflix.spinnaker.clouddriver.ecs.cache.client.EcsInstanceCacheClient; @@ -144,16 +145,17 @@ public String getClusterName(String serviceName, String accountName, String regi return null; } - public String getTaskPrivateAddress(String accountName, String region, Task task) { - if (task.getContainers().size() > 1) { - throw new IllegalArgumentException("Multiple containers for a task is not supported."); - } - + public String getTaskPrivateAddress(String accountName, String region, Task task) { // int hostPort; - try { - hostPort = task.getContainers().get(0).getNetworkBindings().get(0).getHostPort(); - } catch (Exception e) { - hostPort = -1; + + if (task.getContainers().size() > 1) { + hostPort = getAddressHostPortForMultipleContainers(task); + } else { + try { + hostPort = task.getContainers().get(0).getNetworkBindings().get(0).getHostPort(); + } catch (Exception e) { + hostPort = -1; + } } if (hostPort < 0 || hostPort > 65535) { @@ -210,4 +212,26 @@ private String getAwsAccountName(String ecsAccountName) { } return null; } + + private int getAddressHostPortForMultipleContainers(Task task) { + List hostPorts = new ArrayList() {}; + + task.getContainers() + .forEach( + (c) -> { + List networkBindings = c.getNetworkBindings(); + networkBindings.forEach( + (b) -> { + if (b.getHostPort() != null) { + hostPorts.add(b.getHostPort()); + } + }); + }); + + if (hostPorts.size() == 1) { + return hostPorts.get(0); + } + + return -1; + } } diff --git a/clouddriver-ecs/src/test/groovy/com/netflix/spinnaker/clouddriver/ecs/deploy/ops/CreateServerGroupAtomicOperationSpec.groovy b/clouddriver-ecs/src/test/groovy/com/netflix/spinnaker/clouddriver/ecs/deploy/ops/CreateServerGroupAtomicOperationSpec.groovy index 77101d25005..b47763b1786 100644 --- a/clouddriver-ecs/src/test/groovy/com/netflix/spinnaker/clouddriver/ecs/deploy/ops/CreateServerGroupAtomicOperationSpec.groovy +++ b/clouddriver-ecs/src/test/groovy/com/netflix/spinnaker/clouddriver/ecs/deploy/ops/CreateServerGroupAtomicOperationSpec.groovy @@ -25,6 +25,8 @@ import com.amazonaws.services.elasticloadbalancingv2.model.TargetGroup import com.amazonaws.services.identitymanagement.AmazonIdentityManagement import com.amazonaws.services.identitymanagement.model.GetRoleResult import com.amazonaws.services.identitymanagement.model.Role +import com.fasterxml.jackson.databind.ObjectMapper +import com.netflix.spinnaker.clouddriver.artifacts.ArtifactDownloader import com.netflix.spinnaker.clouddriver.aws.security.AmazonCredentials import com.netflix.spinnaker.clouddriver.aws.security.NetflixAssumeRoleAmazonCredentials import com.netflix.spinnaker.clouddriver.ecs.TestCredential @@ -45,6 +47,8 @@ class CreateServerGroupAtomicOperationSpec extends CommonAtomicOperation { def autoScalingClient = Mock(AWSApplicationAutoScaling) def subnetSelector = Mock(SubnetSelector) def securityGroupSelector = Mock(SecurityGroupSelector) + def objectMapper = Mock(ObjectMapper) + def artifactDownloader = Mock(ArtifactDownloader) def applicationName = 'myapp' def stack = 'kcats' @@ -515,6 +519,168 @@ class CreateServerGroupAtomicOperationSpec extends CommonAtomicOperation { environments.get("CLOUD_DETAIL") == "liated" } + def 'should generate a RegisterTaskDefinitionRequest object from artifact'() { + given: + def resolvedArtifact = [ + name: "taskdef.json", + reference: "fake.github.com/repos/org/repo/taskdef.json", + artifactAccount: "my-github-acct", + type: "github/file" + ] + def containerDef1 = + new ContainerDefinition() + .withName("web") + .withImage("PLACEHOLDER") + .withMemoryReservation(512) + def containerDef2 = + new ContainerDefinition() + .withName("logs") + .withImage("PLACEHOLDER") + .withMemoryReservation(1024) + def registerTaskDefRequest = + new RegisterTaskDefinitionRequest() + .withContainerDefinitions([containerDef1, containerDef2]) + .withExecutionRoleArn("arn:aws:role/myExecutionRole") + def description = Mock(CreateServerGroupDescription) + description.getApplication() >> 'v1' + description.getStack() >> 'ecs' + description.getFreeFormDetails() >> 'test' + description.ecsClusterName = 'test-cluster' + description.iamRole = 'None (No IAM role)' + description.getResolvedTaskDefinitionArtifact() >> resolvedArtifact + description.getContainerToImageMap() >> [ + web: "docker-image-url/one", + logs: "docker-image-url/two" + ] + + def operation = new CreateServerGroupAtomicOperation(description) + operation.artifactDownloader = artifactDownloader + operation.mapper = objectMapper + + artifactDownloader.download(_) >> new ByteArrayInputStream() + objectMapper.readValue(_,_) >> registerTaskDefRequest + + when: + RegisterTaskDefinitionRequest result = + operation.makeTaskDefinitionRequestFromArtifact("test-role", "v1-ecs-test-v001") + + then: + result.getTaskRoleArn() == null + result.getFamily() == "v1-ecs-test" + result.getExecutionRoleArn() == "arn:aws:role/myExecutionRole" + + result.getContainerDefinitions().size() == 2 + + def webContainer = result.getContainerDefinitions().find {it.getName() == "web"} + assert webContainer != null + webContainer.image == "docker-image-url/one" + webContainer.memoryReservation == 512 + + def logsContainer = result.getContainerDefinitions().find {it.getName() == "logs"} + assert logsContainer != null + logsContainer.image == "docker-image-url/two" + logsContainer.memoryReservation == 1024 + + result.getContainerDefinitions().forEach({ + it.environment.size() == 3 + + def environments = [:] + for(elem in it.environment){ + environments.put(elem.getName(), elem.getValue()) + } + environments.get("SERVER_GROUP") == "v1-ecs-test-v001" + environments.get("CLOUD_STACK") == "ecs" + environments.get("CLOUD_DETAIL") == "test" + }) + } + + def 'should set spinnaker role on FARGATE RegisterTaskDefinitionRequest if none in artifact'() { + given: + def resolvedArtifact = [ + name: "taskdef.json", + reference: "fake.github.com/repos/org/repo/taskdef.json", + artifactAccount: "my-github-acct", + type: "github/file" + ] + def containerDef = + new ContainerDefinition() + .withName("web") + .withImage("PLACEHOLDER") + .withMemoryReservation(512) + def registerTaskDefRequest = + new RegisterTaskDefinitionRequest().withContainerDefinitions([containerDef]) + def description = Mock(CreateServerGroupDescription) + description.getApplication() >> 'v1' + description.getStack() >> 'ecs' + description.getFreeFormDetails() >> 'test' + description.ecsClusterName = 'test-cluster' + description.iamRole = 'None (No IAM role)' + description.getLaunchType() >> 'FARGATE' + description.getResolvedTaskDefinitionArtifact() >> resolvedArtifact + description.getContainerToImageMap() >> [ + web: "docker-image-url" + ] + + def operation = new CreateServerGroupAtomicOperation(description) + operation.artifactDownloader = artifactDownloader + operation.mapper = objectMapper + + artifactDownloader.download(_) >> new ByteArrayInputStream() + objectMapper.readValue(_,_) >> registerTaskDefRequest + + when: + RegisterTaskDefinitionRequest result = + operation.makeTaskDefinitionRequestFromArtifact("test-role", "v1-ecs-test-v001") + + then: + result.getTaskRoleArn() == null + result.getFamily() == "v1-ecs-test" + result.getExecutionRoleArn() == "test-role" + + result.getContainerDefinitions().size() == 1 + def containerDefinition = result.getContainerDefinitions().first() + containerDefinition.name == "web" + containerDefinition.image == "docker-image-url" + containerDefinition.memoryReservation == 512 + } + + def 'should fail if network mode in artifact does not match description'() { + given: + def resolvedArtifact = [ + name: "taskdef.json", + reference: "fake.github.com/repos/org/repo/taskdef.json", + artifactAccount: "my-github-acct", + type: "github/file" + ] + def registerTaskDefRequest = + new RegisterTaskDefinitionRequest() + .withContainerDefinitions([new ContainerDefinition()]) + .withNetworkMode("bridge") + def description = Mock(CreateServerGroupDescription) + description.getApplication() >> 'v1' + description.getStack() >> 'ecs' + description.getFreeFormDetails() >> 'test' + description.ecsClusterName = 'test-cluster' + description.getLaunchType() >> 'FARGATE' + description.getNetworkMode() >> 'awsvpc' + description.getResolvedTaskDefinitionArtifact() >> resolvedArtifact + + def operation = new CreateServerGroupAtomicOperation(description) + operation.artifactDownloader = artifactDownloader + operation.mapper = objectMapper + + artifactDownloader.download(_) >> new ByteArrayInputStream() + objectMapper.readValue(_,_) >> registerTaskDefRequest + + when: + operation.makeTaskDefinitionRequestFromArtifact("test-role", "v1-ecs-test-v001") + + then: + IllegalArgumentException exception = thrown() + exception.message == + "Task definition networkMode does not match server group value. Found 'bridge' but expected 'awsvpc'" + } + def 'should set additional environment variables'() { given: def description = Mock(CreateServerGroupDescription) diff --git a/clouddriver-ecs/src/test/groovy/com/netflix/spinnaker/clouddriver/ecs/services/ContainerInformationServiceSpec.groovy b/clouddriver-ecs/src/test/groovy/com/netflix/spinnaker/clouddriver/ecs/services/ContainerInformationServiceSpec.groovy index 15e2fc1c887..e3bed9bd0bb 100644 --- a/clouddriver-ecs/src/test/groovy/com/netflix/spinnaker/clouddriver/ecs/services/ContainerInformationServiceSpec.groovy +++ b/clouddriver-ecs/src/test/groovy/com/netflix/spinnaker/clouddriver/ecs/services/ContainerInformationServiceSpec.groovy @@ -327,18 +327,99 @@ class ContainerInformationServiceSpec extends Specification { retrievedIp == null } - def 'should throw an exception when task has multiple containers'() { + def 'should return null when task has multiple network bindings'() { given: + def account = 'test-account' + def region = 'us-west-1' + + def ecsAccount = new ECSCredentialsConfig.Account( + name: account, + awsAccount: 'aws-' + account + ) + def task = new Task( - containers: [new Container(), new Container(), new Container()] + containerInstanceArn: 'container-instance-arn', + containers: [ + new Container( + networkBindings: [ + new NetworkBinding( + hostPort: 1234 + ) + ] + ), + new Container( + networkBindings: [ + new NetworkBinding( + hostPort: 5678 + ) + ] + ) + ] + ) + + def containerInstance = new ContainerInstance( + ec2InstanceId: 'i-deadbeef' ) + def instance = new Instance( + privateIpAddress: '127.0.0.1' + ) + + containerInstanceCacheClient.get(_) >> containerInstance + ecsInstanceCacheClient.find(_, _, _) >> [instance] + ecsCredentialsConfig.getAccounts() >> [ecsAccount] + when: - service.getTaskPrivateAddress('test-account', 'region', task) + def retrievedIp = service.getTaskPrivateAddress(account, region, task) then: - IllegalArgumentException exception = thrown() - exception.message == 'Multiple containers for a task is not supported.' + retrievedIp == null + } + + def 'should return a proper address when task has multiple containers but only one network binding'() { + given: + def account = 'test-account' + def region = 'us-west-1' + def containerInstanceArn = 'container-instance-arn' + def ip = '127.0.0.1' + def port = 1337 + + def ecsAccount = new ECSCredentialsConfig.Account( + name: account, + awsAccount: 'aws-' + account + ) + + def task = new Task( + containerInstanceArn: containerInstanceArn, + containers: [ + new Container( + networkBindings: [ + new NetworkBinding( + hostPort: port + ) + ] + ), + new Container() + ] + ) + + def containerInstance = new ContainerInstance( + ec2InstanceId: 'i-deadbeef' + ) + + def instance = new Instance( + privateIpAddress: ip + ) + + containerInstanceCacheClient.get(_) >> containerInstance + ecsInstanceCacheClient.find(_, _, _) >> [instance] + ecsCredentialsConfig.getAccounts() >> [ecsAccount] + + when: + def retrievedIp = service.getTaskPrivateAddress(account, region, task) + + then: + retrievedIp == ip + ':' + port } def 'should throw an exception when container has multiple ec2 instances'() {