Skip to content

Commit

Permalink
feat(artifact/decoration): Artifact decoration spinnaker/spinnaker#1348
Browse files Browse the repository at this point in the history
… (#202)
  • Loading branch information
brujoand authored and Matt Duftler committed May 23, 2017
1 parent b2cadca commit 072cfa3
Show file tree
Hide file tree
Showing 17 changed files with 367 additions and 92 deletions.
@@ -0,0 +1,31 @@
/*
* Copyright 2017 Schibsted ASA.
*
* 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 com.netflix.spinnaker.rosco.api

import groovy.transform.CompileStatic
import groovy.transform.EqualsAndHashCode
import groovy.transform.ToString

@EqualsAndHashCode
@ToString(includeNames = true)
class Artifact {
String name
String type
String version
String reference
Map<String, String> metadata
}
Expand Up @@ -34,4 +34,5 @@ class Bake {
String id
String ami
String image_name
Artifact artifact
}
Expand Up @@ -17,9 +17,11 @@
package com.netflix.spinnaker.rosco.executor

import com.netflix.spectator.api.Registry
import com.netflix.spinnaker.rosco.api.Artifact
import com.netflix.spinnaker.rosco.api.Bake
import com.netflix.spinnaker.rosco.api.BakeRequest
import com.netflix.spinnaker.rosco.api.BakeStatus
import com.netflix.spinnaker.rosco.jobs.BakeRecipe
import com.netflix.spinnaker.rosco.jobs.JobExecutor
import com.netflix.spinnaker.rosco.persistence.BakeStore
import com.netflix.spinnaker.rosco.providers.registry.CloudProviderBakeHandlerRegistry
Expand Down Expand Up @@ -197,6 +199,7 @@ class BakePoller implements ApplicationListener<ContextRefreshedEvent> {
}
}


void completeBake(String bakeId, String logsContent) {
if (logsContent) {
def cloudProvider = bakeStore.retrieveCloudProviderById(bakeId)
Expand All @@ -211,6 +214,14 @@ class BakePoller implements ApplicationListener<ContextRefreshedEvent> {
if (region) {
Bake bakeDetails = cloudProviderBakeHandler.scrapeCompletedBakeResults(region, bakeId, logsContent)

if (bakeDetails) {
BakeRequest bakeRequest = bakeStore.retrieveBakeRequestById(bakeId)
BakeRecipe bakeRecipe = bakeStore.retrieveBakeRecipeById(bakeId)
bakeDetails.artifact = cloudProviderBakeHandler.produceArtifactDecorationFrom(
bakeRequest, bakeRecipe, bakeDetails, cloudProvider
)
}

bakeStore.updateBakeDetails(bakeDetails)

return
Expand Down
@@ -0,0 +1,23 @@
/*
* Copyright 2017 Schibsted ASA.
*
* 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 com.netflix.spinnaker.rosco.jobs

class BakeRecipe {
String name
String version
List<String> command
}
Expand Up @@ -19,6 +19,7 @@ package com.netflix.spinnaker.rosco.persistence
import com.netflix.spinnaker.rosco.api.Bake
import com.netflix.spinnaker.rosco.api.BakeRequest
import com.netflix.spinnaker.rosco.api.BakeStatus
import com.netflix.spinnaker.rosco.jobs.BakeRecipe

/**
* Persistence service for in-flight and completed bakes.
Expand All @@ -31,10 +32,10 @@ interface BakeStore {
public boolean acquireBakeLock(String bakeKey)

/**
* Store the region, bakeRequest and bakeStatus in association with both the bakeKey and bakeId. If bake key
* Store the region, bakeRecipe, bakeRequest and bakeStatus in association with both the bakeKey and bakeId. If bake key
* has already been set, return a bakeStatus with that bake's id instead. None of the arguments may be null.
*/
public BakeStatus storeNewBakeStatus(String bakeKey, String region, BakeRequest bakeRequest, BakeStatus bakeStatus, String command)
public BakeStatus storeNewBakeStatus(String bakeKey, String region, BakeRecipe bakeRecipe, BakeRequest bakeRequest, BakeStatus bakeStatus, String command)

/**
* Update the completed bake details associated with both the bakeKey and bakeDetails.id. bakeDetails may not be null.
Expand Down Expand Up @@ -72,6 +73,16 @@ interface BakeStore {
*/
public BakeStatus retrieveBakeStatusById(String bakeId)

/**
* Retrieve the bake request associated with the bakeId.
*/
public BakeRequest retrieveBakeRequestById(String bakeId)

/**
* Retrieve the bake recipe associated with the bakeId.
*/
public BakeRecipe retrieveBakeRecipeById(String bakeId)

/**
* Retrieve the completed bake details associated with the bakeId. bakeId may be null.
*/
Expand Down
Expand Up @@ -20,6 +20,8 @@ import com.fasterxml.jackson.databind.ObjectMapper
import com.netflix.spinnaker.rosco.api.Bake
import com.netflix.spinnaker.rosco.api.BakeRequest
import com.netflix.spinnaker.rosco.api.BakeStatus
import com.netflix.spinnaker.rosco.jobs.BakeRecipe
import com.netflix.spinnaker.rosco.providers.CloudProviderBakeHandler
import groovy.transform.CompileStatic
import org.springframework.beans.factory.annotation.Autowired
import redis.clients.jedis.JedisPool
Expand Down Expand Up @@ -84,23 +86,25 @@ class RedisBackedBakeStore implements BakeStore {
redis.call('HMSET', KEYS[2],
'bakeKey', KEYS[3],
'region', ARGV[2],
'bakeRequest', ARGV[3],
'bakeStatus', ARGV[4],
'bakeLogs', ARGV[5],
'command', ARGV[6],
'roscoInstanceId', ARGV[7],
'bakeRecipe', ARGV[3],
'bakeRequest', ARGV[4],
'bakeStatus', ARGV[5],
'bakeLogs', ARGV[6],
'command', ARGV[7],
'roscoInstanceId', ARGV[8],
'createdTimestamp', ARGV[1],
'updatedTimestamp', ARGV[1])
-- Set bake key hash values.
redis.call('HMSET', KEYS[3],
'id', KEYS[2],
'region', ARGV[2],
'bakeRequest', ARGV[3],
'bakeStatus', ARGV[4],
'bakeLogs', ARGV[5],
'command', ARGV[6],
'roscoInstanceId', ARGV[7],
'bakeRecipe', ARGV[3],
'bakeRequest', ARGV[4],
'bakeStatus', ARGV[5],
'bakeLogs', ARGV[6],
'command', ARGV[7],
'roscoInstanceId', ARGV[8],
'createdTimestamp', ARGV[1],
'updatedTimestamp', ARGV[1])
Expand Down Expand Up @@ -300,14 +304,15 @@ class RedisBackedBakeStore implements BakeStore {
}

@Override
public BakeStatus storeNewBakeStatus(String bakeKey, String region, BakeRequest bakeRequest, BakeStatus bakeStatus, String command) {
public BakeStatus storeNewBakeStatus(String bakeKey, String region, BakeRecipe bakeRecipe, BakeRequest bakeRequest, BakeStatus bakeStatus, String command) {
def lockKey = "lock:$bakeKey"
def bakeRecipeJson = mapper.writeValueAsString(bakeRecipe)
def bakeRequestJson = mapper.writeValueAsString(bakeRequest)
def bakeStatusJson = mapper.writeValueAsString(bakeStatus)
def bakeLogsJson = mapper.writeValueAsString(bakeStatus.logsContent ? [logsContent: bakeStatus.logsContent] : [:])
def createdTimestampMilliseconds = timeInMilliseconds
def keyList = ["allBakes", bakeStatus.id, bakeKey, thisInstanceIncompleteBakesKey, lockKey.toString()]
def argList = [createdTimestampMilliseconds + "", region, bakeRequestJson, bakeStatusJson, bakeLogsJson, command, roscoInstanceId]
def argList = [createdTimestampMilliseconds as String, region, bakeRecipeJson, bakeRequestJson, bakeStatusJson, bakeLogsJson, command, roscoInstanceId]
def result = evalSHA("storeNewBakeStatusSHA", keyList, argList)

// Check if the script returned a bake status set by the winner of a race.
Expand Down Expand Up @@ -404,6 +409,25 @@ class RedisBackedBakeStore implements BakeStore {
}
}

@Override
public BakeRequest retrieveBakeRequestById(String bakeId) {
def jedis = jedisPool.getResource()

jedis.withCloseable {
def bakeRequestJson = jedis.hget(bakeId, "bakeRequest")
return bakeRequestJson ? mapper.readValue(bakeRequestJson, BakeRequest) : null
}
}

@Override
public BakeRecipe retrieveBakeRecipeById(String bakeId) {
def jedis = jedisPool.getResource()
jedis.withCloseable {
def bakeRecipeJson = jedis.hget(bakeId, "bakeRecipe")
return bakeRecipeJson ? mapper.readValue(bakeRecipeJson, BakeRecipe) : null
}
}

@Override
public Bake retrieveBakeDetailsById(String bakeId) {
def jedis = jedisPool.getResource()
Expand Down
Expand Up @@ -16,10 +16,12 @@

package com.netflix.spinnaker.rosco.providers

import com.netflix.spinnaker.rosco.api.Artifact
import com.netflix.spinnaker.rosco.api.Bake
import com.netflix.spinnaker.rosco.api.BakeOptions
import com.netflix.spinnaker.rosco.api.BakeOptions.BaseImage
import com.netflix.spinnaker.rosco.api.BakeRequest
import com.netflix.spinnaker.rosco.jobs.BakeRecipe
import com.netflix.spinnaker.rosco.providers.util.ImageNameFactory
import com.netflix.spinnaker.rosco.providers.util.PackageNameConverter
import com.netflix.spinnaker.rosco.providers.util.PackerCommandFactory
Expand Down Expand Up @@ -104,6 +106,25 @@ abstract class CloudProviderBakeHandler {
*/
abstract Bake scrapeCompletedBakeResults(String region, String bakeId, String logsContent)

/**
* Returns a decorated artifact for a given bake. Right now this is a generic approach
* but it could be useful to override this method for specific providers.
*/
def Artifact produceArtifactDecorationFrom(BakeRequest bakeRequest, BakeRecipe bakeRecipe, Bake bakeDetails, String cloudProvider) {
Artifact bakedArtifact = new Artifact(
name: bakeRecipe?.name,
version: bakeRecipe?.version,
type: cloudProvider,
reference: bakeDetails.ami ?: bakeDetails.image_name,
metadata: [
build_info_url: bakeRequest?.build_info_url,
build_number: bakeRequest?.build_number
]
)

return bakedArtifact
}

/**
* Finds the appropriate virtualization settings in this provider's configuration based on the region and
* bake request parameters. Throws an IllegalArgumentException if the virtualization settings cannot be
Expand Down Expand Up @@ -133,9 +154,11 @@ abstract class CloudProviderBakeHandler {
abstract String getTemplateFileName(BakeOptions.BaseImage baseImage)

/**
* Build provider-specific command for packer.
* Right now this builds a recipe for packer.
* In the future this method should be abstract, and have
* provider-specific implementations.
*/
List<String> producePackerCommand(String region, BakeRequest bakeRequest) {
BakeRecipe produceBakeRecipe(String region, BakeRequest bakeRequest) {
def virtualizationSettings = findVirtualizationSettings(region, bakeRequest)

BakeOptions.Selected selectedOptions = new BakeOptions.Selected(baseImage: findBaseImage(bakeRequest))
Expand Down Expand Up @@ -188,11 +211,12 @@ abstract class CloudProviderBakeHandler {
def finaltemplateFilePath = "$configDir/$finalTemplateFileName"
def finalVarFileName = bakeRequest.var_file_name ? "$configDir/$bakeRequest.var_file_name" : null
def baseCommand = getBaseCommand(finalTemplateFileName)
def packerCommand = packerCommandFactory.buildPackerCommand(baseCommand,
parameterMap,
finalVarFileName,
finaltemplateFilePath)

return packerCommandFactory.buildPackerCommand(baseCommand,
parameterMap,
finalVarFileName,
finaltemplateFilePath)
return new BakeRecipe(name: imageName, version: appVersionStr, command: packerCommand)
}

protected Map unrollParameters(Map.Entry entry) {
Expand Down
Expand Up @@ -17,18 +17,21 @@
package com.netflix.spinnaker.rosco.executor

import com.netflix.spectator.api.DefaultRegistry
import com.netflix.spinnaker.rosco.api.Artifact
import com.netflix.spinnaker.rosco.api.Bake
import com.netflix.spinnaker.rosco.api.BakeRequest
import com.netflix.spinnaker.rosco.api.BakeStatus
import com.netflix.spinnaker.rosco.jobs.BakeRecipe
import com.netflix.spinnaker.rosco.persistence.RedisBackedBakeStore
import com.netflix.spinnaker.rosco.providers.CloudProviderBakeHandler
import com.netflix.spinnaker.rosco.providers.registry.CloudProviderBakeHandlerRegistry
import com.netflix.spinnaker.rosco.jobs.JobExecutor
import com.netflix.spinnaker.rosco.providers.util.TestDefaults
import spock.lang.Specification
import spock.lang.Subject
import spock.lang.Unroll

class BakePollerSpec extends Specification {
class BakePollerSpec extends Specification implements TestDefaults {

private static final String REGION = "some-region"
private static final String JOB_ID = "123"
Expand Down Expand Up @@ -80,6 +83,8 @@ class BakePollerSpec extends Specification {
state: bakeState,
result: bakeResult,
logsContent: "$LOGS_CONTENT\n$LOGS_CONTENT")
def bakeRequest = new BakeRequest(build_info_url: SOME_BUILD_INFO_URL)
def bakeRecipe = new BakeRecipe(name: SOME_BAKE_RECIPE_NAME, version: SOME_APP_VERSION_STR, command: [])
def bakeDetails = new Bake(id: JOB_ID, ami: AMI_ID, image_name: IMAGE_NAME)

@Subject
Expand All @@ -97,6 +102,8 @@ class BakePollerSpec extends Specification {
1 * cloudProviderBakeHandlerRegistryMock.lookup(BakeRequest.CloudProviderType.gce) >> cloudProviderBakeHandlerMock
2 * bakeStoreMock.retrieveRegionById(JOB_ID) >> REGION // 1 for metrics
1 * cloudProviderBakeHandlerMock.scrapeCompletedBakeResults(REGION, JOB_ID, "$LOGS_CONTENT\n$LOGS_CONTENT") >> bakeDetails
1 * bakeStoreMock.retrieveBakeRequestById(JOB_ID) >> bakeRequest
1 * bakeStoreMock.retrieveBakeRecipeById(JOB_ID) >> bakeRecipe
1 * bakeStoreMock.updateBakeDetails(bakeDetails)
1 * bakeStoreMock.updateBakeStatus(completeBakeStatus)
1 * bakeStoreMock.retrieveBakeStatusById(JOB_ID) >> completeBakeStatus
Expand Down Expand Up @@ -129,4 +136,46 @@ class BakePollerSpec extends Specification {
1 * bakeStoreMock.retrieveBakeStatusById(JOB_ID) >> new BakeStatus()
}

void 'decorate the bakeDetails with an artifact if bake is successful'() {
setup:
def cloudProviderBakeHandlerRegistryMock = Mock(CloudProviderBakeHandlerRegistry)
def cloudProviderBakeHandlerMock = Mock(CloudProviderBakeHandler)
def bakeStoreMock = Mock(RedisBackedBakeStore)
def jobExecutorMock = Mock(JobExecutor)
def bakeRequest = new BakeRequest(build_info_url: SOME_BUILD_INFO_URL)
def bakeRecipe = new BakeRecipe(name: SOME_BAKE_RECIPE_NAME, version: SOME_APP_VERSION_STR, command: [])
def bakedArtifact = new Artifact(
name: bakeRecipe.name,
version: bakeRecipe.version,
type: DOCKER_CLOUD_PROVIDER,
reference: AMI_ID,
metadata: [
build_info_url: bakeRequest.build_info_url,
build_number: bakeRequest.build_number
]
)
def bakeDetails = new Bake(id: JOB_ID, ami: AMI_ID, image_name: IMAGE_NAME, artifact: bakedArtifact)
def decoratedBakeDetails = new Bake(id: JOB_ID, ami: AMI_ID, image_name: IMAGE_NAME, artifact: bakedArtifact)

@Subject
def bakePoller = new BakePoller(
bakeStore: bakeStoreMock,
executor: jobExecutorMock,
cloudProviderBakeHandlerRegistry: cloudProviderBakeHandlerRegistryMock,
registry: new DefaultRegistry())

when:
bakePoller.completeBake(JOB_ID, LOGS_CONTENT)

then:
1 * bakeStoreMock.retrieveCloudProviderById(JOB_ID) >> DOCKER_CLOUD_PROVIDER.toString()
1 * cloudProviderBakeHandlerRegistryMock.lookup(DOCKER_CLOUD_PROVIDER) >> cloudProviderBakeHandlerMock
1 * bakeStoreMock.retrieveRegionById(JOB_ID) >> REGION
1 * cloudProviderBakeHandlerMock.scrapeCompletedBakeResults(REGION, JOB_ID, LOGS_CONTENT) >> bakeDetails
1 * cloudProviderBakeHandlerMock.produceArtifactDecorationFrom(bakeRequest, bakeRecipe, bakeDetails, DOCKER_CLOUD_PROVIDER.toString()) >> bakedArtifact
1 * bakeStoreMock.retrieveBakeRequestById(JOB_ID) >> bakeRequest
1 * bakeStoreMock.retrieveBakeRecipeById(JOB_ID) >> bakeRecipe
1 * bakeStoreMock.updateBakeDetails(decoratedBakeDetails)
}

}

0 comments on commit 072cfa3

Please sign in to comment.