Skip to content

Commit

Permalink
feat(ecs): Add support for task definition artifacts (#3016)
Browse files Browse the repository at this point in the history
  • Loading branch information
allisaurus authored and ajordens committed Jul 5, 2019
1 parent bce489e commit 5f7a1d0
Show file tree
Hide file tree
Showing 2 changed files with 283 additions and 34 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,16 @@

package com.netflix.spinnaker.orca.clouddriver.tasks.providers.ecs

import com.fasterxml.jackson.databind.ObjectMapper
import com.netflix.spinnaker.kork.artifacts.model.Artifact
import com.netflix.spinnaker.orca.clouddriver.tasks.servergroup.ServerGroupCreator
import com.netflix.spinnaker.orca.kato.tasks.DeploymentDetailsAware
import com.netflix.spinnaker.orca.pipeline.model.DockerTrigger
import com.netflix.spinnaker.orca.pipeline.model.Execution.ExecutionType
import com.netflix.spinnaker.orca.pipeline.model.Stage
import com.netflix.spinnaker.orca.pipeline.util.ArtifactResolver
import groovy.util.logging.Slf4j
import javax.annotation.Nullable
import org.springframework.stereotype.Component

@Slf4j
Expand All @@ -33,6 +37,13 @@ class EcsServerGroupCreator implements ServerGroupCreator, DeploymentDetailsAwar

final Optional<String> healthProviderName = Optional.of("ecs")

final ObjectMapper mapper = new ObjectMapper()
final ArtifactResolver artifactResolver

EcsServerGroupCreator(ArtifactResolver artifactResolver) {
this.artifactResolver = artifactResolver
}

@Override
List<Map> getOperations(Stage stage) {
def operation = [:]
Expand All @@ -43,45 +54,26 @@ class EcsServerGroupCreator implements ServerGroupCreator, DeploymentDetailsAwar
operation.credentials = operation.account
}

def imageDescription = (Map<String, Object>) operation.imageDescription

if (imageDescription) {
if (imageDescription.fromContext) {
if (stage.execution.type == ExecutionType.ORCHESTRATION) {
// Use image from specific "find image from tags" stage
def imageStage = stage.ancestorsWithParentPipelines().find {
it.refId == imageDescription.stageId && it.context.containsKey("amiDetails")
}

if (!imageStage) {
throw new IllegalStateException("No image stage found in context for $imageDescription.imageLabelOrSha.")
}

imageDescription.imageId = imageStage.context.amiDetails.imageId.value.get(0).toString()
}
if (operation.useTaskDefinitionArtifact) {
if (operation.taskDefinitionArtifact) {
operation.resolvedTaskDefinitionArtifact = getTaskDefArtifact(stage, operation.taskDefinitionArtifact)
} else {
throw new IllegalStateException("No task definition artifact found in context for operation.")
}

if (imageDescription.fromTrigger) {
if (stage.execution.type == ExecutionType.PIPELINE) {
def trigger = stage.execution.trigger

if (trigger instanceof DockerTrigger && trigger.account == imageDescription.account && trigger.repository == imageDescription.repository) {
imageDescription.tag = trigger.tag
}

imageDescription.imageId = buildImageId(imageDescription.registry, imageDescription.repository, imageDescription.tag)
}

if (!imageDescription.tag) {
throw new IllegalStateException("No tag found for image ${imageDescription.registry}/${imageDescription.repository} in trigger context.")
}
// container mappings are required for artifacts, so we know which container(s) get which images
if (operation.containerMappings) {
def containerMappings = (ArrayList<Map<String, Object>>) operation.containerMappings
operation.containerToImageMap = getContainerToImageMap(containerMappings, stage)
} else {
throw new IllegalStateException("No container mappings for task definition artifact found in context for operation.")
}
}

if (!imageDescription.imageId) {
imageDescription.imageId = buildImageId(imageDescription.registry, imageDescription.repository, imageDescription.tag)
}
def imageDescription = (Map<String, Object>) operation.imageDescription

operation.dockerImageAddress = imageDescription.imageId
if (imageDescription) {
operation.dockerImageAddress = getImageAddressFromDescription(imageDescription, stage)
} else if (!operation.dockerImageAddress) {
// Fall back to previous behavior: use image from any previous "find image from tags" stage by default
def bakeStage = getPreviousStageWithImage(stage, operation.region, cloudProvider)
Expand All @@ -101,4 +93,73 @@ class EcsServerGroupCreator implements ServerGroupCreator, DeploymentDetailsAwar
return "$repo:$tag"
}
}

private Artifact getTaskDefArtifact(Stage stage, Object input) {
TaskDefinitionArtifact taskDefArtifactInput = mapper.convertValue(input, TaskDefinitionArtifact.class)

Artifact taskDef = artifactResolver.getBoundArtifactForStage(
stage,
taskDefArtifactInput.artifactId,
taskDefArtifactInput.artifact)
if (taskDef == null) {
throw new IllegalArgumentException("Unable to bind the task definition artifact");
}
return taskDef
}

private Map<String, String> getContainerToImageMap(ArrayList<Map<String, Object>> mappings, Stage stage) {
def containerToImageMap = [:]

// each mapping should be in the shape { containerName: "", imageDescription: {}}
mappings.each{
def imageValue = (Map<String, Object>) it.imageDescription
def resolvedImageAddress = getImageAddressFromDescription(imageValue, stage)
def name = (String) it.containerName
containerToImageMap.put(name, resolvedImageAddress)
}
return containerToImageMap
}

private String getImageAddressFromDescription(Map<String, Object> description, Stage givenStage) {
if (description.fromContext) {
if (givenStage.execution.type == ExecutionType.ORCHESTRATION) {
// Use image from specific "find image from tags" stage
def imageStage = givenStage.findAncestor({
return it.context.containsKey("amiDetails") && it.refId == description.stageId
})

if (!imageStage) {
throw new IllegalStateException("No image stage found in context for $description.imageLabelOrSha.")
}

description.imageId = imageStage.context.amiDetails.imageId.value.get(0).toString()
}
}

if (description.fromTrigger) {
if (givenStage.execution.type == ExecutionType.PIPELINE) {
def trigger = givenStage.execution.trigger

if (trigger instanceof DockerTrigger && trigger.account == description.account && trigger.repository == description.repository) {
description.tag = trigger.tag
}
description.imageId = buildImageId(description.registry, description.repository, description.tag)
}

if (!description.tag) {
throw new IllegalStateException("No tag found for image ${description.registry}/${description.repository} in trigger context.")
}
}

if (!description.imageId) {
description.imageId = buildImageId(description.registry, description.repository, description.tag)
}

return description.imageId
}

private static class TaskDefinitionArtifact {
@Nullable public String artifactId
@Nullable public Artifact artifact
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@
/*
* Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License").
* You may not use this file except in compliance with the License.
* A copy of the License is located at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* or in the "license" file accompanying this file. This file 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 com.netflix.spinnaker.orca.clouddriver.tasks.providers.ecs

import com.google.common.collect.Maps
import spock.lang.Specification
import spock.lang.Subject
import com.netflix.spinnaker.kork.artifacts.model.Artifact
import com.netflix.spinnaker.orca.pipeline.model.Execution
import com.netflix.spinnaker.orca.pipeline.model.Execution.ExecutionType
import com.netflix.spinnaker.orca.pipeline.util.ArtifactResolver
import static com.netflix.spinnaker.orca.test.model.ExecutionBuilder.stage

class EcsServerGroupCreatorSpec extends Specification {

@Subject
ArtifactResolver mockResolver
EcsServerGroupCreator creator
def stage = stage {}

def deployConfig = [
credentials: "testUser",
application: "ecs"
]

def setup() {
mockResolver = Stub(ArtifactResolver)
creator = new EcsServerGroupCreator(mockResolver)
stage.execution.stages.add(stage)
stage.context = deployConfig
}

def cleanup() {
stage.execution.stages.clear()
stage.execution.stages.add(stage)
}

def "creates operation from trigger image"() {
given:
def (testReg,testRepo,testTag) = ["myregistry.io","myrepo","latest"]
def testDescription = [
fromTrigger: "true",
registry: testReg,
repository: testRepo,
tag: testTag
]
stage.context.imageDescription = testDescription
stage.execution = new Execution(ExecutionType.PIPELINE, 'ecs')
def expected = Maps.newHashMap(deployConfig)
expected.dockerImageAddress = "$testReg/$testRepo:$testTag"

when:
def operations = creator.getOperations(stage)

then:
operations.find {
it.containsKey("createServerGroup")
}.createServerGroup == expected
}

def "creates operation from context image"() {
given:
def (testReg, testRepo, testTag) = ["myregistry.io", "myrepo", "latest"]
def parentStageId = "PARENTID123"
def testDescription = [
fromContext: "true",
stageId : parentStageId,
registry : testReg,
repository : testRepo,
tag : testTag
]

def parentStage = stage {}
parentStage.id = parentStageId
parentStage.refId = parentStageId
parentStage.context.amiDetails = [imageId: [value: ["$testReg/$testRepo:$testTag"]]]

stage.context.imageDescription = testDescription
stage.parentStageId = parentStageId
stage.execution = new Execution(ExecutionType.ORCHESTRATION, 'ecs')
stage.execution.stages.add(parentStage)

def expected = Maps.newHashMap(deployConfig)
expected.dockerImageAddress = "$testReg/$testRepo:$testTag"

when:
def operations = creator.getOperations(stage)

then:
operations.find {
it.containsKey("createServerGroup")
}.createServerGroup == expected
}

def "creates operation from previous 'find image from tags' stage"() {
given:
def (testReg, testRepo, testTag, testRegion) = ["myregistry.io", "myrepo", "latest", "us-west-2"]
def parentStageId = "PARENTID123"

def parentStage = stage {}
parentStage.id = parentStageId
parentStage.context.region = testRegion
parentStage.context.cloudProviderType = "ecs"
parentStage.context.amiDetails = [imageId: [value: ["$testReg/$testRepo:$testTag"]]]

stage.context.region = testRegion
stage.parentStageId = parentStageId
stage.execution = new Execution(ExecutionType.PIPELINE, 'ecs')
stage.execution.stages.add(parentStage)

def expected = Maps.newHashMap(deployConfig)
expected.dockerImageAddress = "$testReg/$testRepo:$testTag"

when:
def operations = creator.getOperations(stage)

then:
operations.find {
it.containsKey("createServerGroup")
}.createServerGroup == expected
}

def "creates operation from taskDefinitionArtifact provided as artifact"() {
given:
// define artifact inputs
def testArtifactId = "aaaa-bbbb-cccc-dddd"
def taskDefArtifact = [
artifactId: testArtifactId
]
Artifact resolvedArtifact = new Artifact().builder().type('s3/object').name('s3://testfile.json').build()
mockResolver.getBoundArtifactForStage(stage, testArtifactId, null) >> resolvedArtifact
// define container mappings inputs
def (testReg,testRepo,testTag) = ["myregistry.io","myrepo","latest"]
def testDescription = [
fromTrigger: "true",
registry: testReg,
repository: testRepo,
tag: testTag
]
def testMappings = []
def map1 = [
containerName: "web",
imageDescription: testDescription
]
def map2 = [
containerName: "logs",
imageDescription: testDescription
]
testMappings.add(map1)
testMappings.add(map2)

def containerToImageMap = [
web: "$testReg/$testRepo:$testTag",
logs: "$testReg/$testRepo:$testTag"
]

// add inputs to stage context
stage.execution = new Execution(ExecutionType.PIPELINE, 'ecs')
stage.context.useTaskDefinitionArtifact = true
stage.context.taskDefinitionArtifact = taskDefArtifact
stage.context.containerMappings = testMappings

def expected = Maps.newHashMap(deployConfig)
expected.resolvedTaskDefinitionArtifact = resolvedArtifact
expected.containerToImageMap = containerToImageMap

when:
def operations = creator.getOperations(stage)

then:
operations.find {
it.containsKey("createServerGroup")
}.createServerGroup == expected
}
}

0 comments on commit 5f7a1d0

Please sign in to comment.