Skip to content

Commit

Permalink
feat(Kayenta): Support canaryConfigName as canary identifier (#3215)
Browse files Browse the repository at this point in the history
- Allow canaries to be specified via canaryConfigName, in addition to
  canaryConfigId.
- canaryConfigId is still the default, and if it is provided, no
  additional work is necessary.
- if canaryConfigId is null, then if a canaryConfigName is provided, it
  can be used, together with the currentApplication name, to resolve a
  valid canaryConfigId using the Kayenta `/canaryConfig` end point.
- Once a list of candidate canaryConfigIds is produced, an additional
  check is performed to ensure only one candidate was returned. If no
  candidates were found, an error is thrown, and likewise if more than
  one candidate matches on both canaryConfigName and the current
  application name, an error is thrown.
  • Loading branch information
nisanharamati committed Oct 4, 2019
1 parent 109c3b2 commit 3d9eaf5
Show file tree
Hide file tree
Showing 7 changed files with 64 additions and 2 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,9 @@ interface KayentaService {

@GET("/credentials")
fun getCredentials(): List<KayentaCredential>

@GET("/canaryConfig")
fun getAllCanaryConfigs(): List<KayentaCanaryConfig>
}

data class CanaryExecutionRequest(
Expand Down Expand Up @@ -130,3 +133,11 @@ data class KayentaCredential(
val name: String,
val type: String
)

data class KayentaCanaryConfig(
val id: String,
val name: String,
val updatedTimestamp: Long?,
val updateTimestampIso: String?,
val applications: List<String>
)
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,8 @@ data class KayentaCanaryContext(
val metricsAccountName: String? = null,
val configurationAccountName: String? = null,
val storageAccountName: String? = null,
val canaryConfigId: String,
val canaryConfigId: String? = null,
val canaryConfigName: String? = null,
val scopes: List<CanaryConfigScope> = emptyList(),
val scoreThresholds: Thresholds = Thresholds(pass = 75, marginal = 50),
val siteLocal: Map<String, Any> = emptyMap(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,8 @@ internal data class RunCanaryContext(
val metricsAccountName: String?,
val configurationAccountName: String?,
val storageAccountName: String?,
val canaryConfigId: String,
val canaryConfigId: String?,
val canaryConfigName: String?,
val scopes: Map<String, CanaryScopes> = emptyMap(),
val scoreThresholds: Thresholds,
val siteLocal: Map<String, Any> = emptyMap()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,7 @@ class RunCanaryIntervalsStage(private val clock: Clock) : StageDefinitionBuilder
canaryConfig.configurationAccountName,
canaryConfig.storageAccountName,
canaryConfig.canaryConfigId,
canaryConfig.canaryConfigName,
buildRequestScopes(canaryConfig, getDeployDetails(parent), i, canaryAnalysisInterval),
canaryConfig.scoreThresholds,
canaryConfig.siteLocal
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,13 @@
package com.netflix.spinnaker.orca.kayenta.pipeline

import com.netflix.spinnaker.orca.CancellableStage
import com.netflix.spinnaker.orca.ext.mapTo
import com.netflix.spinnaker.orca.ext.withTask
import com.netflix.spinnaker.orca.kayenta.KayentaService
import com.netflix.spinnaker.orca.kayenta.model.RunCanaryContext
import com.netflix.spinnaker.orca.kayenta.tasks.MonitorKayentaCanaryTask
import com.netflix.spinnaker.orca.kayenta.tasks.RunKayentaCanaryTask
import com.netflix.spinnaker.orca.kayenta.tasks.ResolveKayentaConfigIdTask
import com.netflix.spinnaker.orca.pipeline.StageDefinitionBuilder
import com.netflix.spinnaker.orca.pipeline.TaskNode
import com.netflix.spinnaker.orca.pipeline.model.Stage
Expand All @@ -37,6 +40,13 @@ class RunCanaryPipelineStage(
private val log = LoggerFactory.getLogger(javaClass)

override fun taskGraph(stage: Stage, builder: TaskNode.Builder) {
val context = stage.mapTo<RunCanaryContext>()
if (context.canaryConfigId.isNullOrEmpty()) {
if (context.canaryConfigName.isNullOrEmpty()) {
throw IllegalArgumentException("Canary config must be specified as either UUID or name string")
}
builder.withTask<ResolveKayentaConfigIdTask>("resolveKayentaConfigId")
}
builder
.withTask<RunKayentaCanaryTask>("runCanary")
.withTask<MonitorKayentaCanaryTask>("monitorCanary")
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package com.netflix.spinnaker.orca.kayenta.tasks

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.kayenta.KayentaService
import com.netflix.spinnaker.orca.pipeline.model.Stage
import org.slf4j.LoggerFactory
import org.springframework.stereotype.Component

@Component
class ResolveKayentaConfigIdTask(
private val kayentaService: KayentaService
) : Task {

private val log = LoggerFactory.getLogger(javaClass)

override fun execute(stage: Stage): TaskResult {
val configName = stage.mapTo<String?>("/canaryConfigName")
val currentApplication = stage.execution.application
val canaryConfigList = kayentaService.getAllCanaryConfigs()
val candidates = canaryConfigList.asSequence()
.filter { it.name == configName && it.applications.contains(currentApplication) }
.toList()

if (candidates.size == 0) {
throw NoSuchElementException("Couldn't find a configId for configName $configName and application $currentApplication")
} else if (candidates.size > 1) {
throw IllegalArgumentException("Found more than one configId for configName $configName and application $currentApplication")
}
return TaskResult.builder(SUCCEEDED).context("canaryConfigId", candidates[0].id).build()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,10 @@ class RunKayentaCanaryTask(
override fun execute(stage: Stage): TaskResult {
val context = stage.mapTo<RunCanaryContext>()

if (context.canaryConfigId.isNullOrEmpty()) {
throw IllegalArgumentException("Invalid canaryConfigId. Was the correct configName or configId used?")
}

val canaryPipelineExecutionId = kayentaService.create(
context.canaryConfigId,
stage.execution.application,
Expand Down

0 comments on commit 3d9eaf5

Please sign in to comment.