All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.netflix.spinnaker.keel.actuation.EnvironmentPromotionChecker.kt Maven / Gradle / Ivy

package com.netflix.spinnaker.keel.actuation

import com.netflix.spinnaker.keel.actuation.EnvironmentConstraintRunner.EnvironmentContext
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.ConstraintStatus.PASS
import com.netflix.spinnaker.keel.core.api.EnvironmentArtifactVetoes
import com.netflix.spinnaker.keel.core.api.PinnedEnvironment
import com.netflix.spinnaker.keel.persistence.KeelRepository
import com.netflix.spinnaker.keel.telemetry.ArtifactVersionApproved
import com.netflix.spinnaker.keel.telemetry.EnvironmentCheckComplete
import com.newrelic.api.agent.Trace
import org.slf4j.LoggerFactory
import org.springframework.context.ApplicationEventPublisher
import org.springframework.stereotype.Component
import java.time.Clock
import java.time.Duration

/**
 * This class is responsible for approving artifacts in environments
 */
@Component
class EnvironmentPromotionChecker(
  private val repository: KeelRepository,
  private val constraintRunner: EnvironmentConstraintRunner,
  private val publisher: ApplicationEventPublisher,
  private val clock: Clock = Clock.systemDefaultZone()
) {
  private val log by lazy { LoggerFactory.getLogger(javaClass) }

  @Trace(dispatcher = true)
  suspend fun checkEnvironments(deliveryConfig: DeliveryConfig) {
    val startTime = clock.instant()
    try {
      val pinnedEnvs: Map = repository
        .pinnedEnvironments(deliveryConfig)
        .associateBy { envPinKey(it.targetEnvironment, it.artifact) }

      val vetoedArtifacts: Map = repository
        .vetoedEnvironmentVersions(deliveryConfig)
        .associateBy { envPinKey(it.targetEnvironment, it.artifact) }

      deliveryConfig
        .artifacts
        .associateWith { repository.artifactVersions(it) }
        .forEach { (artifact, versions) ->
          if (versions.isEmpty()) {
            log.warn("No versions for ${artifact.type} artifact name ${artifact.name} and reference ${artifact.reference} are known")
          } else {
            deliveryConfig.environments.forEach { environment ->
              if (artifact.isUsedIn(environment)) {
                val envContext = EnvironmentContext(
                  deliveryConfig = deliveryConfig,
                  environment = environment,
                  artifact = artifact,
                  versions = versions.map { it.version },
                  vetoedVersions = (vetoedArtifacts[envPinKey(environment.name, artifact)]?.versions)
                    ?: emptySet()
                )

                if (pinnedEnvs.hasPinFor(environment.name, artifact)) {
                  val pinnedVersion = pinnedEnvs.versionFor(environment.name, artifact)
                  // approve version first to fast track deployment
                  approveVersion(deliveryConfig, artifact, pinnedVersion!!, environment)
                  // then evaluate constraints
                  constraintRunner.checkEnvironment(envContext)
                } else {
                  constraintRunner.checkEnvironment(envContext)

                  // everything the constraint runner has already approved
                  val queuedForApproval = repository
                    .getArtifactVersionsQueuedForApproval(deliveryConfig.name, environment.name, artifact)
                    .toMutableList()

                  /**
                   * Approve all constraints starting with oldest first so that the ordering is
                   * maintained.
                   */
                  queuedForApproval
                    .reversed()
                    .forEach { artifactVersion ->
                      /**
                       * We don't need to re-invoke stateful constraint evaluators for these, but we still
                       * check stateless constraints to avoid approval outside of allowed-times.
                       */
                      log.debug(
                        "Version ${artifactVersion.version} of artifact ${artifact.name} is queued for approval, " +
                          "and being evaluated for stateless constraints in environment ${environment.name}"
                      )
                      if (constraintRunner.checkStatelessConstraints(artifact, deliveryConfig, artifactVersion.version, environment)) {
                        approveVersion(deliveryConfig, artifact, artifactVersion.version, environment)
                        repository.deleteArtifactVersionQueuedForApproval(
                          deliveryConfig.name, environment.name, artifact, artifactVersion.version)
                      } else {
                        log.debug("Version ${artifactVersion.version} of $artifact does not currently pass stateless constraints")
                        queuedForApproval.remove(artifactVersion)
                      }
                    }

                  val versionSelected = queuedForApproval.firstOrNull()
                  if (versionSelected == null) {
                    log.warn("No version of {} passes constraints for environment {}", artifact, environment.name)
                  }
                }
              } else {
                log.debug("Skipping checks for {} as it is not used in environment {}", artifact, environment.name)
              }
            }
          }
        }
    } finally {
      publisher.publishEvent(
        EnvironmentCheckComplete(
          application = deliveryConfig.application,
          deliveryConfigName = deliveryConfig.name,
          duration = Duration.between(startTime, clock.instant())
        )
      )
    }
  }

  private fun approveVersion(
    deliveryConfig: DeliveryConfig,
    artifact: DeliveryArtifact,
    version: String,
    targetEnvironment: Environment
  ) {
    log.debug("Approving version $version of ${artifact.type} artifact ${artifact.name} in environment ${targetEnvironment.name}")
    val isNewVersion = repository
      .approveVersionFor(deliveryConfig, artifact, version, targetEnvironment.name)
    if (isNewVersion) {
      log.info(
        "Approved {} {} version {} for {} environment {} in {}",
        artifact.name,
        artifact.type,
        version,
        deliveryConfig.name,
        targetEnvironment.name,
        deliveryConfig.application
      )

      publisher.publishEvent(
        ArtifactVersionApproved(
          deliveryConfig.application,
          deliveryConfig.name,
          targetEnvironment.name,
          artifact.name,
          artifact.type,
          version
        )
      )

      // persist the status of stateless constraints because their current value is all we care about
      snapshotStatelessConstraintStatus(deliveryConfig, artifact, version, targetEnvironment)

      // recheck all resources in an environment, so action can be taken right away
      repository.triggerResourceRecheck(targetEnvironment.name, deliveryConfig.application)
    }
  }

  /**
   * Save the passing status of all stateless constraints when a version is approved so that
   * their status stays the same forever. We don't want them to be evaluated anymore.
   */
  private fun snapshotStatelessConstraintStatus(
    deliveryConfig: DeliveryConfig,
    artifact: DeliveryArtifact,
    version: String,
    environment: Environment
  ) {
    constraintRunner.getStatelessConstraintSnapshots(
      artifact = artifact,
      deliveryConfig = deliveryConfig,
      version = version,
      environment = environment,
      currentStatus = PASS // We just checked that all these pass since a version was approved
    ).forEach { constraintState ->
      log.debug("Storing final constraint state snapshot for {} constraint for {} version {} for {} environment {} in {}",
        constraintState.type,
        artifact,
        version,
        deliveryConfig.name,
        environment.name,
        deliveryConfig.application
      )
      repository.storeConstraintState(constraintState)
    }

  }

  private fun Map.hasPinFor(
    environmentName: String,
    artifact: DeliveryArtifact
  ): Boolean {
    if (isEmpty()) {
      return false
    }

    val key = envPinKey(environmentName, artifact)
    return containsKey(key) && checkNotNull(get(key)).artifact == artifact
  }

  private fun Map.versionFor(
    environmentName: String,
    artifact: DeliveryArtifact
  ): String? =
    get(envPinKey(environmentName, artifact))?.version

  fun envPinKey(environmentName: String, artifact: DeliveryArtifact): String =
    "$environmentName:${artifact.reference}"
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy