Skip to content

Commit

Permalink
feat(ecs): Add support for task definition artifact (#3825)
Browse files Browse the repository at this point in the history
* feat(ecs): Add support for task definition artifact

* add fixes:
 * set spinnaker execution role only if none provided
 * fix getTaskPrivateAddress for multi container tasks
 * make cpu, reservedMemory, and dockerImageAddress nullable w/ artifact

* add unit tests, fix server group name resolution

* add check and throw exception if network mode in task def doesn't match server group
  • Loading branch information
allisaurus authored and maggieneterval committed Jul 2, 2019
1 parent 659b970 commit 9b3fef6
Show file tree
Hide file tree
Showing 7 changed files with 475 additions and 53 deletions.
2 changes: 2 additions & 0 deletions clouddriver-ecs/clouddriver-ecs.gradle
Original file line number Diff line number Diff line change
@@ -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")
Expand All @@ -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"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -35,13 +37,13 @@ public class CreateServerGroupDescription extends AbstractECSDescription {

String portProtocol;

Integer computeUnits;
Integer reservedMemory;
@Nullable Integer computeUnits;
@Nullable Integer reservedMemory;

Map<String, String> environmentVariables;
Map<String, String> tags;

String dockerImageAddress;
@Nullable String dockerImageAddress;
String dockerImageCredentialsSecret;

ServerGroup.Capacity capacity;
Expand All @@ -67,6 +69,12 @@ public class CreateServerGroupDescription extends AbstractECSDescription {

List<ServiceDiscoveryAssociation> serviceDiscoveryAssociations;

boolean useTaskDefinitionArtifact;
Artifact resolvedTaskDefinitionArtifact;
String taskDefinitionArtifactAccount;
Map<String, String> containerToImageMap;
String loadBalancedContainer;

@Override
public String getRegion() {
// CreateServerGroupDescription does not contain a region. Instead it has AvailabilityZones
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;

Expand All @@ -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");
}
Expand Down Expand Up @@ -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);

Expand All @@ -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()
Expand Down Expand Up @@ -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);

Expand Down Expand Up @@ -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<ContainerDefinition> 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<ContainerDefinition> 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<KeyValuePair> updatedEnv =
setSpinnakerEnvVars(c.getEnvironment(), newServerGroupName);
c.setEnvironment(updatedEnv);

Map<String, String> 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,
Expand Down Expand Up @@ -552,7 +654,12 @@ private Collection<LoadBalancer> retrieveLoadBalancers(String containerName) {
Collection<LoadBalancer> 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();
Expand Down Expand Up @@ -623,6 +730,38 @@ private String getServerGroupName(Service service) {
return getRegion() + ":" + service.getServiceName();
}

private Collection<KeyValuePair> setSpinnakerEnvVars(
Collection<KeyValuePair> 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<String, String> setSpinnakerDockerLabels(
Map<String, String> targetMap, String newServerGroupName) {

Map<String, String> 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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -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(
Expand Down
Loading

0 comments on commit 9b3fef6

Please sign in to comment.