Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(kayenta): pass the accountId to Kayenta for deployments #2889

Merged
merged 1 commit into from
May 2, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ data class CanaryScope(
val start: Instant,
val end: Instant,
val step: Long = 60, // TODO: would be nice to use a Duration
val extendedScopeParams: Map<String, String> = emptyMap()
val extendedScopeParams: Map<String, String?> = emptyMap()
)

data class Thresholds(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@

package com.netflix.spinnaker.orca.kayenta.pipeline

import com.fasterxml.jackson.annotation.JsonCreator
import com.fasterxml.jackson.databind.SerializationFeature.WRITE_DATES_AS_TIMESTAMPS
import com.fasterxml.jackson.module.kotlin.convertValue
import com.netflix.spinnaker.orca.ext.mapTo
Expand Down Expand Up @@ -46,6 +47,18 @@ class RunCanaryIntervalsStage(private val clock: Clock) : StageDefinitionBuilder
override fun taskGraph(stage: Stage, builder: TaskNode.Builder) {
}

private fun getDeployDetails(stage: Stage) : DeployedServerGroupContext? {
val deployedServerGroupsStage = stage.parent?.execution?.stages?.find {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IMO, you'll want to use StageNavigator and not just look for the first matching stage in the execution.

The navigator will look up the graph and allow you to grab the "closest" stage.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll look into this, thanks

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Scratch that comment ... misunderstanding around the shape of this graph.

it.type == DeployCanaryServerGroupsStage.STAGE_TYPE && it.parentStageId == stage.parentStageId
}
if (deployedServerGroupsStage == null) {
return null
}
val deployedServerGroups = deployedServerGroupsStage.outputs["deployedServerGroups"] as List<*>
val data = deployedServerGroups.first() as Map<String, String>
return DeployedServerGroupContext.from(data)
}

override fun beforeStages(parent: Stage, graph: StageGraphBuilder) {
val canaryConfig = parent.mapTo<KayentaCanaryContext>("/canaryConfig")

Expand Down Expand Up @@ -89,7 +102,7 @@ class RunCanaryIntervalsStage(private val clock: Clock) : StageDefinitionBuilder
canaryConfig.configurationAccountName,
canaryConfig.storageAccountName,
canaryConfig.canaryConfigId,
buildRequestScopes(canaryConfig, i, canaryAnalysisInterval),
buildRequestScopes(canaryConfig, getDeployDetails(parent), i, canaryAnalysisInterval),
canaryConfig.scoreThresholds
)

Expand All @@ -104,6 +117,7 @@ class RunCanaryIntervalsStage(private val clock: Clock) : StageDefinitionBuilder

private fun buildRequestScopes(
config: KayentaCanaryContext,
deploymentDetails: DeployedServerGroupContext?,
interval: Int,
intervalDuration: Duration
): Map<String, CanaryScopes> {
Expand All @@ -127,22 +141,55 @@ class RunCanaryIntervalsStage(private val clock: Clock) : StageDefinitionBuilder
start = end.minus(config.lookback)
}

val controlScope = CanaryScope(
scope.controlScope,
scope.controlLocation,
val controlExtendedScopeParams = mutableMapOf<String, String?>()
controlExtendedScopeParams.putAll(scope.extendedScopeParams)
var controlLocation = scope.controlLocation
var controlScope = scope.controlScope
if (deploymentDetails != null) {
if (!controlExtendedScopeParams.containsKey("dataset")) {
controlExtendedScopeParams["dataset"] = "regional"
}
controlLocation = deploymentDetails.controlLocation
controlScope = deploymentDetails.controlScope
controlExtendedScopeParams["type"] = "asg"
if (deploymentDetails.controlAccountId != null) {
controlExtendedScopeParams["accountId"] = deploymentDetails.controlAccountId
}
}

val experimentExtendedScopeParams = mutableMapOf<String, String?>()
experimentExtendedScopeParams.putAll(scope.extendedScopeParams)
var experimentLocation = scope.experimentLocation
var experimentScope = scope.experimentScope
if (deploymentDetails != null) {
if (!experimentExtendedScopeParams.containsKey("dataset")) {
experimentExtendedScopeParams["dataset"] = "regional"
}
experimentLocation = deploymentDetails.experimentLocation
experimentScope = deploymentDetails.experimentScope
experimentExtendedScopeParams["type"] = "asg"
if (deploymentDetails.experimentAccountId != null) {
experimentExtendedScopeParams["accountId"] = deploymentDetails.experimentAccountId
}
}

val controlScopeData = CanaryScope(
controlScope,
controlLocation,
start,
end,
config.step.seconds,
scope.extendedScopeParams
controlExtendedScopeParams
)
val experimentScope = controlScope.copy(
scope = scope.experimentScope,
location = scope.experimentLocation
val experimentScopeData = controlScopeData.copy(
scope = experimentScope,
location = experimentLocation,
extendedScopeParams = experimentExtendedScopeParams
)

requestScopes[scope.scopeName] = CanaryScopes(
controlScope = controlScope,
experimentScope = experimentScope
controlScope = controlScopeData,
experimentScope = experimentScopeData
)
}
return requestScopes
Expand All @@ -164,3 +211,25 @@ private val KayentaCanaryContext.startTime: Instant?

private val KayentaCanaryContext.step: Duration
get() = Duration.ofSeconds(scopes.first().step)

data class DeployedServerGroupContext @JsonCreator constructor(
val controlLocation: String,
val controlScope: String,
val controlAccountId: String?,
val experimentLocation: String,
val experimentScope: String,
val experimentAccountId: String?
) {
companion object {
fun from(data: Map<String, String>) : DeployedServerGroupContext {
return DeployedServerGroupContext(
data["controlLocation"].orEmpty(),
data["controlScope"].orEmpty(),
data["controlAccountId"],
data["experimentLocation"].orEmpty(),
data["experimentScope"].orEmpty(),
data["experimentAccountId"]
)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,29 +21,42 @@ import com.fasterxml.jackson.annotation.JsonProperty
import com.netflix.spinnaker.orca.ExecutionStatus
import com.netflix.spinnaker.orca.Task
import com.netflix.spinnaker.orca.TaskResult
import com.netflix.spinnaker.orca.clouddriver.MortService
import com.netflix.spinnaker.orca.ext.mapTo
import com.netflix.spinnaker.orca.kayenta.pipeline.DeployCanaryServerGroupsStage.Companion.DEPLOY_CONTROL_SERVER_GROUPS
import com.netflix.spinnaker.orca.kayenta.pipeline.DeployCanaryServerGroupsStage.Companion.DEPLOY_EXPERIMENT_SERVER_GROUPS
import com.netflix.spinnaker.orca.pipeline.model.Stage
import org.springframework.stereotype.Component

@Component
class PropagateDeployedServerGroupScopes : Task {
class PropagateDeployedServerGroupScopes(
private val mortService: MortService
) : Task {

private fun findAccountId(accountName: String): String? {
val details = mortService.getAccountDetails(accountName)
val accountId = details["accountId"] as String?
return accountId
}

override fun execute(stage: Stage): TaskResult {
val serverGroupPairs =
stage.childrenOf(DEPLOY_CONTROL_SERVER_GROUPS) zip stage.childrenOf(DEPLOY_EXPERIMENT_SERVER_GROUPS)

val scopes = serverGroupPairs.map { (control, experiment) ->
val scope = mutableMapOf<String, Any>()
control.mapTo<DeployServerGroupContext>().deployServerGroups.entries.first().let { (location, serverGroups) ->
val scope = mutableMapOf<String, Any?>()
val controlContext = control.mapTo<DeployServerGroupContext>()
controlContext.deployServerGroups.entries.first().let { (location, serverGroups) ->
scope["controlLocation"] = location
scope["controlScope"] = serverGroups.first()
}
experiment.mapTo<DeployServerGroupContext>().deployServerGroups.entries.first().let { (location, serverGroups) ->
scope["controlAccountId"] = findAccountId(controlContext.accountName)
val experimentContext = experiment.mapTo<DeployServerGroupContext>()
experimentContext.deployServerGroups.entries.first().let { (location, serverGroups) ->
scope["experimentLocation"] = location
scope["experimentScope"] = serverGroups.first()
}
scope["experimentAccountId"] = findAccountId(experimentContext.accountName)
scope
}

Expand All @@ -53,6 +66,7 @@ class PropagateDeployedServerGroupScopes : Task {
}
}


private fun Stage.childrenOf(name: String): List<Stage> {
val stage = execution.stages.find {
it.name == name
Expand All @@ -66,5 +80,6 @@ private fun Stage.childrenOf(name: String): List<Stage> {


data class DeployServerGroupContext @JsonCreator constructor(
@param:JsonProperty("deploy.server.groups") val deployServerGroups: Map<String, List<String>>
@param:JsonProperty("deploy.server.groups") val deployServerGroups: Map<String, List<String>>,
@param:JsonProperty("deploy.account.name") val accountName: String
)
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,11 @@

package com.netflix.spinnaker.orca.kayenta.tasks

import com.fasterxml.jackson.module.kotlin.convertValue
import com.netflix.spinnaker.orca.ExecutionStatus.SUCCEEDED
import com.netflix.spinnaker.orca.Task
import com.netflix.spinnaker.orca.TaskResult
import com.netflix.spinnaker.orca.ext.mapTo
import com.netflix.spinnaker.orca.jackson.OrcaObjectMapper
import com.netflix.spinnaker.orca.kayenta.CanaryExecutionRequest
import com.netflix.spinnaker.orca.kayenta.CanaryScopes
import com.netflix.spinnaker.orca.kayenta.KayentaService
import com.netflix.spinnaker.orca.kayenta.model.RunCanaryContext
import com.netflix.spinnaker.orca.pipeline.model.Stage
Expand All @@ -40,12 +37,6 @@ class RunKayentaCanaryTask(

override fun execute(stage: Stage): TaskResult {
val context = stage.mapTo<RunCanaryContext>()
// The `DeployCanaryServerGroups` stage will deploy a list of experiment/control
// pairs, but we will only canary the first pair in `deployedServerGroups`.
val scopes = stage.context["deployedServerGroups"]?.let {
val pairs = OrcaObjectMapper.newInstance().convertValue<List<DeployedServerGroupPair>>(it)
context.scopes.from(pairs.first())
} ?: context.scopes

val canaryPipelineExecutionId = kayentaService.create(
context.canaryConfigId,
Expand All @@ -54,31 +45,9 @@ class RunKayentaCanaryTask(
context.metricsAccountName,
context.configurationAccountName,
context.storageAccountName,
CanaryExecutionRequest(scopes, context.scoreThresholds)
CanaryExecutionRequest(context.scopes, context.scoreThresholds)
)["canaryExecutionId"] as String

return TaskResult.builder(SUCCEEDED).context("canaryPipelineExecutionId", canaryPipelineExecutionId).build()
}
}

private fun Map<String, CanaryScopes>.from(pair: DeployedServerGroupPair): Map<String, CanaryScopes> {
return entries.associate { (key, scope) ->
key to scope.copy(
controlScope = scope.controlScope.copy(
scope = pair.controlScope,
location = pair.controlLocation
),
experimentScope = scope.experimentScope.copy(
scope = pair.experimentScope,
location = pair.experimentLocation
)
)
}
}

internal data class DeployedServerGroupPair(
val controlLocation: String,
val controlScope: String,
val experimentLocation: String,
val experimentScope: String
)
Original file line number Diff line number Diff line change
Expand Up @@ -335,7 +335,6 @@ object RunCanaryIntervalsStageTest : Spek({
.allMatch { it == attributes }
}
}

}
})

Expand Down Expand Up @@ -379,4 +378,3 @@ val Int.minutesInSeconds: Int

val Long.minutesInSeconds: Long
get() = Duration.ofMinutes(this).seconds

Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,18 @@
package com.netflix.spinnaker.orca.kayenta.tasks

import com.fasterxml.jackson.module.kotlin.convertValue
import com.netflix.spinnaker.orca.clouddriver.MortService
import com.netflix.spinnaker.orca.clouddriver.pipeline.servergroup.CreateServerGroupStage
import com.netflix.spinnaker.orca.fixture.pipeline
import com.netflix.spinnaker.orca.fixture.stage
import com.netflix.spinnaker.orca.jackson.OrcaObjectMapper
import com.netflix.spinnaker.orca.kato.pipeline.ParallelDeployStage
import com.netflix.spinnaker.orca.kayenta.CanaryScopes
import com.netflix.spinnaker.orca.kayenta.pipeline.DeployCanaryServerGroupsStage
import com.netflix.spinnaker.orca.kayenta.pipeline.DeployCanaryServerGroupsStage.Companion.DEPLOY_CONTROL_SERVER_GROUPS
import com.netflix.spinnaker.orca.kayenta.pipeline.DeployCanaryServerGroupsStage.Companion.DEPLOY_EXPERIMENT_SERVER_GROUPS
import com.nhaarman.mockito_kotlin.doReturn
import com.nhaarman.mockito_kotlin.mock
import org.assertj.core.api.Assertions.assertThat
import org.assertj.core.api.Assertions.fail
import org.jetbrains.spek.api.Spek
Expand All @@ -33,8 +37,12 @@ import org.jetbrains.spek.api.dsl.it


object PropagateDeployedServerGroupScopesTest : Spek({
val mort = mock<MortService> {
on { getAccountDetails("foo") } doReturn mapOf("accountId" to "abc123")
on { getAccountDetails("bar") } doReturn mapOf("accountId" to "def456")
}

val subject = PropagateDeployedServerGroupScopes()
val subject = PropagateDeployedServerGroupScopes(mort)
val objectMapper = OrcaObjectMapper.newInstance()

given("upstream experiment and control deploy stages") {
Expand All @@ -52,13 +60,15 @@ object PropagateDeployedServerGroupScopesTest : Spek({
context["deploy.server.groups"] = mapOf(
"us-central1" to listOf("app-control-a-v000")
)
context["deploy.account.name"] = "foo"
}

stage {
type = CreateServerGroupStage.PIPELINE_CONFIG_TYPE
context["deploy.server.groups"] = mapOf(
"us-central1" to listOf("app-control-b-v000")
)
context["deploy.account.name"] = "bar"
}
}

Expand All @@ -71,13 +81,15 @@ object PropagateDeployedServerGroupScopesTest : Spek({
context["deploy.server.groups"] = mapOf(
"us-central1" to listOf("app-experiment-a-v000")
)
context["deploy.account.name"] = "foo"
}

stage {
type = CreateServerGroupStage.PIPELINE_CONFIG_TYPE
context["deploy.server.groups"] = mapOf(
"us-central1" to listOf("app-experiment-b-v000")
)
context["deploy.account.name"] = "bar"
}
}
}
Expand All @@ -88,20 +100,33 @@ object PropagateDeployedServerGroupScopesTest : Spek({
objectMapper.convertValue<List<DeployedServerGroupPair>>(pairs).let {
assertThat(it).containsExactlyInAnyOrder(
DeployedServerGroupPair(
experimentAccountId = "abc123",
experimentScope = "app-experiment-a-v000",
experimentLocation = "us-central1",
controlAccountId = "abc123",
controlScope = "app-control-a-v000",
controlLocation = "us-central1"
),
DeployedServerGroupPair(
experimentAccountId = "def456",
experimentScope = "app-experiment-b-v000",
experimentLocation = "us-central1",
controlScope = "app-control-b-v000",
controlLocation = "us-central1"
controlLocation = "us-central1",
controlAccountId = "def456"
)
)
}
}
} ?: fail("Task should output `deployedServerGroups`")
}
})

internal data class DeployedServerGroupPair(
val controlLocation: String,
val controlScope: String,
val controlAccountId: String?,
val experimentLocation: String,
val experimentScope: String,
val experimentAccountId: String?
)
Loading