Skip to content

Commit

Permalink
Delay promotion if verifications are still running (#1834)
Browse files Browse the repository at this point in the history
* feat(verifications): query to find pending verifications for an environment

* feat(verifications): implicit contraint to delay promotion until any pending verifications complete

* feat(verifications): check for running verifications for previous versions before launching

* feat(verifications): make sure we update the state of any prior version verifications

* chore(verifications): clarify some things

* fix(verifications): fix a stupid logic bug
  • Loading branch information
robfletcher authored Mar 4, 2021
1 parent 65151e4 commit a2db125
Show file tree
Hide file tree
Showing 8 changed files with 367 additions and 8 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,6 @@ interface VerificationRepository {
context: VerificationContext,
) : Map<String, VerificationState>


/**
* Query the repository for the states of multiple contexts.
*
Expand Down Expand Up @@ -56,6 +55,14 @@ interface VerificationRepository {
metadata: Map<String, Any?> = emptyMap()
)

/**
* Returns any pending verifications for the specified environment.
*/
fun pendingInEnvironment(
deliveryConfig: DeliveryConfig,
environmentName: String
) : Collection<PendingVerification>

fun nextEnvironmentsForVerification(minTimeSinceLastCheck: Duration, limit: Int) : Collection<VerificationContext>
}

Expand Down Expand Up @@ -88,3 +95,9 @@ data class VerificationContext(

fun verification(id: String): Verification? = verifications.firstOrNull { it.id == id }
}

data class PendingVerification(
val context: VerificationContext,
val verification: Verification,
val state: VerificationState
)
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,11 @@ import com.netflix.spinnaker.keel.api.Environment
import com.netflix.spinnaker.keel.api.Verification
import com.netflix.spinnaker.keel.api.constraints.ConstraintStatus
import com.netflix.spinnaker.keel.api.constraints.ConstraintStatus.FAIL
import com.netflix.spinnaker.keel.api.constraints.ConstraintStatus.NOT_EVALUATED
import com.netflix.spinnaker.keel.api.constraints.ConstraintStatus.OVERRIDE_PASS
import com.netflix.spinnaker.keel.api.constraints.ConstraintStatus.PASS
import com.netflix.spinnaker.keel.api.constraints.ConstraintStatus.PENDING
import com.netflix.spinnaker.keel.api.verification.PendingVerification
import com.netflix.spinnaker.keel.api.verification.VerificationContext
import com.netflix.spinnaker.keel.api.verification.VerificationRepository
import com.netflix.spinnaker.keel.api.verification.VerificationState
Expand All @@ -23,6 +25,7 @@ import strikt.api.expect
import strikt.api.expectCatching
import strikt.api.expectThat
import strikt.assertions.containsExactly
import strikt.assertions.containsExactlyInAnyOrder
import strikt.assertions.containsKey
import strikt.assertions.first
import strikt.assertions.get
Expand All @@ -34,6 +37,7 @@ import strikt.assertions.isNotEmpty
import strikt.assertions.isNotNull
import strikt.assertions.isNull
import strikt.assertions.isSuccess
import strikt.assertions.map
import strikt.assertions.one
import strikt.assertions.withFirst
import java.time.Duration
Expand Down Expand Up @@ -118,6 +122,24 @@ abstract class VerificationRepositoryTests<IMPLEMENTATION : VerificationReposito
.with(VerificationState::metadata) { isEqualTo(metadata) }
}

@Test
fun `pending verifications for an environment can be fetched`() {
context.setup()
val context2 = context.copy(version = "fnord-0.191.0-h379.0b74d6f").also { it.setup() }
val context3 = context.copy(version = "fnord-0.192.0-h380.9361c66").also { it.setup() }

subject.updateState(context, verification, PASS)
subject.updateState(context2, verification, PENDING)
subject.updateState(context3, verification, NOT_EVALUATED)

expectCatching {
subject.pendingInEnvironment(deliveryConfig, context.environmentName)
}
.isSuccess()
.map(PendingVerification::context)
.containsExactlyInAnyOrder(context2, context3)
}

