Skip to content

Commit

Permalink
feat(plugins): Add callback API for constraint state changes (#1915)
Browse files Browse the repository at this point in the history
Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com>
  • Loading branch information
luispollo and mergify[bot] authored Apr 12, 2021
1 parent 43f6199 commit acf3857
Show file tree
Hide file tree
Showing 5 changed files with 129 additions and 19 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -91,4 +91,13 @@ interface StatefulConstraintEvaluator<T : Constraint, A : ConstraintStateAttribu
constraint: T,
state: ConstraintState
): Boolean

/**
* Callback API for [ConstraintStateChanged]. Override this method if your [StatefulConstraintEvaluator] needs
* to take action when a supported constraint's state changes.
*/
@JvmDefault
fun onConstraintStateChanged(event: ConstraintStateChanged) {
// default implementation is a no-op
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,13 +25,19 @@ import com.netflix.spinnaker.keel.api.support.EventPublisher
import com.netflix.spinnaker.kork.plugins.api.internal.SpinnakerExtensionPoint

/**
* TODO: Docs
* A [ConstraintEvaluator] is a Keel plugin that implements the handling of a specific type
* of environment promotion [Constraint].
*
* Constraint evaluators can be stateless, for constraints that require re-evaluation every time
* an environment promotion is considered, such as a dependency on a successful deployment in a
* previous environment in a sequence, or stateful, when the constraint requires storing and checking
* state, for example a manual approval (where the approver and the time of approval would be recorded).
*/
interface ConstraintEvaluator<T : Constraint> : SpinnakerExtensionPoint {

companion object {
/**
* TODO: Docs
* @return The constraint of the type supported by this evaluator within the specified target environment.
*/
fun <T> getConstraintForEnvironment(
deliveryConfig: DeliveryConfig,
Expand All @@ -51,13 +57,10 @@ interface ConstraintEvaluator<T : Constraint> : SpinnakerExtensionPoint {
}

/**
* TODO: Docs
* The supported constraint type mapping for this evaluator.
*/
val supportedType: SupportedConstraintType<T>

/**
* TODO: Docs
*/
val eventPublisher: EventPublisher

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package com.netflix.spinnaker.keel.events

import com.netflix.spinnaker.keel.api.Constraint
import com.netflix.spinnaker.keel.api.constraints.StatefulConstraintEvaluator
import com.netflix.spinnaker.keel.api.events.ConstraintStateChanged
import org.springframework.context.event.EventListener
import org.springframework.stereotype.Component

/**
* An event listener that listens to [ConstraintStateChanged] events and relays them to the
* corresponding [StatefulConstraintEvaluator] for processing.
*
* Note: in theory, constraint evaluators should be able to subscribe to these events directly,
* but we've found that Spring auto-wiring does not seem to work for external plugins, so this
* provides an alternative.
*/
@Component
class ConstraintStateChangedRelay(
private val statefulEvaluators: List<StatefulConstraintEvaluator<*, *>>
) {
@EventListener(ConstraintStateChanged::class)
fun onConstraintStateChanged(event: ConstraintStateChanged) {
statefulEvaluators.supporting(event.constraint)?.onConstraintStateChanged(event)
}

private fun List<StatefulConstraintEvaluator<*, *>>.supporting(constraint: Constraint) =
this.firstOrNull { it.supportedType.name == constraint.type }
}
Original file line number Diff line number Diff line change
Expand Up @@ -436,8 +436,8 @@ class ApplicationService(
}

private fun getVerificationStates(
deliveryConfig: DeliveryConfig,
artifactVersions: List<PublishedArtifact>
deliveryConfig: DeliveryConfig,
artifactVersions: List<PublishedArtifact>
) =
if (verificationsEnabled) {
repository.getVerificationStates(deliveryConfig, artifactVersions)
Expand Down Expand Up @@ -546,14 +546,14 @@ class ApplicationService(
): PublishedArtifact? {
// there can only be one pinned version
val pinnedVersion = context.artifactSummariesInEnv.firstOrNull { it.pinned != null }?.version
return if (pinnedVersion != version) {
pinnedVersion?.let {
context.allVersions.find { it.version == pinnedVersion }
}
} else { //if pinnedVersion == current version, fetch the version which the pinned version replaced
val chosenVersion = context.artifactInfoInEnvironment.find { it.replacedByVersion == pinnedVersion && it.status == PREVIOUS }?.version
context.allVersions.find { it.version == chosenVersion }
}
return if (pinnedVersion != version) {
pinnedVersion?.let {
context.allVersions.find { it.version == pinnedVersion }
}
} else { //if pinnedVersion == current version, fetch the version which the pinned version replaced
val chosenVersion = context.artifactInfoInEnvironment.find { it.replacedByVersion == pinnedVersion && it.status == PREVIOUS }?.version
context.allVersions.find { it.version == chosenVersion }
}
}

/**
Expand All @@ -569,9 +569,9 @@ class ApplicationService(
*/
private fun ArtifactSummaryInEnvironment.addConstraintSummaries(
deliveryConfig: DeliveryConfig,
environment: Environment,
version: String,
artifact: DeliveryArtifact
environment: Environment,
version: String,
artifact: DeliveryArtifact
): ArtifactSummaryInEnvironment {
val persistedStates = repository
.constraintStateFor(deliveryConfig.name, environment.name, version, artifact.reference)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
package com.netflix.spinnaker.keel.events

import com.netflix.spinnaker.keel.api.Environment
import com.netflix.spinnaker.keel.api.constraints.ConstraintState
import com.netflix.spinnaker.keel.api.constraints.ConstraintStatus.OVERRIDE_PASS
import com.netflix.spinnaker.keel.api.constraints.DefaultConstraintAttributes
import com.netflix.spinnaker.keel.api.constraints.StatefulConstraintEvaluator
import com.netflix.spinnaker.keel.api.constraints.SupportedConstraintType
import com.netflix.spinnaker.keel.api.events.ConstraintStateChanged
import com.netflix.spinnaker.keel.constraints.StatefulConstraintEvaluatorTests.Fixture.FakeConstraint
import dev.minutest.junit.JUnit5Minutests
import dev.minutest.rootContext
import io.mockk.every
import io.mockk.just
import io.mockk.mockk
import io.mockk.runs
import io.mockk.verify

class ConstraintStateChangedRelayTests : JUnit5Minutests {
object Fixture {
private val constraint = FakeConstraint()

val event = ConstraintStateChanged(
environment = Environment(
name = "test",
constraints = setOf(constraint)
),
constraint = constraint,
previousState = null,
currentState = ConstraintState(
deliveryConfigName = "myconfig",
environmentName = "test",
artifactReference = "whatever",
artifactVersion = "whatever",
status = OVERRIDE_PASS,
type = constraint.type
)
)

internal val matchingEvaluator = mockk<StatefulConstraintEvaluator<FakeConstraint, DefaultConstraintAttributes>>() {
every { supportedType } returns SupportedConstraintType("fake", FakeConstraint::class.java)
every { onConstraintStateChanged(event) } just runs
}

internal val nonMatchingEvaluator = mockk<StatefulConstraintEvaluator<FakeConstraint, DefaultConstraintAttributes>>() {
every { supportedType } returns SupportedConstraintType("the-wrong-fake", FakeConstraint::class.java)
}

val subject = ConstraintStateChangedRelay(listOf(matchingEvaluator, nonMatchingEvaluator))
}

fun tests() = rootContext<Fixture> {
context("a stateful constraint state changed event is emitted") {
fixture { Fixture }

before {
subject.onConstraintStateChanged(event)
}

test("the event is relayed to the correct constraint evaluator") {
verify(exactly = 1) {
matchingEvaluator.onConstraintStateChanged(event)
}
verify(exactly = 0) {
nonMatchingEvaluator.onConstraintStateChanged(event)
}
}
}
}
}

0 comments on commit acf3857

Please sign in to comment.