@Test
fun `successive updates do not wipe out metadata`() {
context.setup()
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
package com.netflix.spinnaker.keel.constraints

import com.netflix.spinnaker.keel.api.DeliveryConfig
import com.netflix.spinnaker.keel.api.Environment
import com.netflix.spinnaker.keel.api.artifacts.DeliveryArtifact
import com.netflix.spinnaker.keel.api.constraints.SupportedConstraintType
import com.netflix.spinnaker.keel.api.plugins.ConstraintEvaluator
import com.netflix.spinnaker.keel.api.support.EventPublisher
import com.netflix.spinnaker.keel.api.verification.VerificationRepository
import com.netflix.spinnaker.keel.core.api.PendingVerificationsConstraint
import org.slf4j.LoggerFactory

class PendingVerificationsConstraintEvaluator(
private val verificationRepository: VerificationRepository,
override val eventPublisher: EventPublisher
) : ConstraintEvaluator<PendingVerificationsConstraint> {

override fun isImplicit() = true

override val supportedType =
SupportedConstraintType<PendingVerificationsConstraint>("pending-verifications")

override fun canPromote(
artifact: DeliveryArtifact,
version: String,
deliveryConfig: DeliveryConfig,
targetEnvironment: Environment
): Boolean =
if (targetEnvironment.verifyWith.isEmpty()) {
log.debug("{} / {} has no verifications", deliveryConfig.name, targetEnvironment.name)
true
} else {
val pendingVerifications =
verificationRepository.pendingInEnvironment(deliveryConfig, targetEnvironment.name)
if (pendingVerifications.isNotEmpty()) {
log.info(
"{} / {} is awaiting results from {} before another deployment can proceed",
deliveryConfig.name,
targetEnvironment.name,
pendingVerifications.map { it.verification.id }.joinToString()
)
false
} else {
log.debug(
"{} / {} has no pending verifications",
deliveryConfig.name,
targetEnvironment.name
)
true
}
}

private val log by lazy { LoggerFactory.getLogger(javaClass) }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package com.netflix.spinnaker.keel.core.api

import com.netflix.spinnaker.keel.api.Constraint

class PendingVerificationsConstraint : Constraint("pending-verifications")
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,31 @@ class VerificationRunner(
* particular environment and artifact version.
*/
fun runVerificationsFor(context: VerificationContext) {
if (context.environment.verifyWith.isEmpty()) {
return
}

verificationRepository
.pendingInEnvironment(context.deliveryConfig, context.environmentName)
// only consider other versions, we'll handle verifications for the version in context later
.filterNot { it.context.version == context.version }
// get the latest status by re-evaluating each one (which will update in the database)
.associateWith { it.context.latestStatus(it.verification) }
// filter out things that have now completed (since we last checked)
.filterNot { (_, status) ->
status?.complete ?: false
}
.let { pendingVerifications ->
// if we still have any pending verifications then something is still running for a previous
// version of the artifact -- we should wait
if (pendingVerifications.isNotEmpty()) {
pendingVerifications.forEach { (pendingVerification, status)->
log.debug("Previous verification {} for {} is still {}", pendingVerification.verification.id, pendingVerification.context.version, status)
}
return
}
}

with(context) {
val statuses = environment
.verifyWith
Expand Down Expand Up @@ -66,6 +91,9 @@ class VerificationRunner(
}
}

/**
* `true` if any of the statuses is [PENDING], `false` if none are or the collection is empty.
*/
private val Collection<Pair<*, ConstraintStatus?>>.anyStillRunning: Boolean
get() = any { (_, status) -> status == PENDING }

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
package com.netflix.spinnaker.keel.constraints

import com.netflix.spinnaker.keel.api.DeliveryConfig
import com.netflix.spinnaker.keel.api.Environment
import com.netflix.spinnaker.keel.api.Verification
import com.netflix.spinnaker.keel.api.constraints.ConstraintStatus.PENDING
import com.netflix.spinnaker.keel.api.support.EventPublisher
import com.netflix.spinnaker.keel.api.verification.PendingVerification
import com.netflix.spinnaker.keel.api.verification.VerificationContext
import com.netflix.spinnaker.keel.api.verification.VerificationRepository
import com.netflix.spinnaker.keel.api.verification.VerificationState
import com.netflix.spinnaker.keel.artifacts.DockerArtifact
import io.mockk.every
import io.mockk.mockk
import org.junit.jupiter.api.Test
import strikt.api.expectCatching
import strikt.assertions.isFalse
import strikt.assertions.isSuccess
import strikt.assertions.isTrue
import java.time.Instant.now

internal class PendingVerificationsConstraintEvaluatorTests {

private val artifact = DockerArtifact(
name = "fnord",
reference = "fnord"
)

private val environmentWithNoVerifications = Environment(
name = "test"
)
private val verification = DummyVerification("1")
private val environmentWithVerification =
environmentWithNoVerifications.copy(verifyWith = listOf(verification))

private val deliveryConfig = DeliveryConfig(
application = "fnord",
name = "fnord-manifest",
serviceAccount = "[email protected]",
artifacts = setOf(artifact),
environments = setOf(environmentWithNoVerifications)
)

private val verificationRepository = mockk<VerificationRepository>()
private val eventPublisher = mockk<EventPublisher>()

private val subject = PendingVerificationsConstraintEvaluator(verificationRepository, eventPublisher)

@Test
fun `can promote if the environment has no verifications`() {
expectCatching {
subject.canPromote(artifact, "v1", deliveryConfig, environmentWithNoVerifications)
}
.isSuccess()
.isTrue()
}

@Test
fun `can promote if there are no verifications currently running`() {
with(deliveryConfig.copy(environments = setOf(environmentWithVerification))) {
every { verificationRepository.pendingInEnvironment(any(), any()) } returns emptyList()

expectCatching {
subject.canPromote(artifact, "v1", this, environmentWithVerification)
}
.isSuccess()
.isTrue()
}
}

@Test
fun `cannot promote if there are any verifications currently running`() {
with(deliveryConfig.copy(environments = setOf(environmentWithVerification))) {
every { verificationRepository.pendingInEnvironment(any(), any()) } returns listOf(
PendingVerification(
VerificationContext(this, environmentWithVerification.name, artifact.reference, "v1"),
verification,
VerificationState(PENDING, now(), null)
)
)

expectCatching {
subject.canPromote(artifact, "v1", this, environmentWithVerification)
}
.isSuccess()
.isFalse()
}
}

private data class DummyVerification(override val id: String) : Verification {
override val type: String = "dummy"
}
}
Loading

0 comments on commit a2db125

Please sign in to comment.