Please wait. This can take some minutes ...
Many resources are needed to download a project. Please understand that we have to compensate our server costs. Thank you in advance.
Project price only 1 $
You can buy this project and download/modify it how often you want.
com.netflix.spinnaker.keel.sql.SqlArtifactRepository.kt Maven / Gradle / Ivy
package com.netflix.spinnaker.keel.sql
import com.fasterxml.jackson.databind.ObjectMapper
import com.fasterxml.jackson.module.kotlin.convertValue
import com.fasterxml.jackson.module.kotlin.readValue
import com.netflix.spinnaker.keel.api.DeliveryConfig
import com.netflix.spinnaker.keel.api.Environment
import com.netflix.spinnaker.keel.api.artifacts.ArtifactMetadata
import com.netflix.spinnaker.keel.api.artifacts.ArtifactStatus
import com.netflix.spinnaker.keel.api.artifacts.ArtifactType
import com.netflix.spinnaker.keel.api.artifacts.DeliveryArtifact
import com.netflix.spinnaker.keel.api.artifacts.PublishedArtifact
import com.netflix.spinnaker.keel.api.plugins.ArtifactSupplier
import com.netflix.spinnaker.keel.api.plugins.supporting
import com.netflix.spinnaker.keel.artifacts.DockerArtifact
import com.netflix.spinnaker.keel.core.api.ActionMetadata
import com.netflix.spinnaker.keel.core.api.ArtifactSummaryInEnvironment
import com.netflix.spinnaker.keel.core.api.ArtifactVersionStatus
import com.netflix.spinnaker.keel.core.api.ArtifactVersionVetoData
import com.netflix.spinnaker.keel.core.api.ArtifactVersions
import com.netflix.spinnaker.keel.core.api.EnvironmentArtifactPin
import com.netflix.spinnaker.keel.core.api.EnvironmentArtifactVeto
import com.netflix.spinnaker.keel.core.api.EnvironmentArtifactVetoes
import com.netflix.spinnaker.keel.core.api.EnvironmentSummary
import com.netflix.spinnaker.keel.core.api.PinnedEnvironment
import com.netflix.spinnaker.keel.core.api.PromotionStatus
import com.netflix.spinnaker.keel.core.api.PromotionStatus.APPROVED
import com.netflix.spinnaker.keel.core.api.PromotionStatus.CURRENT
import com.netflix.spinnaker.keel.core.api.PromotionStatus.DEPLOYING
import com.netflix.spinnaker.keel.core.api.PromotionStatus.PENDING
import com.netflix.spinnaker.keel.core.api.PromotionStatus.PREVIOUS
import com.netflix.spinnaker.keel.core.api.PromotionStatus.SKIPPED
import com.netflix.spinnaker.keel.core.api.PromotionStatus.VETOED
import com.netflix.spinnaker.keel.core.api.PublishedArtifactInEnvironment
import com.netflix.spinnaker.keel.core.api.randomUID
import com.netflix.spinnaker.keel.persistence.ArtifactNotFoundException
import com.netflix.spinnaker.keel.persistence.ArtifactRepository
import com.netflix.spinnaker.keel.persistence.NoSuchArtifactException
import com.netflix.spinnaker.keel.persistence.NoSuchArtifactVersionException
import com.netflix.spinnaker.keel.persistence.metamodel.Tables.ACTIVE_ENVIRONMENT
import com.netflix.spinnaker.keel.persistence.metamodel.Tables.ARTIFACT_LAST_CHECKED
import com.netflix.spinnaker.keel.persistence.metamodel.Tables.ARTIFACT_VERSIONS
import com.netflix.spinnaker.keel.persistence.metamodel.Tables.DELIVERY_ARTIFACT
import com.netflix.spinnaker.keel.persistence.metamodel.Tables.DELIVERY_CONFIG
import com.netflix.spinnaker.keel.persistence.metamodel.Tables.ENVIRONMENT
import com.netflix.spinnaker.keel.persistence.metamodel.Tables.ENVIRONMENT_ARTIFACT_PIN
import com.netflix.spinnaker.keel.persistence.metamodel.Tables.ENVIRONMENT_ARTIFACT_VERSIONS
import com.netflix.spinnaker.keel.persistence.metamodel.Tables.ENVIRONMENT_ARTIFACT_VETO
import com.netflix.spinnaker.keel.services.StatusInfoForArtifactInEnvironment
import com.netflix.spinnaker.keel.sql.RetryCategory.READ
import com.netflix.spinnaker.keel.sql.RetryCategory.WRITE
import com.netflix.spinnaker.keel.telemetry.AboutToBeChecked
import io.github.resilience4j.retry.Retry
import io.github.resilience4j.retry.RetryConfig
import org.jooq.DSLContext
import org.jooq.Record1
import org.jooq.Select
import org.jooq.SelectConditionStep
import org.jooq.impl.DSL
import org.jooq.impl.DSL.select
import org.jooq.impl.DSL.selectOne
import org.jooq.util.mysql.MySQLDSL
import org.slf4j.LoggerFactory
import org.springframework.context.ApplicationEventPublisher
import java.security.MessageDigest
import java.time.Clock
import java.time.Duration
import java.time.Instant
import java.time.Instant.EPOCH
import java.time.temporal.TemporalAccessor
import javax.xml.bind.DatatypeConverter
class SqlArtifactRepository(
private val jooq: DSLContext,
private val clock: Clock,
private val objectMapper: ObjectMapper,
private val sqlRetry: SqlRetry,
private val artifactSuppliers: List> = emptyList(),
private val publisher: ApplicationEventPublisher
) : ArtifactRepository {
override fun register(artifact: DeliveryArtifact) {
val id: String = (
sqlRetry.withRetry(READ) {
jooq
.select(DELIVERY_ARTIFACT.UID)
.from(DELIVERY_ARTIFACT)
.where(
DELIVERY_ARTIFACT.TYPE.eq(artifact.type)
.and(DELIVERY_ARTIFACT.DELIVERY_CONFIG_NAME.eq(artifact.deliveryConfigName))
.and(DELIVERY_ARTIFACT.REFERENCE.eq(artifact.reference))
)
.fetchOne(DELIVERY_ARTIFACT.UID)
}
?: randomUID().toString()
)
sqlRetry.withRetry(WRITE) {
jooq.insertInto(DELIVERY_ARTIFACT)
.set(DELIVERY_ARTIFACT.UID, id)
.set(DELIVERY_ARTIFACT.FINGERPRINT, artifact.fingerprint())
.set(DELIVERY_ARTIFACT.NAME, artifact.name)
.set(DELIVERY_ARTIFACT.TYPE, artifact.type)
.set(DELIVERY_ARTIFACT.REFERENCE, artifact.reference)
.set(DELIVERY_ARTIFACT.DELIVERY_CONFIG_NAME, artifact.deliveryConfigName)
.set(DELIVERY_ARTIFACT.DETAILS, artifact.detailsAsJson())
.onDuplicateKeyUpdate()
.set(DELIVERY_ARTIFACT.NAME, artifact.name)
.set(DELIVERY_ARTIFACT.DETAILS, artifact.detailsAsJson())
.execute()
jooq.insertInto(ARTIFACT_LAST_CHECKED)
.set(ARTIFACT_LAST_CHECKED.ARTIFACT_UID, id)
.set(ARTIFACT_LAST_CHECKED.AT, EPOCH.plusSeconds(1))
.onDuplicateKeyUpdate()
.set(ARTIFACT_LAST_CHECKED.AT, EPOCH.plusSeconds(1))
.execute()
}
}
private fun DeliveryArtifact.detailsAsJson(): String {
val details = objectMapper.convertValue>(this)
.toMutableMap()
// remove all the basic fields that have their own columns; everything else is serialized
// as one json blob in the `details` column
.also {
it.remove("name")
it.remove("deliveryConfigName")
it.remove("type")
it.remove("reference")
}
return objectMapper.writeValueAsString(details)
}
override fun get(name: String, type: ArtifactType, deliveryConfigName: String): List {
return sqlRetry.withRetry(READ) {
jooq
.select(DELIVERY_ARTIFACT.DETAILS, DELIVERY_ARTIFACT.REFERENCE)
.from(DELIVERY_ARTIFACT)
.where(DELIVERY_ARTIFACT.NAME.eq(name))
.and(DELIVERY_ARTIFACT.TYPE.eq(type))
.and(DELIVERY_ARTIFACT.DELIVERY_CONFIG_NAME.eq(deliveryConfigName))
.fetch { (details, reference) ->
mapToArtifact(artifactSuppliers.supporting(type), name, type, details, reference, deliveryConfigName)
}
} ?: throw NoSuchArtifactException(name, type)
}
override fun get(name: String, type: ArtifactType, reference: String, deliveryConfigName: String): DeliveryArtifact {
return sqlRetry.withRetry(READ) {
jooq
.select(DELIVERY_ARTIFACT.DETAILS, DELIVERY_ARTIFACT.REFERENCE)
.from(DELIVERY_ARTIFACT)
.where(DELIVERY_ARTIFACT.NAME.eq(name))
.and(DELIVERY_ARTIFACT.TYPE.eq(type))
.and(DELIVERY_ARTIFACT.DELIVERY_CONFIG_NAME.eq(deliveryConfigName))
.and(DELIVERY_ARTIFACT.REFERENCE.eq(reference))
.fetchOne()
}
?.let { (details, reference) ->
mapToArtifact(artifactSuppliers.supporting(type), name, type, details, reference, deliveryConfigName)
} ?: throw ArtifactNotFoundException(reference, deliveryConfigName)
}
override fun get(deliveryConfigName: String, reference: String): DeliveryArtifact {
return sqlRetry.withRetry(READ) {
jooq
.select(DELIVERY_ARTIFACT.NAME, DELIVERY_ARTIFACT.DETAILS, DELIVERY_ARTIFACT.REFERENCE, DELIVERY_ARTIFACT.TYPE)
.from(DELIVERY_ARTIFACT)
.where(
DELIVERY_ARTIFACT.DELIVERY_CONFIG_NAME.eq(deliveryConfigName),
DELIVERY_ARTIFACT.REFERENCE.eq(reference)
)
.fetchOne()
}
?.let { (name, details, reference, type) ->
mapToArtifact(artifactSuppliers.supporting(type), name, type, details, reference, deliveryConfigName)
} ?: throw ArtifactNotFoundException(reference, deliveryConfigName)
}
override fun delete(artifact: DeliveryArtifact) {
requireNotNull(artifact.deliveryConfigName) { "Error removing artifact - it has no delivery config!" }
jooq.transaction { config ->
val txn = DSL.using(config)
txn.deleteFrom(DELIVERY_ARTIFACT)
.where(DELIVERY_ARTIFACT.UID.eq(artifact.uid))
.execute()
}
}
override fun isRegistered(name: String, type: ArtifactType): Boolean =
sqlRetry.withRetry(READ) {
jooq
.selectCount()
.from(DELIVERY_ARTIFACT)
.where(DELIVERY_ARTIFACT.NAME.eq(name))
.and(DELIVERY_ARTIFACT.TYPE.eq(type))
.fetchSingle()
.value1()
} > 0
override fun getAll(type: ArtifactType?, name: String?): List =
sqlRetry.withRetry(READ) {
jooq
.select(DELIVERY_ARTIFACT.NAME, DELIVERY_ARTIFACT.TYPE, DELIVERY_ARTIFACT.DETAILS, DELIVERY_ARTIFACT.REFERENCE, DELIVERY_ARTIFACT.DELIVERY_CONFIG_NAME)
.from(DELIVERY_ARTIFACT)
.apply { if (type != null) where(DELIVERY_ARTIFACT.TYPE.eq(type.toString())) }
.apply { if (name != null) where(DELIVERY_ARTIFACT.NAME.eq(name)) }
.fetch { (name, storedType, details, reference, configName) ->
mapToArtifact(artifactSuppliers.supporting(storedType), name, storedType.toLowerCase(), details, reference, configName)
}
}
override fun versions(artifact: DeliveryArtifact, limit: Int): List {
val retry = Retry.of(
"artifact registered",
RetryConfig.custom()
// retry a couple times since this is invoked from ArtifactListener and there's a race
// condition with persisting the delivery config on upsert.
.maxAttempts(5)
.waitDuration(Duration.ofMillis(100))
.retryOnResult { registered ->
if (!registered) {
log.debug("Retrying registered check for $artifact")
true
} else {
false
}
}
.build()
)
val registered = retry.executeSupplier {
isRegistered(artifact.name, artifact.type)
}
if (!registered) {
throw NoSuchArtifactException(artifact)
}
return sqlRetry.withRetry(READ) {
jooq
.select(
ARTIFACT_VERSIONS.NAME,
ARTIFACT_VERSIONS.TYPE,
ARTIFACT_VERSIONS.VERSION,
ARTIFACT_VERSIONS.RELEASE_STATUS,
ARTIFACT_VERSIONS.CREATED_AT,
ARTIFACT_VERSIONS.GIT_METADATA,
ARTIFACT_VERSIONS.BUILD_METADATA
)
.from(ARTIFACT_VERSIONS)
.where(ARTIFACT_VERSIONS.NAME.eq(artifact.name))
.and(ARTIFACT_VERSIONS.TYPE.eq(artifact.type))
.fetchSortedArtifactVersions(artifact, limit)
.map { it.copy(reference = artifact.reference) }
}
}
override fun getVersionsWithoutMetadata(limit: Int, maxAge: Duration): List {
val cutoff = clock.instant().minus(maxAge)
return sqlRetry.withRetry(READ) {
jooq
.select(
ARTIFACT_VERSIONS.NAME,
ARTIFACT_VERSIONS.TYPE,
ARTIFACT_VERSIONS.VERSION,
ARTIFACT_VERSIONS.RELEASE_STATUS,
ARTIFACT_VERSIONS.CREATED_AT,
ARTIFACT_VERSIONS.GIT_METADATA,
ARTIFACT_VERSIONS.BUILD_METADATA,
ARTIFACT_VERSIONS.ORIGINAL_METADATA
)
.from(ARTIFACT_VERSIONS)
.where(ARTIFACT_VERSIONS.GIT_METADATA.isNull.or(ARTIFACT_VERSIONS.BUILD_METADATA.isNull))
.and(ARTIFACT_VERSIONS.CREATED_AT.greaterOrEqual(cutoff))
.limit(limit)
.fetch { (name, type, version, status, createdAt, gitMetadata, buildMetadata, originalMetadata) ->
PublishedArtifact(
name = name,
type = type,
version = version,
status = status,
createdAt = createdAt,
gitMetadata = gitMetadata,
buildMetadata = buildMetadata,
metadata = originalMetadata?.let { objectMapper.readValue(it) } ?: emptyMap()
)
}
}
}
override fun storeArtifactVersion(artifactVersion: PublishedArtifact): Boolean {
with(artifactVersion) {
if (!isRegistered(name, type)) {
throw NoSuchArtifactException(name, type)
}
return sqlRetry.withRetry(WRITE) {
jooq.insertInto(ARTIFACT_VERSIONS)
.set(ARTIFACT_VERSIONS.NAME, name)
.set(ARTIFACT_VERSIONS.TYPE, type)
.set(ARTIFACT_VERSIONS.VERSION, version)
.set(ARTIFACT_VERSIONS.RELEASE_STATUS, status)
.set(ARTIFACT_VERSIONS.CREATED_AT, createdAt)
.set(ARTIFACT_VERSIONS.GIT_METADATA, gitMetadata)
.set(ARTIFACT_VERSIONS.BUILD_METADATA, buildMetadata)
.set(ARTIFACT_VERSIONS.ORIGINAL_METADATA, objectMapper.writeValueAsString(artifactVersion.metadata))
.onDuplicateKeyIgnore()
.execute()
} == 1
}
}
override fun getArtifactVersion(artifact: DeliveryArtifact, version: String, status: ArtifactStatus?): PublishedArtifact? {
return sqlRetry.withRetry(READ) {
jooq
.select(
ARTIFACT_VERSIONS.NAME,
ARTIFACT_VERSIONS.TYPE,
ARTIFACT_VERSIONS.VERSION,
ARTIFACT_VERSIONS.RELEASE_STATUS,
ARTIFACT_VERSIONS.CREATED_AT,
ARTIFACT_VERSIONS.GIT_METADATA,
ARTIFACT_VERSIONS.BUILD_METADATA,
ARTIFACT_VERSIONS.ORIGINAL_METADATA
)
.from(ARTIFACT_VERSIONS)
.where(ARTIFACT_VERSIONS.NAME.eq(artifact.name))
.and(ARTIFACT_VERSIONS.TYPE.eq(artifact.type))
.and(ARTIFACT_VERSIONS.VERSION.eq(version))
.apply { if (status != null) and(ARTIFACT_VERSIONS.RELEASE_STATUS.eq(status)) }
.fetchOne { (name, type, version, status, createdAt, gitMetadata, buildMetadata, originalMetadata) ->
PublishedArtifact(
name = name,
type = type,
version = version,
status = status,
createdAt = createdAt,
gitMetadata = gitMetadata,
buildMetadata = buildMetadata,
metadata = originalMetadata?.let { objectMapper.readValue(it) } ?: emptyMap()
)
}
}
}
override fun updateArtifactMetadata(artifact: PublishedArtifact, artifactMetadata: ArtifactMetadata) {
with(artifact) {
if (!isRegistered(name, type)) {
throw NoSuchArtifactException(name, type)
}
sqlRetry.withRetry(WRITE) {
jooq.update(ARTIFACT_VERSIONS)
.set(ARTIFACT_VERSIONS.BUILD_METADATA, artifactMetadata.buildMetadata)
.set(ARTIFACT_VERSIONS.GIT_METADATA, artifactMetadata.gitMetadata)
.where(
ARTIFACT_VERSIONS.NAME.eq(name),
ARTIFACT_VERSIONS.TYPE.eq(type),
ARTIFACT_VERSIONS.VERSION.eq(version).or(ARTIFACT_VERSIONS.VERSION.eq("$name-$version")))
.apply { if (status != null) and(ARTIFACT_VERSIONS.RELEASE_STATUS.eq(status)) }
.execute()
}
}
}
override fun getReleaseStatus(artifact: DeliveryArtifact, version: String): ArtifactStatus? =
if (isRegistered(artifact.name, artifact.type)) {
sqlRetry.withRetry(READ) {
jooq
.select(ARTIFACT_VERSIONS.RELEASE_STATUS)
.from(ARTIFACT_VERSIONS)
.where(ARTIFACT_VERSIONS.NAME.eq(artifact.name))
.and(ARTIFACT_VERSIONS.TYPE.eq(artifact.type))
.and(ARTIFACT_VERSIONS.VERSION.eq(version))
.fetchOne(ARTIFACT_VERSIONS.RELEASE_STATUS)
}
} else {
throw NoSuchArtifactException(artifact)
}
override fun latestVersionApprovedIn(
deliveryConfig: DeliveryConfig,
artifact: DeliveryArtifact,
targetEnvironment: String
): String? {
val environment = deliveryConfig.environmentNamed(targetEnvironment)
val envUid = deliveryConfig.getUidFor(environment)
val artifactId = artifact.uid
/**
* If [targetEnvironment] has been pinned to an artifact version, return
* the pinned version. Otherwise return the most recently approved version.
*/
sqlRetry.withRetry(READ) {
jooq.select(ENVIRONMENT_ARTIFACT_PIN.ARTIFACT_VERSION)
.from(ENVIRONMENT_ARTIFACT_PIN)
.where(
ENVIRONMENT_ARTIFACT_PIN.ENVIRONMENT_UID.eq(envUid),
ENVIRONMENT_ARTIFACT_PIN.ARTIFACT_UID.eq(artifactId)
)
.fetchOne(ENVIRONMENT_ARTIFACT_PIN.ARTIFACT_VERSION)
}
?.also { return it }
return sqlRetry.withRetry(READ) {
jooq
.select(
ARTIFACT_VERSIONS.NAME,
ARTIFACT_VERSIONS.TYPE,
ARTIFACT_VERSIONS.VERSION,
ARTIFACT_VERSIONS.RELEASE_STATUS,
ARTIFACT_VERSIONS.CREATED_AT,
ARTIFACT_VERSIONS.GIT_METADATA,
ARTIFACT_VERSIONS.BUILD_METADATA
)
.from(ARTIFACT_VERSIONS, ENVIRONMENT_ARTIFACT_VERSIONS)
.where(ENVIRONMENT_ARTIFACT_VERSIONS.ARTIFACT_VERSION.eq(ARTIFACT_VERSIONS.VERSION))
.and(ENVIRONMENT_ARTIFACT_VERSIONS.ENVIRONMENT_UID.eq(envUid))
.and(ENVIRONMENT_ARTIFACT_VERSIONS.ARTIFACT_UID.eq(artifactId))
.and(ENVIRONMENT_ARTIFACT_VERSIONS.APPROVED_AT.isNotNull)
.and(ENVIRONMENT_ARTIFACT_VERSIONS.PROMOTION_STATUS.ne(VETOED))
.orderBy(ENVIRONMENT_ARTIFACT_VERSIONS.APPROVED_AT.desc())
.limit(20)
.fetchArtifactVersions()
.sortedWith(artifact.sortingStrategy.comparator).firstOrNull()?.version
}
}
override fun approveVersionFor(
deliveryConfig: DeliveryConfig,
artifact: DeliveryArtifact,
version: String,
targetEnvironment: String
): Boolean {
val environment = deliveryConfig.environmentNamed(targetEnvironment)
return sqlRetry.withRetry(WRITE) {
jooq
.insertInto(ENVIRONMENT_ARTIFACT_VERSIONS)
.set(ENVIRONMENT_ARTIFACT_VERSIONS.ENVIRONMENT_UID, deliveryConfig.getUidFor(environment))
.set(ENVIRONMENT_ARTIFACT_VERSIONS.ARTIFACT_UID, artifact.uid)
.set(ENVIRONMENT_ARTIFACT_VERSIONS.ARTIFACT_VERSION, version)
.set(ENVIRONMENT_ARTIFACT_VERSIONS.APPROVED_AT, clock.instant())
.set(ENVIRONMENT_ARTIFACT_VERSIONS.PROMOTION_STATUS, APPROVED)
.onDuplicateKeyIgnore()
.execute()
} > 0
}
override fun isApprovedFor(
deliveryConfig: DeliveryConfig,
artifact: DeliveryArtifact,
version: String,
targetEnvironment: String
): Boolean {
val environment = deliveryConfig.environmentNamed(targetEnvironment)
return sqlRetry.withRetry(READ) {
jooq
.fetchExists(
ENVIRONMENT_ARTIFACT_VERSIONS,
ENVIRONMENT_ARTIFACT_VERSIONS.ENVIRONMENT_UID.eq(deliveryConfig.getUidFor(environment))
.and(ENVIRONMENT_ARTIFACT_VERSIONS.ARTIFACT_UID.eq(artifact.uid))
.and(ENVIRONMENT_ARTIFACT_VERSIONS.ARTIFACT_VERSION.eq(version))
.and(ENVIRONMENT_ARTIFACT_VERSIONS.PROMOTION_STATUS.ne(VETOED))
)
}
}
override fun wasSuccessfullyDeployedTo(
deliveryConfig: DeliveryConfig,
artifact: DeliveryArtifact,
version: String,
targetEnvironment: String
): Boolean {
val environment = deliveryConfig.environmentNamed(targetEnvironment)
return sqlRetry.withRetry(READ) {
jooq
.fetchExists(
ENVIRONMENT_ARTIFACT_VERSIONS,
ENVIRONMENT_ARTIFACT_VERSIONS.ENVIRONMENT_UID.eq(deliveryConfig.getUidFor(environment))
.and(ENVIRONMENT_ARTIFACT_VERSIONS.ARTIFACT_UID.eq(artifact.uid))
.and(ENVIRONMENT_ARTIFACT_VERSIONS.ARTIFACT_VERSION.eq(version))
.and(ENVIRONMENT_ARTIFACT_VERSIONS.DEPLOYED_AT.isNotNull)
.and(ENVIRONMENT_ARTIFACT_VERSIONS.PROMOTION_STATUS.`in`(listOf(CURRENT, PREVIOUS)))
)
}
}
override fun isCurrentlyDeployedTo(
deliveryConfig: DeliveryConfig,
artifact: DeliveryArtifact,
version: String,
targetEnvironment: String
): Boolean {
val environment = deliveryConfig.environmentNamed(targetEnvironment)
return sqlRetry.withRetry(READ) {
jooq
.fetchExists(
ENVIRONMENT_ARTIFACT_VERSIONS,
ENVIRONMENT_ARTIFACT_VERSIONS.ENVIRONMENT_UID.eq(deliveryConfig.getUidFor(environment))
.and(ENVIRONMENT_ARTIFACT_VERSIONS.ARTIFACT_UID.eq(artifact.uid))
.and(ENVIRONMENT_ARTIFACT_VERSIONS.ARTIFACT_VERSION.eq(version))
.and(ENVIRONMENT_ARTIFACT_VERSIONS.DEPLOYED_AT.isNotNull)
.and(ENVIRONMENT_ARTIFACT_VERSIONS.PROMOTION_STATUS.eq(CURRENT))
)
}
}
override fun markAsDeployingTo(
deliveryConfig: DeliveryConfig,
artifact: DeliveryArtifact,
version: String,
targetEnvironment: String
) {
val environment = deliveryConfig.environmentNamed(targetEnvironment)
val environmentUid = deliveryConfig.getUidFor(environment)
val now = clock.instant()
sqlRetry.withRetry(WRITE) {
jooq.transaction { config ->
val txn = DSL.using(config)
val stuckVersions = txn.select(ENVIRONMENT_ARTIFACT_VERSIONS.ARTIFACT_VERSION)
.from(ENVIRONMENT_ARTIFACT_VERSIONS)
.where(ENVIRONMENT_ARTIFACT_VERSIONS.ENVIRONMENT_UID.eq(environmentUid))
.and(ENVIRONMENT_ARTIFACT_VERSIONS.ARTIFACT_UID.eq(artifact.uid))
.and(ENVIRONMENT_ARTIFACT_VERSIONS.PROMOTION_STATUS.eq(DEPLOYING))
.and(ENVIRONMENT_ARTIFACT_VERSIONS.ARTIFACT_VERSION.ne(version))
.fetch(ENVIRONMENT_ARTIFACT_VERSIONS.ARTIFACT_VERSION)
if (stuckVersions.isNotEmpty()) {
log.error("Stuck deploying versions ${stuckVersions.joinToString()} for artifact '${artifact.reference}' in delivery config ${deliveryConfig.name} found when deploying version $version")
txn.update(ENVIRONMENT_ARTIFACT_VERSIONS)
.set(ENVIRONMENT_ARTIFACT_VERSIONS.PROMOTION_STATUS, SKIPPED)
.set(ENVIRONMENT_ARTIFACT_VERSIONS.REPLACED_BY, version)
.set(ENVIRONMENT_ARTIFACT_VERSIONS.REPLACED_AT, now)
.where(ENVIRONMENT_ARTIFACT_VERSIONS.ENVIRONMENT_UID.eq(environmentUid))
.and(ENVIRONMENT_ARTIFACT_VERSIONS.ARTIFACT_UID.eq(artifact.uid))
.and(ENVIRONMENT_ARTIFACT_VERSIONS.PROMOTION_STATUS.eq(DEPLOYING))
.execute()
}
txn
.insertInto(ENVIRONMENT_ARTIFACT_VERSIONS)
.set(ENVIRONMENT_ARTIFACT_VERSIONS.ENVIRONMENT_UID, environmentUid)
.set(ENVIRONMENT_ARTIFACT_VERSIONS.ARTIFACT_UID, artifact.uid)
.set(ENVIRONMENT_ARTIFACT_VERSIONS.ARTIFACT_VERSION, version)
.set(ENVIRONMENT_ARTIFACT_VERSIONS.PROMOTION_STATUS, DEPLOYING)
.onDuplicateKeyUpdate()
.set(ENVIRONMENT_ARTIFACT_VERSIONS.PROMOTION_STATUS, DEPLOYING)
.execute()
}
}
}
override fun isDeployingTo(
deliveryConfig: DeliveryConfig,
targetEnvironment: String
): Boolean =
jooq.selectCount()
.from(ENVIRONMENT_ARTIFACT_VERSIONS)
.where(ENVIRONMENT_ARTIFACT_VERSIONS.ENVIRONMENT_UID.eq(deliveryConfig.getUidFor(targetEnvironment)))
.and(ENVIRONMENT_ARTIFACT_VERSIONS.PROMOTION_STATUS.eq(DEPLOYING))
.fetchSingleInto() > 0
override fun markAsSuccessfullyDeployedTo(
deliveryConfig: DeliveryConfig,
artifact: DeliveryArtifact,
version: String,
targetEnvironment: String
) {
val environment = deliveryConfig.environmentNamed(targetEnvironment)
val environmentUid = deliveryConfig.getUidFor(environment)
val environmentUidString = deliveryConfig.getUidStringFor(environment)
val artifactUid = artifact.uidString
val artifactVersion = getArtifactVersion(artifact, version)
?: throw NoSuchArtifactVersionException(artifact, version)
sqlRetry.withRetry(WRITE) {
jooq.transaction { config ->
val txn = DSL.using(config)
log.debug("markAsSuccessfullyDeployedTo: start transaction. name: ${artifact.name}. version: $version. env: $targetEnvironment")
val currentUpdates = txn
.insertInto(ENVIRONMENT_ARTIFACT_VERSIONS)
.set(ENVIRONMENT_ARTIFACT_VERSIONS.ENVIRONMENT_UID, environmentUid)
.set(ENVIRONMENT_ARTIFACT_VERSIONS.ARTIFACT_UID, artifact.uid)
.set(ENVIRONMENT_ARTIFACT_VERSIONS.ARTIFACT_VERSION, version)
.set(ENVIRONMENT_ARTIFACT_VERSIONS.DEPLOYED_AT, clock.instant())
.set(ENVIRONMENT_ARTIFACT_VERSIONS.PROMOTION_STATUS, CURRENT)
.onDuplicateKeyUpdate()
.set(ENVIRONMENT_ARTIFACT_VERSIONS.DEPLOYED_AT, clock.instant())
.set(ENVIRONMENT_ARTIFACT_VERSIONS.PROMOTION_STATUS, CURRENT)
.execute()
log.debug("markAsSuccessfullyDeployedTo: # of records marked CURRENT: $currentUpdates. name: ${artifact.name}. version: $version. env: $targetEnvironment")
// update old "CURRENT" to "PREVIOUS
val previousUpdates = txn
.update(ENVIRONMENT_ARTIFACT_VERSIONS)
.set(ENVIRONMENT_ARTIFACT_VERSIONS.PROMOTION_STATUS, PREVIOUS)
.set(ENVIRONMENT_ARTIFACT_VERSIONS.REPLACED_BY, version)
.set(ENVIRONMENT_ARTIFACT_VERSIONS.REPLACED_AT, clock.instant())
.where(ENVIRONMENT_ARTIFACT_VERSIONS.ENVIRONMENT_UID.eq(environmentUid))
.and(ENVIRONMENT_ARTIFACT_VERSIONS.ARTIFACT_UID.eq(artifact.uid))
.and(ENVIRONMENT_ARTIFACT_VERSIONS.PROMOTION_STATUS.eq(CURRENT))
.and(ENVIRONMENT_ARTIFACT_VERSIONS.ARTIFACT_VERSION.ne(version))
.execute()
log.debug("markAsSuccessfullyDeployedTo: # of records marked PREVIOUS: $previousUpdates. name: ${artifact.name}. version: $version. env: $targetEnvironment")
// update any past artifacts that were "APPROVED" to be "SKIPPED"
// because the new version takes precedence
val approved = txn.select(
ARTIFACT_VERSIONS.NAME,
ARTIFACT_VERSIONS.TYPE,
ARTIFACT_VERSIONS.VERSION,
ARTIFACT_VERSIONS.RELEASE_STATUS,
ARTIFACT_VERSIONS.CREATED_AT,
ARTIFACT_VERSIONS.GIT_METADATA,
ARTIFACT_VERSIONS.BUILD_METADATA
)
.from(ENVIRONMENT_ARTIFACT_VERSIONS, DELIVERY_ARTIFACT, ARTIFACT_VERSIONS)
.where(ENVIRONMENT_ARTIFACT_VERSIONS.ENVIRONMENT_UID.eq(environmentUid))
.and(ENVIRONMENT_ARTIFACT_VERSIONS.ARTIFACT_UID.eq(artifact.uid))
.and(ENVIRONMENT_ARTIFACT_VERSIONS.PROMOTION_STATUS.eq(APPROVED))
.and(DELIVERY_ARTIFACT.UID.eq(ENVIRONMENT_ARTIFACT_VERSIONS.ARTIFACT_UID))
.and(ARTIFACT_VERSIONS.NAME.eq(DELIVERY_ARTIFACT.NAME))
.and(ARTIFACT_VERSIONS.TYPE.eq(DELIVERY_ARTIFACT.TYPE))
.and(ARTIFACT_VERSIONS.VERSION.eq(ENVIRONMENT_ARTIFACT_VERSIONS.ARTIFACT_VERSION))
.fetchSortedArtifactVersions(artifact)
log.debug("markAsSuccessfullyDeployedTo: # of records marked APPROVED: ${approved.size}. name: ${artifact.name}. version: $version. env: $targetEnvironment")
val approvedButOld = approved
.filter { isOlder(artifact, it, artifactVersion) }
.map { it.version }
.toTypedArray()
log.debug("markAsSuccessfullyDeployedTo: # of approvedButOld: ${approvedButOld.size}. ${artifact.name}. version: $version. env: $targetEnvironment")
if (approvedButOld.isNotEmpty()) {
val skippedUpdates = txn
.update(ENVIRONMENT_ARTIFACT_VERSIONS)
.set(ENVIRONMENT_ARTIFACT_VERSIONS.PROMOTION_STATUS, SKIPPED)
.set(ENVIRONMENT_ARTIFACT_VERSIONS.REPLACED_BY, version)
.set(ENVIRONMENT_ARTIFACT_VERSIONS.REPLACED_AT, clock.instant())
.where(ENVIRONMENT_ARTIFACT_VERSIONS.ENVIRONMENT_UID.eq(environmentUid))
.and(ENVIRONMENT_ARTIFACT_VERSIONS.ARTIFACT_UID.eq(artifact.uid))
.and(ENVIRONMENT_ARTIFACT_VERSIONS.PROMOTION_STATUS.eq(APPROVED))
.and(ENVIRONMENT_ARTIFACT_VERSIONS.ARTIFACT_VERSION.`in`(*approvedButOld))
.execute()
log.debug("markAsSuccessfullyDeployedTo: # of records marked SKIPPED: $skippedUpdates. name: ${artifact.name}. version: $version. env: $targetEnvironment")
}
val pendingButOld = getPendingVersionsInEnvironment(
deliveryConfig,
artifact.reference,
targetEnvironment
).filter { isOlder(artifact, it, artifactVersion) }
.map { it.version }
.take(100) // only take some of the records so this isn't a giant query
.toTypedArray()
log.debug("markAsSuccessfullyDeployedTo: # of pendingButOld: ${pendingButOld.size}. ${artifact.name}. version: $version. env: $targetEnvironment")
if (pendingButOld.isNotEmpty()) {
val skippedUpdates = txn
.insertInto(ENVIRONMENT_ARTIFACT_VERSIONS,
ENVIRONMENT_ARTIFACT_VERSIONS.ENVIRONMENT_UID,
ENVIRONMENT_ARTIFACT_VERSIONS.ARTIFACT_UID,
ENVIRONMENT_ARTIFACT_VERSIONS.ARTIFACT_VERSION,
ENVIRONMENT_ARTIFACT_VERSIONS.PROMOTION_STATUS,
ENVIRONMENT_ARTIFACT_VERSIONS.REPLACED_BY,
ENVIRONMENT_ARTIFACT_VERSIONS.REPLACED_AT
)
.apply {
// this should skip all pending old versions in the env
pendingButOld.forEach {
values(environmentUidString, artifactUid, it, SKIPPED, version, clock.instant())
}
}
.execute()
log.debug("markAsSuccessfullyDeployedTo: # of pending versions marked SKIPPED: $skippedUpdates. name: ${artifact.name}. version: $version. env: $targetEnvironment")
}
}
}
log.debug("markAsSuccessfullyDeployedTo complete. name: ${artifact.name}. version: $version. env: $targetEnvironment")
}
override fun vetoedEnvironmentVersions(deliveryConfig: DeliveryConfig): List {
val artifactsById = deliveryConfig.artifacts
.associateBy { it.uidString }
val vetoes: MutableMap = mutableMapOf()
jooq.select(
ENVIRONMENT.NAME,
ENVIRONMENT_ARTIFACT_VERSIONS.ARTIFACT_UID,
ENVIRONMENT_ARTIFACT_VERSIONS.ARTIFACT_VERSION,
ENVIRONMENT_ARTIFACT_VETO.VETOED_BY,
ENVIRONMENT_ARTIFACT_VETO.VETOED_AT,
ENVIRONMENT_ARTIFACT_VETO.COMMENT,
)
.from(ENVIRONMENT)
.innerJoin(ENVIRONMENT_ARTIFACT_VERSIONS)
.on(ENVIRONMENT_ARTIFACT_VERSIONS.ENVIRONMENT_UID.eq(ENVIRONMENT.UID))
.innerJoin(ENVIRONMENT_ARTIFACT_VETO)
.on(ENVIRONMENT_ARTIFACT_VERSIONS.ENVIRONMENT_UID.eq(ENVIRONMENT_ARTIFACT_VETO.ENVIRONMENT_UID))
.and(ENVIRONMENT_ARTIFACT_VERSIONS.ARTIFACT_VERSION.eq(ENVIRONMENT_ARTIFACT_VETO.ARTIFACT_VERSION))
.where(
ENVIRONMENT.DELIVERY_CONFIG_UID.eq(deliveryConfig.uid),
ENVIRONMENT_ARTIFACT_VERSIONS.PROMOTION_STATUS.eq(VETOED)
)
.fetch { (envName, artifactId, version, vetoedBy, vetoedAt, comment) ->
if (artifactsById.containsKey(artifactId)) {
vetoes.getOrPut(
vetoKey(envName, artifactId)
) {
EnvironmentArtifactVetoes(
deliveryConfigName = deliveryConfig.name,
targetEnvironment = envName,
artifact = artifactsById[artifactId]
?: error("Invalid artifactId $artifactId for deliveryConfig ${deliveryConfig.name}"),
versions = mutableSetOf()
)
}
.versions.add(
ArtifactVersionVetoData(
version = version,
vetoedAt = vetoedAt,
vetoedBy = vetoedBy,
comment = comment
)
)
}
}
return vetoes.values.toList()
}
/**
* Record that an artifact version should be vetoed (marked as bad) in a target environment
*
* @param deliveryConfig config object for looking up artifact information by artifact reference
* @param veto information about the artifact version to be vetoed and the target environment to veto in
* @param force if true, record the version as vetoed even if it is marked as the promotion reference
* (automated rollback target) of a previously vetoed version
*
* @return true on success
*
* Preconditions for success:
*
* 1. [veto.version] is not currently pinned in the target environment
* 2. One of the following is true:
* a. There is no record in the environment_artifacts_version table for this version
* b. The record in the environment_artifacts_versions table for this version has promotion_reference=NULL
* c. [force] is true
*
* Note: 2b is a precondition to avoid cascading veto-triggered deployments. If R contains a promotion reference, then
* [veto.version] was originally deployed as a result of another artifact version being vetoed (see postcondition 3a).
*
*
* Postconditions on success:
*
* 1. There exists a record R in the environment_artifacts_versions table such that:
* a. R references the artifact version and the target environment encoded in [veto]
* b. R.promotion_status="VETOED"
* c. If there exists a version that was previously deployed in [veto.targetEnvironment]:
* - R.promotion_reference=
* d. If there does not exist a version that was previously deployed in [veto.targetEnvironment]:
* - R.promotion_reference=[veto.version]
*
* 2. There exists a record T in the environment_artifacts_vetoes table such that:
* a. T references the the target environment and artifact version encoded in [veto]
*
* 3. If there exists a version that was previously deployed in [veto.targetEnironment]:
* a. There exists a record P in the environment_artifacts_versions table such that
* - P.artifact_version=
* - P.promotion_reference=[veto.version]
*/
override fun markAsVetoedIn(
deliveryConfig: DeliveryConfig,
veto: EnvironmentArtifactVeto,
force: Boolean
): Boolean {
val artifact = deliveryConfig.matchingArtifactByReference(veto.reference)
?: throw ArtifactNotFoundException(veto.reference, deliveryConfig.name)
val (envUid, artUid) = environmentAndArtifactIds(deliveryConfig, veto.targetEnvironment, artifact)
if (isPinned(envUid, artUid, veto.version)) {
log.warn(
"Pinned artifact version cannot be vetoed: " +
"deliveryConfig=${deliveryConfig.name}, " +
"environment=${veto.targetEnvironment}, " +
"artifactVersion=${veto.version}"
)
return false
}
/**
* If there's a promotion reference, that means this artifact version was deployed as a result of
* another artifact version being vetoed. In that case, we don't veto unless [force] is enabled.
*/
selectPromotionReference(envUid, artUid, veto.version)
.fetchOne(ENVIRONMENT_ARTIFACT_VERSIONS.PROMOTION_REFERENCE)
?.let { reference ->
if (!force) {
log.warn(
"Not vetoing artifact version as it appears to have already been an automated rollback target: " +
"deliveryConfig=${deliveryConfig.name}, " +
"environment=${veto.targetEnvironment}, " +
"artifactVersion=${veto.version}, " +
"priorVersionReference=$reference"
)
return false
}
}
val prior = priorVersionDeployedIn(envUid, artUid, veto.version)
sqlRetry.withRetry(WRITE) {
jooq.transaction { config ->
val txn = DSL.using(config)
txn.upsertAsVetoedInEnvironmentArtifactVersionsTable(prior, veto, envUid, artUid)
txn.addRecordToEnvironmentArtifactVetoTable(envUid, artUid, veto)
/**
* If there's a previously deployed version in [targetEnvironment], set `promotion_reference`
* to the version that's currently being vetoed. If that version also fails to fully deploy,
* this is used to short-circuit further automated vetoes. We want to avoid a cloud provider
* or other issue unrelated to an artifact version triggering continual automated rollbacks
* thru all previously deployed versions.
*/
prior?.let { txn.setPromotionReference(veto.version, envUid, artUid, it) }
}
}
return true
}
private fun DSLContext.setPromotionReference(version: String, envUid: String, artUid: String, prior: String) {
update(ENVIRONMENT_ARTIFACT_VERSIONS)
.set(ENVIRONMENT_ARTIFACT_VERSIONS.PROMOTION_REFERENCE, version)
.where(
ENVIRONMENT_ARTIFACT_VERSIONS.ENVIRONMENT_UID.eq(envUid),
ENVIRONMENT_ARTIFACT_VERSIONS.ARTIFACT_UID.eq(artUid),
ENVIRONMENT_ARTIFACT_VERSIONS.ARTIFACT_VERSION.eq(prior)
)
.execute()
}
private fun DSLContext.addRecordToEnvironmentArtifactVetoTable(
envUid: String,
artUid: String,
veto: EnvironmentArtifactVeto
) {
insertInto(ENVIRONMENT_ARTIFACT_VETO)
.set(ENVIRONMENT_ARTIFACT_VETO.ENVIRONMENT_UID, envUid)
.set(ENVIRONMENT_ARTIFACT_VETO.ARTIFACT_UID, artUid)
.set(ENVIRONMENT_ARTIFACT_VETO.ARTIFACT_VERSION, veto.version)
.set(ENVIRONMENT_ARTIFACT_VETO.VETOED_AT, clock.instant())
.set(ENVIRONMENT_ARTIFACT_VETO.VETOED_BY, veto.vetoedBy)
.set(ENVIRONMENT_ARTIFACT_VETO.COMMENT, veto.comment)
.onDuplicateKeyUpdate()
.set(ENVIRONMENT_ARTIFACT_VETO.VETOED_AT, clock.instant())
.set(ENVIRONMENT_ARTIFACT_VETO.VETOED_BY, veto.vetoedBy)
.set(ENVIRONMENT_ARTIFACT_VETO.COMMENT, veto.comment)
.execute()
}
private fun DSLContext.upsertAsVetoedInEnvironmentArtifactVersionsTable(
prior: String?,
veto: EnvironmentArtifactVeto,
envUid: String,
artUid: String
) {
insertInto(ENVIRONMENT_ARTIFACT_VERSIONS)
.set(ENVIRONMENT_ARTIFACT_VERSIONS.ENVIRONMENT_UID, envUid)
.set(ENVIRONMENT_ARTIFACT_VERSIONS.ARTIFACT_UID, artUid)
.set(ENVIRONMENT_ARTIFACT_VERSIONS.ARTIFACT_VERSION, veto.version)
.set(ENVIRONMENT_ARTIFACT_VERSIONS.PROMOTION_STATUS, VETOED)
.set(ENVIRONMENT_ARTIFACT_VERSIONS.PROMOTION_REFERENCE, prior ?: veto.version)
.onDuplicateKeyUpdate()
.set(ENVIRONMENT_ARTIFACT_VERSIONS.PROMOTION_STATUS, VETOED)
.set(ENVIRONMENT_ARTIFACT_VERSIONS.PROMOTION_REFERENCE, prior ?: veto.version)
.execute()
}
private fun selectPromotionReference(
envUid: String,
artUid: String,
version: String
): SelectConditionStep> {
return jooq
.select(ENVIRONMENT_ARTIFACT_VERSIONS.PROMOTION_REFERENCE)
.from(ENVIRONMENT_ARTIFACT_VERSIONS)
.where(
ENVIRONMENT_ARTIFACT_VERSIONS.ENVIRONMENT_UID.eq(envUid),
ENVIRONMENT_ARTIFACT_VERSIONS.ARTIFACT_UID.eq(artUid),
ENVIRONMENT_ARTIFACT_VERSIONS.ARTIFACT_VERSION.eq(version)
)
}
private fun isPinned(
envUid: String,
artUid: String,
version: String
): Boolean {
return sqlRetry.withRetry(READ) {
jooq
.fetchExists(
ENVIRONMENT_ARTIFACT_PIN,
ENVIRONMENT_ARTIFACT_PIN.ENVIRONMENT_UID.eq(envUid)
.and(ENVIRONMENT_ARTIFACT_PIN.ARTIFACT_UID.eq(artUid))
.and(ENVIRONMENT_ARTIFACT_PIN.ARTIFACT_VERSION.eq(version))
)
}
}
private fun environmentAndArtifactIds(
deliveryConfig: DeliveryConfig,
targetEnvironment: String,
artifact: DeliveryArtifact
): Pair {
return sqlRetry.withRetry(READ) {
Pair(
deliveryConfig.getUidStringFor(
deliveryConfig.environmentNamed(targetEnvironment)
),
artifact.uidString
)
}
}
override fun deleteVeto(
deliveryConfig: DeliveryConfig,
artifact: DeliveryArtifact,
version: String,
targetEnvironment: String
) {
val envId = deliveryConfig.getUidFor(
deliveryConfig.environmentNamed(targetEnvironment)
)
val artId = artifact.uidString
sqlRetry.withRetry(WRITE) {
val referenceVersion: String? = jooq.select(ENVIRONMENT_ARTIFACT_VERSIONS.PROMOTION_REFERENCE)
.from(ENVIRONMENT_ARTIFACT_VERSIONS)
.where(
ENVIRONMENT_ARTIFACT_VERSIONS.ENVIRONMENT_UID.eq(envId),
ENVIRONMENT_ARTIFACT_VERSIONS.ARTIFACT_UID.eq(artifact.uid),
ENVIRONMENT_ARTIFACT_VERSIONS.ARTIFACT_VERSION.eq(version)
)
.fetchOne(ENVIRONMENT_ARTIFACT_VERSIONS.PROMOTION_REFERENCE)
/**
* If there are bidirectional `PROMOTION_REFERENCE` markers between this [version]
* and another (i.e. the veto was applied in order to rollback from this version
* to the other), both sides are removed.
*/
val referencesReferenceVersion: String? = when (referenceVersion) {
null -> null
else -> {
jooq.select(ENVIRONMENT_ARTIFACT_VERSIONS.PROMOTION_REFERENCE)
.from(ENVIRONMENT_ARTIFACT_VERSIONS)
.where(
ENVIRONMENT_ARTIFACT_VERSIONS.ENVIRONMENT_UID.eq(envId),
ENVIRONMENT_ARTIFACT_VERSIONS.ARTIFACT_UID.eq(artId),
ENVIRONMENT_ARTIFACT_VERSIONS.ARTIFACT_VERSION.eq(referenceVersion)
)
.fetchOne(ENVIRONMENT_ARTIFACT_VERSIONS.PROMOTION_REFERENCE)
}
}
val status: PromotionStatus = jooq
.select(
ENVIRONMENT_ARTIFACT_VERSIONS.DEPLOYED_AT,
ENVIRONMENT_ARTIFACT_VERSIONS.REPLACED_BY
)
.from(ENVIRONMENT_ARTIFACT_VERSIONS)
.where(
ENVIRONMENT_ARTIFACT_VERSIONS.ENVIRONMENT_UID.eq(envId),
ENVIRONMENT_ARTIFACT_VERSIONS.ARTIFACT_UID.eq(artId),
ENVIRONMENT_ARTIFACT_VERSIONS.ARTIFACT_VERSION.eq(version)
)
.fetchSingle { (deployedAt, replacedBy) ->
if (deployedAt != null && replacedBy != null) {
PREVIOUS
} else if (deployedAt != null) {
CURRENT
} else {
APPROVED
}
}
jooq.transaction { config ->
val txn = DSL.using(config)
txn.update(ENVIRONMENT_ARTIFACT_VERSIONS)
.set(ENVIRONMENT_ARTIFACT_VERSIONS.PROMOTION_STATUS, status)
.setNull(ENVIRONMENT_ARTIFACT_VERSIONS.PROMOTION_REFERENCE)
.where(
ENVIRONMENT_ARTIFACT_VERSIONS.ENVIRONMENT_UID.eq(envId),
ENVIRONMENT_ARTIFACT_VERSIONS.ARTIFACT_UID.eq(artId),
ENVIRONMENT_ARTIFACT_VERSIONS.ARTIFACT_VERSION.eq(version)
)
.execute()
if (referencesReferenceVersion != null && referenceVersion == referencesReferenceVersion) {
txn.update(ENVIRONMENT_ARTIFACT_VERSIONS)
.setNull(ENVIRONMENT_ARTIFACT_VERSIONS.PROMOTION_REFERENCE)
.where(
ENVIRONMENT_ARTIFACT_VERSIONS.ENVIRONMENT_UID.eq(envId),
ENVIRONMENT_ARTIFACT_VERSIONS.ARTIFACT_UID.eq(artId),
ENVIRONMENT_ARTIFACT_VERSIONS.ARTIFACT_VERSION.eq(referenceVersion)
)
.execute()
}
txn.deleteFrom(ENVIRONMENT_ARTIFACT_VETO)
.where(ENVIRONMENT_ARTIFACT_VETO.ENVIRONMENT_UID.eq(envId))
.and(ENVIRONMENT_ARTIFACT_VETO.ARTIFACT_UID.eq(artId))
.and(ENVIRONMENT_ARTIFACT_VETO.ARTIFACT_VERSION.eq(version))
.execute()
}
}
}
override fun markAsSkipped(
deliveryConfig: DeliveryConfig,
artifact: DeliveryArtifact,
version: String,
targetEnvironment: String,
supersededByVersion: String?
) {
val environment = deliveryConfig.environmentNamed(targetEnvironment)
val environmentUid = deliveryConfig.getUidFor(environment)
sqlRetry.withRetry(WRITE) {
jooq
.insertInto(ENVIRONMENT_ARTIFACT_VERSIONS)
.set(ENVIRONMENT_ARTIFACT_VERSIONS.ENVIRONMENT_UID, environmentUid)
.set(ENVIRONMENT_ARTIFACT_VERSIONS.ARTIFACT_UID, artifact.uid)
.set(ENVIRONMENT_ARTIFACT_VERSIONS.ARTIFACT_VERSION, version)
.set(ENVIRONMENT_ARTIFACT_VERSIONS.PROMOTION_STATUS, SKIPPED)
.set(ENVIRONMENT_ARTIFACT_VERSIONS.REPLACED_BY, supersededByVersion)
.set(ENVIRONMENT_ARTIFACT_VERSIONS.REPLACED_AT, clock.instant())
.onDuplicateKeyUpdate()
.set(ENVIRONMENT_ARTIFACT_VERSIONS.PROMOTION_STATUS, SKIPPED)
.set(ENVIRONMENT_ARTIFACT_VERSIONS.REPLACED_BY, supersededByVersion)
.set(ENVIRONMENT_ARTIFACT_VERSIONS.REPLACED_AT, clock.instant())
.execute()
}
}
override fun getArtifactVersionsByStatus(
deliveryConfig: DeliveryConfig,
environmentName: String,
statuses: List
): List {
return sqlRetry.withRetry(READ) {
jooq
.select(
ARTIFACT_VERSIONS.NAME,
ARTIFACT_VERSIONS.TYPE,
ARTIFACT_VERSIONS.VERSION,
DELIVERY_ARTIFACT.REFERENCE,
ARTIFACT_VERSIONS.RELEASE_STATUS,
ARTIFACT_VERSIONS.CREATED_AT,
ARTIFACT_VERSIONS.GIT_METADATA,
ARTIFACT_VERSIONS.BUILD_METADATA
)
.from(ENVIRONMENT_ARTIFACT_VERSIONS)
.innerJoin(DELIVERY_ARTIFACT)
.on(ENVIRONMENT_ARTIFACT_VERSIONS.ARTIFACT_UID.eq(DELIVERY_ARTIFACT.UID))
.innerJoin(ARTIFACT_VERSIONS)
.on(DELIVERY_ARTIFACT.NAME.eq(ARTIFACT_VERSIONS.NAME))
.and(DELIVERY_ARTIFACT.TYPE.eq(ARTIFACT_VERSIONS.TYPE))
.and(ENVIRONMENT_ARTIFACT_VERSIONS.ARTIFACT_VERSION.eq(ARTIFACT_VERSIONS.VERSION))
.where(ENVIRONMENT_ARTIFACT_VERSIONS.ENVIRONMENT_UID.eq(deliveryConfig.getUidFor(environmentName)))
.and(ENVIRONMENT_ARTIFACT_VERSIONS.PROMOTION_STATUS.`in`(statuses))
.fetch { (name, type, version, reference, status, createdAt, gitMetadata, buildMetadata) ->
PublishedArtifact(
name = name,
type = type,
version = version,
reference = reference,
status = status,
createdAt = createdAt,
gitMetadata = gitMetadata,
buildMetadata = buildMetadata
)
}
}
}
override fun getAllVersionsForEnvironment(
artifact: DeliveryArtifact,
config: DeliveryConfig,
environmentName: String
): List {
val existingVersions: List = sqlRetry.withRetry(READ) {
jooq
.select(
ARTIFACT_VERSIONS.NAME,
ARTIFACT_VERSIONS.TYPE,
ARTIFACT_VERSIONS.VERSION,
DELIVERY_ARTIFACT.REFERENCE,
ARTIFACT_VERSIONS.RELEASE_STATUS,
ARTIFACT_VERSIONS.CREATED_AT,
ARTIFACT_VERSIONS.GIT_METADATA,
ARTIFACT_VERSIONS.BUILD_METADATA,
ENVIRONMENT_ARTIFACT_VERSIONS.PROMOTION_STATUS,
ENVIRONMENT_ARTIFACT_VERSIONS.DEPLOYED_AT,
ENVIRONMENT_ARTIFACT_VERSIONS.REPLACED_BY
)
.from(ENVIRONMENT_ARTIFACT_VERSIONS)
.innerJoin(DELIVERY_ARTIFACT)
.on(ENVIRONMENT_ARTIFACT_VERSIONS.ARTIFACT_UID.eq(DELIVERY_ARTIFACT.UID))
.innerJoin(ARTIFACT_VERSIONS)
.on(DELIVERY_ARTIFACT.NAME.eq(ARTIFACT_VERSIONS.NAME))
.innerJoin(ACTIVE_ENVIRONMENT)
.on(ACTIVE_ENVIRONMENT.UID.eq(ENVIRONMENT_ARTIFACT_VERSIONS.ENVIRONMENT_UID))
.and(DELIVERY_ARTIFACT.TYPE.eq(ARTIFACT_VERSIONS.TYPE))
.and(ENVIRONMENT_ARTIFACT_VERSIONS.ARTIFACT_VERSION.eq(ARTIFACT_VERSIONS.VERSION))
.and(ENVIRONMENT_ARTIFACT_VERSIONS.ARTIFACT_UID.eq(artifact.uid))
.where(ACTIVE_ENVIRONMENT.NAME.eq(environmentName))
.fetch { (name, type, version, reference, status, createdAt, gitMetadata, buildMetadata, promotionStatus, deployedAt, replacedBy) ->
val publishedArtifact = PublishedArtifact(
name = name,
type = type,
version = version,
reference = reference,
status = status,
createdAt = createdAt,
gitMetadata = gitMetadata,
buildMetadata = buildMetadata,
)
PublishedArtifactInEnvironment(
publishedArtifact,
promotionStatus,
environmentName,
deployedAt,
replacedBy
)
}
}
val pending = getPendingVersions(artifact, config, environmentName)
return existingVersions + pending
}
/**
* Gets all pending version for each environment
*/
private fun getPendingVersions(
artifact: DeliveryArtifact,
config: DeliveryConfig,
environmentName: String
): List =
getPendingVersionsInEnvironment(config, artifact.reference, environmentName)
.map { publishedArtifact ->
PublishedArtifactInEnvironment(
publishedArtifact,
PENDING,
environmentName
)
}
override fun getPendingVersionsInEnvironment(
deliveryConfig: DeliveryConfig,
artifactReference: String,
environmentName: String
): List {
val artifact = deliveryConfig.matchingArtifactByReference(artifactReference)
?: throw ArtifactNotFoundException(artifactReference, deliveryConfig.name)
return sqlRetry.withRetry(READ) {
jooq
.select(
ARTIFACT_VERSIONS.NAME,
ARTIFACT_VERSIONS.TYPE,
ARTIFACT_VERSIONS.VERSION,
ARTIFACT_VERSIONS.RELEASE_STATUS,
ARTIFACT_VERSIONS.CREATED_AT,
ARTIFACT_VERSIONS.GIT_METADATA,
ARTIFACT_VERSIONS.BUILD_METADATA,
)
.from(
ARTIFACT_VERSIONS,
DELIVERY_ARTIFACT,
ACTIVE_ENVIRONMENT,
DELIVERY_CONFIG
)
.where(DELIVERY_ARTIFACT.NAME.eq(artifact.name))
.and(DELIVERY_ARTIFACT.TYPE.eq(artifact.type))
.and(DELIVERY_ARTIFACT.REFERENCE.eq(artifact.reference))
.and(DELIVERY_ARTIFACT.DELIVERY_CONFIG_NAME.eq(deliveryConfig.name))
.and(ACTIVE_ENVIRONMENT.DELIVERY_CONFIG_UID.eq(DELIVERY_CONFIG.UID))
.and(DELIVERY_CONFIG.NAME.eq(deliveryConfig.name))
.and(ACTIVE_ENVIRONMENT.NAME.eq(environmentName))
.and(ARTIFACT_VERSIONS.NAME.eq(artifact.name))
.and(ARTIFACT_VERSIONS.TYPE.eq(artifact.type))
.apply { if (artifact.statuses.isNotEmpty()) and(ARTIFACT_VERSIONS.RELEASE_STATUS.`in`(*artifact.statuses.toTypedArray())) }
.andNotExists(
selectOne()
.from(ENVIRONMENT_ARTIFACT_VERSIONS)
.where(ENVIRONMENT_ARTIFACT_VERSIONS.ARTIFACT_VERSION.eq(ARTIFACT_VERSIONS.VERSION))
.and(ENVIRONMENT_ARTIFACT_VERSIONS.ENVIRONMENT_UID.eq(ACTIVE_ENVIRONMENT.UID))
.and(ENVIRONMENT_ARTIFACT_VERSIONS.ARTIFACT_UID.eq(DELIVERY_ARTIFACT.UID))
).fetch { (name, type, version, status, createdAt, gitMetadata, buildMetadata) ->
PublishedArtifact(
name = name,
type = type,
version = version,
reference = artifactReference,
status = status,
createdAt = createdAt,
gitMetadata = gitMetadata,
buildMetadata = buildMetadata,
)
}.filter {
artifact.hasMatchingSource(it.gitMetadata)
}
}
}
override fun getNumPendingToBePromoted(
deliveryConfig: DeliveryConfig,
artifactReference: String,
environmentName: String,
version: String
): Int {
val artifact = deliveryConfig.matchingArtifactByReference(artifactReference)
?: throw ArtifactNotFoundException(artifactReference, deliveryConfig.name)
val pendingVersions = getPendingVersions(artifact, deliveryConfig, environmentName)
.map { it.publishedArtifact }
val currentVersion = getArtifactVersionsByStatus(
deliveryConfig,
environmentName,
listOf(CURRENT)
).firstOrNull { it.reference == artifactReference }
return removeExtra(pendingVersions, artifact, version, currentVersion).size
}
/**
* Removes any pending artifacts that are newer than the specified version, because they would not be promoted.
* Removes any older than the current version, because they are not relevant.
*/
fun removeExtra(versions: List, artifact: DeliveryArtifact, mjVersion: String, currentVersion: PublishedArtifact?): List {
if (versions.isEmpty()) {
return emptyList()
}
val fullVersions = if (currentVersion != null) {
versions + currentVersion
} else {
versions
}
val sortedVersions = fullVersions.sortedWith(artifact.sortingStrategy.comparator) // newest will be first
val mjArtifact = sortedVersions.find { it.version == mjVersion }
?: return sortedVersions // mj version isn't still pending? or there is a bug?
// remove all versions that are newer than the mj version, they won't be promoted
val newerRemoved = sortedVersions.dropWhile { it != mjArtifact }
return if (currentVersion != null) {
// remove all versions that are older than the current version, and the current version
newerRemoved.dropLastWhile { it != currentVersion }.filterNot { it == currentVersion }
} else {
newerRemoved
}
}
override fun getEnvironmentSummaries(deliveryConfig: DeliveryConfig): List {
val pinnedEnvs = getPinnedEnvironments(deliveryConfig)
return deliveryConfig.environments.map { environment ->
val artifactVersions = deliveryConfig.artifacts.map { artifact ->
val versionsInEnvironment = jooq
.select(
ARTIFACT_VERSIONS.NAME,
ARTIFACT_VERSIONS.TYPE,
ARTIFACT_VERSIONS.VERSION,
ARTIFACT_VERSIONS.RELEASE_STATUS,
ARTIFACT_VERSIONS.CREATED_AT,
ARTIFACT_VERSIONS.GIT_METADATA,
ARTIFACT_VERSIONS.BUILD_METADATA,
ENVIRONMENT_ARTIFACT_VERSIONS.PROMOTION_STATUS
)
.from(
ENVIRONMENT_ARTIFACT_VERSIONS,
ARTIFACT_VERSIONS,
DELIVERY_ARTIFACT,
ACTIVE_ENVIRONMENT,
DELIVERY_CONFIG
)
.where(DELIVERY_ARTIFACT.UID.eq(ENVIRONMENT_ARTIFACT_VERSIONS.ARTIFACT_UID))
.and(DELIVERY_ARTIFACT.NAME.eq(artifact.name))
.and(DELIVERY_ARTIFACT.TYPE.eq(artifact.type))
.and(DELIVERY_ARTIFACT.REFERENCE.eq(artifact.reference))
.and(DELIVERY_ARTIFACT.DELIVERY_CONFIG_NAME.eq(deliveryConfig.name))
.and(ENVIRONMENT_ARTIFACT_VERSIONS.ARTIFACT_VERSION.eq(ARTIFACT_VERSIONS.VERSION))
.and(ACTIVE_ENVIRONMENT.UID.eq(ENVIRONMENT_ARTIFACT_VERSIONS.ENVIRONMENT_UID))
.and(ACTIVE_ENVIRONMENT.DELIVERY_CONFIG_UID.eq(DELIVERY_CONFIG.UID))
.and(ACTIVE_ENVIRONMENT.NAME.eq(environment.name))
.and(DELIVERY_CONFIG.NAME.eq(deliveryConfig.name))
.and(ARTIFACT_VERSIONS.NAME.eq(artifact.name))
.and(ARTIFACT_VERSIONS.TYPE.eq(artifact.type))
.apply {
if (artifact.statuses.isNotEmpty()) {
and(ARTIFACT_VERSIONS.RELEASE_STATUS.`in`(*artifact.statuses.toTypedArray()))
}
}
val pendingVersions = jooq
.select(
ARTIFACT_VERSIONS.NAME,
ARTIFACT_VERSIONS.TYPE,
ARTIFACT_VERSIONS.VERSION,
ARTIFACT_VERSIONS.RELEASE_STATUS,
ARTIFACT_VERSIONS.CREATED_AT,
ARTIFACT_VERSIONS.GIT_METADATA,
ARTIFACT_VERSIONS.BUILD_METADATA,
DSL.`val`(PENDING)
)
.from(
ARTIFACT_VERSIONS,
DELIVERY_ARTIFACT,
ACTIVE_ENVIRONMENT,
DELIVERY_CONFIG
)
.where(DELIVERY_ARTIFACT.NAME.eq(artifact.name))
.and(DELIVERY_ARTIFACT.TYPE.eq(artifact.type))
.and(DELIVERY_ARTIFACT.REFERENCE.eq(artifact.reference))
.and(DELIVERY_ARTIFACT.DELIVERY_CONFIG_NAME.eq(deliveryConfig.name))
.and(ACTIVE_ENVIRONMENT.DELIVERY_CONFIG_UID.eq(DELIVERY_CONFIG.UID))
.and(DELIVERY_CONFIG.NAME.eq(deliveryConfig.name))
.and(ACTIVE_ENVIRONMENT.NAME.eq(environment.name))
.and(ARTIFACT_VERSIONS.NAME.eq(artifact.name))
.and(ARTIFACT_VERSIONS.TYPE.eq(artifact.type))
.apply { if (artifact.statuses.isNotEmpty()) and(ARTIFACT_VERSIONS.RELEASE_STATUS.`in`(*artifact.statuses.toTypedArray())) }
.andNotExists(
selectOne()
.from(ENVIRONMENT_ARTIFACT_VERSIONS)
.where(ENVIRONMENT_ARTIFACT_VERSIONS.ARTIFACT_VERSION.eq(ARTIFACT_VERSIONS.VERSION))
.and(ENVIRONMENT_ARTIFACT_VERSIONS.ENVIRONMENT_UID.eq(ACTIVE_ENVIRONMENT.UID))
.and(ENVIRONMENT_ARTIFACT_VERSIONS.ARTIFACT_UID.eq(DELIVERY_ARTIFACT.UID))
)
val unionedVersions = sqlRetry.withRetry(READ) {
versionsInEnvironment
.unionAll(pendingVersions)
.fetch { (name, type, version, status, createdAt, gitMetadata, buildMetadata, promotionStatus) ->
PublishedArtifact(
name = name,
type = type,
version = version,
status = status,
createdAt = createdAt,
gitMetadata = gitMetadata,
buildMetadata = buildMetadata,
) to promotionStatus
}
}
.filter { (artifactVersion, _) ->
if (artifact is DockerArtifact) {
// filter out invalid docker tags
filterDockerTag(artifactVersion.version, artifact)
} else {
true
}
}
val releaseStatuses: Set = unionedVersions
.mapNotNull { (artifactVersion, _) ->
artifactVersion.status
}
.toSet()
val versions = unionedVersions
.sortedWith(compareBy(artifact.sortingStrategy.comparator) { (artifactVersion, _) -> artifactVersion })
.groupBy(
{ (_, promotionStatus) ->
promotionStatus
},
{ (artifactVersion, _) ->
artifactVersion
}
)
val currentVersion = versions[CURRENT]?.firstOrNull()
ArtifactVersions(
name = artifact.name,
type = artifact.type,
reference = artifact.reference,
statuses = releaseStatuses,
versions = ArtifactVersionStatus(
current = currentVersion?.version,
deploying = versions[DEPLOYING]?.firstOrNull()?.version,
// take out stateful constraint values that will never happen
pending = removeOlderIfCurrentExists(artifact, currentVersion, versions[PENDING]).map { it.version },
approved = (versions[APPROVED] ?: emptyList()).map { it.version },
previous = (versions[PREVIOUS] ?: emptyList()).map { it.version },
vetoed = (versions[VETOED] ?: emptyList()).map { it.version },
skipped = removeNewerIfCurrentExists(artifact, currentVersion, versions[PENDING])
.plus(versions[SKIPPED] ?: emptyList()).map { it.version }
),
pinnedVersion = pinnedEnvs.find { it.targetEnvironment == environment.name }?.version
)
}.toSet()
EnvironmentSummary(environment, artifactVersions)
}
}
override fun pinEnvironment(deliveryConfig: DeliveryConfig, environmentArtifactPin: EnvironmentArtifactPin) {
with(environmentArtifactPin) {
val environment = deliveryConfig.environmentNamed(targetEnvironment)
val artifact = get(deliveryConfig.name, reference)
val now = clock.instant()
sqlRetry.withRetry(WRITE) {
jooq.insertInto(ENVIRONMENT_ARTIFACT_PIN)
.set(ENVIRONMENT_ARTIFACT_PIN.ENVIRONMENT_UID, deliveryConfig.getUidFor(environment))
.set(ENVIRONMENT_ARTIFACT_PIN.ARTIFACT_UID, artifact.uid)
.set(ENVIRONMENT_ARTIFACT_PIN.ARTIFACT_VERSION, version)
.set(ENVIRONMENT_ARTIFACT_PIN.PINNED_AT, now)
.set(ENVIRONMENT_ARTIFACT_PIN.PINNED_BY, pinnedBy ?: "anonymous")
.set(ENVIRONMENT_ARTIFACT_PIN.COMMENT, comment)
.onDuplicateKeyUpdate()
.set(ENVIRONMENT_ARTIFACT_PIN.ARTIFACT_VERSION, version)
.set(ENVIRONMENT_ARTIFACT_PIN.PINNED_AT, now)
.set(ENVIRONMENT_ARTIFACT_PIN.PINNED_BY, pinnedBy ?: "anonymous")
.set(ENVIRONMENT_ARTIFACT_PIN.COMMENT, MySQLDSL.values(ENVIRONMENT_ARTIFACT_PIN.COMMENT))
.execute()
}
}
}
override fun getPinnedEnvironments(deliveryConfig: DeliveryConfig): List {
return sqlRetry.withRetry(READ) {
jooq.select(
ENVIRONMENT.NAME,
ENVIRONMENT_ARTIFACT_PIN.ARTIFACT_VERSION,
ENVIRONMENT_ARTIFACT_PIN.PINNED_AT,
ENVIRONMENT_ARTIFACT_PIN.PINNED_BY,
ENVIRONMENT_ARTIFACT_PIN.COMMENT,
DELIVERY_ARTIFACT.NAME,
DELIVERY_ARTIFACT.TYPE,
DELIVERY_ARTIFACT.DETAILS,
DELIVERY_ARTIFACT.REFERENCE
)
.from(ENVIRONMENT)
.innerJoin(ENVIRONMENT_ARTIFACT_PIN)
.on(ENVIRONMENT_ARTIFACT_PIN.ENVIRONMENT_UID.eq(ENVIRONMENT.UID))
.innerJoin(DELIVERY_ARTIFACT)
.on(DELIVERY_ARTIFACT.UID.eq(ENVIRONMENT_ARTIFACT_PIN.ARTIFACT_UID))
.innerJoin(DELIVERY_CONFIG)
.on(DELIVERY_CONFIG.UID.eq(ENVIRONMENT.DELIVERY_CONFIG_UID))
.where(DELIVERY_CONFIG.NAME.eq(deliveryConfig.name))
.fetch { (environmentName, version, pinnedAt, pinnedBy, comment, artifactName, type, details, reference) ->
PinnedEnvironment(
deliveryConfigName = deliveryConfig.name,
targetEnvironment = environmentName,
artifact = mapToArtifact(
artifactSuppliers.supporting(type),
artifactName,
type.toLowerCase(),
details,
reference,
deliveryConfig.name
),
version = version,
pinnedAt = pinnedAt,
pinnedBy = pinnedBy,
comment = comment
)
}
}
}
override fun deletePin(deliveryConfig: DeliveryConfig, targetEnvironment: String) {
sqlRetry.withRetry(WRITE) {
jooq.select(ENVIRONMENT_ARTIFACT_PIN.ENVIRONMENT_UID, ENVIRONMENT_ARTIFACT_PIN.ARTIFACT_UID)
.from(ENVIRONMENT_ARTIFACT_PIN)
.innerJoin(ENVIRONMENT)
.on(ENVIRONMENT_ARTIFACT_PIN.ENVIRONMENT_UID.eq(ENVIRONMENT.UID))
.where(
ENVIRONMENT.NAME.eq(targetEnvironment),
ENVIRONMENT.DELIVERY_CONFIG_UID.eq(deliveryConfig.uid)
)
.fetch { (envUid, artUid) ->
deletePin(envUid, artUid)
}
}
}
override fun getPinnedVersion(
deliveryConfig: DeliveryConfig,
targetEnvironment: String,
reference: String
): String? {
return sqlRetry.withRetry(READ) {
jooq.select(ENVIRONMENT_ARTIFACT_PIN.ARTIFACT_VERSION)
.from(DELIVERY_ARTIFACT)
.innerJoin(ENVIRONMENT_ARTIFACT_PIN)
.on(ENVIRONMENT_ARTIFACT_PIN.ARTIFACT_UID.eq(DELIVERY_ARTIFACT.UID))
.innerJoin(ENVIRONMENT)
.on(ENVIRONMENT_ARTIFACT_PIN.ENVIRONMENT_UID.eq(ENVIRONMENT.UID))
.where(
DELIVERY_ARTIFACT.DELIVERY_CONFIG_NAME.eq(deliveryConfig.name),
DELIVERY_ARTIFACT.REFERENCE.eq(reference),
ENVIRONMENT.NAME.eq(targetEnvironment),
ENVIRONMENT.DELIVERY_CONFIG_UID.eq(deliveryConfig.uid)
)
.fetch(ENVIRONMENT_ARTIFACT_PIN.ARTIFACT_VERSION)
.firstOrNull()
}
}
override fun deletePin(
deliveryConfig: DeliveryConfig,
targetEnvironment: String,
reference: String
) {
sqlRetry.withRetry(WRITE) {
jooq.select(ENVIRONMENT_ARTIFACT_PIN.ENVIRONMENT_UID, ENVIRONMENT_ARTIFACT_PIN.ARTIFACT_UID)
.from(DELIVERY_ARTIFACT)
.innerJoin(ENVIRONMENT_ARTIFACT_PIN)
.on(ENVIRONMENT_ARTIFACT_PIN.ARTIFACT_UID.eq(DELIVERY_ARTIFACT.UID))
.innerJoin(ENVIRONMENT)
.on(ENVIRONMENT_ARTIFACT_PIN.ENVIRONMENT_UID.eq(ENVIRONMENT.UID))
.where(
DELIVERY_ARTIFACT.DELIVERY_CONFIG_NAME.eq(deliveryConfig.name),
DELIVERY_ARTIFACT.REFERENCE.eq(reference),
ENVIRONMENT.NAME.eq(targetEnvironment),
ENVIRONMENT.DELIVERY_CONFIG_UID.eq(deliveryConfig.uid)
)
.fetch { (envUid, artUid) ->
deletePin(envUid, artUid)
}
}
}
override fun getArtifactVersionByPromotionStatus(
deliveryConfig: DeliveryConfig,
environmentName: String,
artifact: DeliveryArtifact,
promotionStatus: PromotionStatus,
version: String?
): PublishedArtifact? {
//only CURRENT and PREVIOUS are supported, as they can be sorted by deploy_at
require(promotionStatus in listOf(CURRENT, PREVIOUS)) { "Invalid promotion status used to query" }
return sqlRetry.withRetry(READ) {
val fetchedVersion = jooq
.select(ENVIRONMENT_ARTIFACT_VERSIONS.ARTIFACT_VERSION)
.from(ENVIRONMENT_ARTIFACT_VERSIONS)
.where(ENVIRONMENT_ARTIFACT_VERSIONS.ENVIRONMENT_UID.eq(deliveryConfig.getUidFor(environmentName)))
.and(ENVIRONMENT_ARTIFACT_VERSIONS.ARTIFACT_UID.eq(artifact.uid))
.and(ENVIRONMENT_ARTIFACT_VERSIONS.PROMOTION_STATUS.eq(promotionStatus))
//special case for pinning
.apply { if (version != null) and(ENVIRONMENT_ARTIFACT_VERSIONS.REPLACED_BY.eq(version)) }
.orderBy(ENVIRONMENT_ARTIFACT_VERSIONS.DEPLOYED_AT.desc())
.fetch(ENVIRONMENT_ARTIFACT_VERSIONS.ARTIFACT_VERSION)
.firstOrNull()
if (fetchedVersion != null) {
getArtifactVersion(artifact, fetchedVersion)
} else {
null
}
}
}
override fun getVersionInfoInEnvironment(
deliveryConfig: DeliveryConfig,
environmentName: String,
artifact: DeliveryArtifact
): List =
sqlRetry.withRetry(READ) {
jooq
.select(
ENVIRONMENT_ARTIFACT_VERSIONS.ARTIFACT_VERSION,
ENVIRONMENT_ARTIFACT_VERSIONS.PROMOTION_STATUS,
ENVIRONMENT_ARTIFACT_VERSIONS.DEPLOYED_AT,
ENVIRONMENT_ARTIFACT_VERSIONS.REPLACED_BY
)
.from(ENVIRONMENT_ARTIFACT_VERSIONS)
.where(ENVIRONMENT_ARTIFACT_VERSIONS.ENVIRONMENT_UID.eq(deliveryConfig.getUidFor(environmentName)))
.and(ENVIRONMENT_ARTIFACT_VERSIONS.ARTIFACT_UID.eq(artifact.uid))
.and(ENVIRONMENT_ARTIFACT_VERSIONS.PROMOTION_STATUS.`in`(listOf(CURRENT, PREVIOUS)))
.orderBy(ENVIRONMENT_ARTIFACT_VERSIONS.DEPLOYED_AT.desc())
.fetch { (version, status, deployedAt, replacedBy) ->
StatusInfoForArtifactInEnvironment(
version,
status,
replacedBy,
deployedAt
)
}
}
/**
* Returns summary information for the provided list of versions.
*/
override fun getArtifactSummariesInEnvironment(
deliveryConfig: DeliveryConfig,
environmentName: String,
artifactReference: String,
versions: List
): List {
val artifact = deliveryConfig.artifacts.firstOrNull { it.reference == artifactReference }
?: error("Artifact not found: name=$artifactReference, deliveryConfig=${deliveryConfig.name}")
// only one version can be pinned
val pinned: Pair? = sqlRetry.withRetry(READ) { //version, pinned by info
jooq.select(
ENVIRONMENT_ARTIFACT_PIN.PINNED_BY,
ENVIRONMENT_ARTIFACT_PIN.PINNED_AT,
ENVIRONMENT_ARTIFACT_PIN.COMMENT,
ENVIRONMENT_ARTIFACT_PIN.ARTIFACT_VERSION
)
.from(ENVIRONMENT_ARTIFACT_PIN)
.where(ENVIRONMENT_ARTIFACT_PIN.ENVIRONMENT_UID.eq(deliveryConfig.getUidFor(environmentName)))
.and(ENVIRONMENT_ARTIFACT_PIN.ARTIFACT_UID.eq(artifact.uid))
.fetchOne { (pinnedBy, pinnedAt, comment, version) ->
Pair(version, ActionMetadata(at = pinnedAt, by = pinnedBy, comment = comment))
}
}
val vetoed: Map = sqlRetry.withRetry(READ) {
jooq.select(
ENVIRONMENT_ARTIFACT_VETO.VETOED_AT,
ENVIRONMENT_ARTIFACT_VETO.VETOED_BY,
ENVIRONMENT_ARTIFACT_VETO.COMMENT,
ENVIRONMENT_ARTIFACT_VETO.ARTIFACT_VERSION
)
.from(ENVIRONMENT_ARTIFACT_VETO)
.where(ENVIRONMENT_ARTIFACT_VETO.ENVIRONMENT_UID.eq(deliveryConfig.getUidFor(environmentName)))
.and(ENVIRONMENT_ARTIFACT_VETO.ARTIFACT_UID.eq(artifact.uid))
.fetch { (vetoedAt, vetoedBy, comment, version) ->
version to ActionMetadata(at = vetoedAt, by = vetoedBy, comment = comment)
}.toMap()
}
return sqlRetry.withRetry(READ) {
jooq.select(
ENVIRONMENT_ARTIFACT_VERSIONS.ARTIFACT_VERSION,
ENVIRONMENT_ARTIFACT_VERSIONS.DEPLOYED_AT,
ENVIRONMENT_ARTIFACT_VERSIONS.PROMOTION_STATUS,
ENVIRONMENT_ARTIFACT_VERSIONS.REPLACED_BY,
ENVIRONMENT_ARTIFACT_VERSIONS.REPLACED_AT
)
.from(ENVIRONMENT_ARTIFACT_VERSIONS)
.where(ENVIRONMENT_ARTIFACT_VERSIONS.ENVIRONMENT_UID.eq(deliveryConfig.getUidFor(environmentName)))
.and(ENVIRONMENT_ARTIFACT_VERSIONS.ARTIFACT_UID.eq(artifact.uid))
.and(ENVIRONMENT_ARTIFACT_VERSIONS.ARTIFACT_VERSION.`in`(versions))
.orderBy(ENVIRONMENT_ARTIFACT_VERSIONS.DEPLOYED_AT.desc())
.fetch { (version, deployedAt, promotionStatus, replacedBy, replacedAt) ->
ArtifactSummaryInEnvironment(
environment = environmentName,
version = version,
state = promotionStatus.name.toLowerCase(),
deployedAt = deployedAt,
replacedAt = replacedAt,
replacedBy = replacedBy,
pinned = if (pinned != null && pinned.first == version) pinned.second else null,
vetoed = vetoed[version]
)
}
}
}
/**
* Replaced by the bulk call ^
*/
override fun getArtifactSummaryInEnvironment(
deliveryConfig: DeliveryConfig,
environmentName: String,
artifactReference: String,
version: String
): ArtifactSummaryInEnvironment? {
return sqlRetry.withRetry(READ) {
val artifact = deliveryConfig.artifacts.firstOrNull { it.reference == artifactReference }
?: error("Artifact not found: name=$artifactReference, deliveryConfig=${deliveryConfig.name}")
jooq
.select(
ENVIRONMENT_ARTIFACT_VERSIONS.ARTIFACT_VERSION,
ENVIRONMENT_ARTIFACT_VERSIONS.DEPLOYED_AT,
ENVIRONMENT_ARTIFACT_VERSIONS.PROMOTION_STATUS,
ENVIRONMENT_ARTIFACT_VERSIONS.REPLACED_BY,
ENVIRONMENT_ARTIFACT_VERSIONS.REPLACED_AT
)
.from(ENVIRONMENT_ARTIFACT_VERSIONS)
.where(ENVIRONMENT_ARTIFACT_VERSIONS.ENVIRONMENT_UID.eq(deliveryConfig.getUidFor(environmentName)))
.and(ENVIRONMENT_ARTIFACT_VERSIONS.ARTIFACT_UID.eq(artifact.uid))
.and(ENVIRONMENT_ARTIFACT_VERSIONS.ARTIFACT_VERSION.eq(version))
.orderBy(ENVIRONMENT_ARTIFACT_VERSIONS.DEPLOYED_AT.desc())
.fetchOne { (version, deployedAt, promotionStatus, replacedBy, replacedAt) ->
val vetoed: ActionMetadata? = jooq
.select(ENVIRONMENT_ARTIFACT_VETO.VETOED_AT, ENVIRONMENT_ARTIFACT_VETO.VETOED_BY, ENVIRONMENT_ARTIFACT_VETO.COMMENT)
.from(ENVIRONMENT_ARTIFACT_VETO)
.where(ENVIRONMENT_ARTIFACT_VETO.ENVIRONMENT_UID.eq(deliveryConfig.getUidFor(environmentName)))
.and(ENVIRONMENT_ARTIFACT_VETO.ARTIFACT_UID.eq(artifact.uid))
.and(ENVIRONMENT_ARTIFACT_VETO.ARTIFACT_VERSION.eq(version))
.fetchOne { (vetoedAt, vetoedBy, comment) ->
ActionMetadata(at = vetoedAt, by = vetoedBy, comment = comment)
}
var pinned: ActionMetadata? = null
if (vetoed == null) {
// a version can't be vetoed and pinned
pinned = jooq
.select(ENVIRONMENT_ARTIFACT_PIN.PINNED_BY, ENVIRONMENT_ARTIFACT_PIN.PINNED_AT, ENVIRONMENT_ARTIFACT_PIN.COMMENT)
.from(ENVIRONMENT_ARTIFACT_PIN)
.where(ENVIRONMENT_ARTIFACT_PIN.ENVIRONMENT_UID.eq(deliveryConfig.getUidFor(environmentName)))
.and(ENVIRONMENT_ARTIFACT_PIN.ARTIFACT_UID.eq(artifact.uid))
.and(ENVIRONMENT_ARTIFACT_PIN.ARTIFACT_VERSION.eq(version))
.fetchOne { (pinnedBy, pinnedAt, comment) ->
ActionMetadata(at = pinnedAt, by = pinnedBy, comment = comment)
}
}
ArtifactSummaryInEnvironment(
environment = environmentName,
version = version,
state = promotionStatus.name.toLowerCase(),
deployedAt = deployedAt,
replacedAt = replacedAt,
replacedBy = replacedBy,
pinned = pinned,
vetoed = vetoed
)
}
}
}
override fun getArtifactPromotionStatus(
deliveryConfig: DeliveryConfig,
artifact: DeliveryArtifact,
version: String,
environmentName: String
): PromotionStatus? {
return sqlRetry.withRetry(READ) {
jooq
.select(ENVIRONMENT_ARTIFACT_VERSIONS.PROMOTION_STATUS)
.from(ENVIRONMENT_ARTIFACT_VERSIONS)
.where(ENVIRONMENT_ARTIFACT_VERSIONS.ENVIRONMENT_UID.eq(deliveryConfig.getUidFor(environmentName)))
.and(ENVIRONMENT_ARTIFACT_VERSIONS.ARTIFACT_UID.eq(artifact.uid))
.and(ENVIRONMENT_ARTIFACT_VERSIONS.ARTIFACT_VERSION.eq(version))
.fetchOne(ENVIRONMENT_ARTIFACT_VERSIONS.PROMOTION_STATUS)
}
}
override fun itemsDueForCheck(minTimeSinceLastCheck: Duration, limit: Int): Collection {
val now = clock.instant()
val cutoff = now.minus(minTimeSinceLastCheck)
return sqlRetry.withRetry(WRITE) {
jooq.inTransaction {
select(
DELIVERY_ARTIFACT.UID,
DELIVERY_ARTIFACT.NAME,
DELIVERY_ARTIFACT.TYPE,
DELIVERY_ARTIFACT.DETAILS,
DELIVERY_ARTIFACT.REFERENCE,
DELIVERY_ARTIFACT.DELIVERY_CONFIG_NAME,
ARTIFACT_LAST_CHECKED.AT
)
.from(DELIVERY_ARTIFACT, ARTIFACT_LAST_CHECKED)
.where(DELIVERY_ARTIFACT.UID.eq(ARTIFACT_LAST_CHECKED.ARTIFACT_UID))
.and(ARTIFACT_LAST_CHECKED.AT.lessOrEqual(cutoff))
.orderBy(ARTIFACT_LAST_CHECKED.AT)
.limit(limit)
.forUpdate()
.fetch()
.also {
it.forEach { (uid, _, _, _, _, deliveryConfigName, lastCheckedAt) ->
insertInto(ARTIFACT_LAST_CHECKED)
.set(ARTIFACT_LAST_CHECKED.ARTIFACT_UID, uid)
.set(ARTIFACT_LAST_CHECKED.AT, now)
.onDuplicateKeyUpdate()
.set(ARTIFACT_LAST_CHECKED.AT, now)
.execute()
publisher.publishEvent(
AboutToBeChecked(
lastCheckedAt,
"artifact",
"deliveryConfig:$deliveryConfigName"
)
)
}
}
.map { (_, name, type, details, reference, deliveryConfigName) ->
mapToArtifact(artifactSuppliers.supporting(type), name, type, details, reference, deliveryConfigName)
}
}
}
}
override fun deploymentsBetween(
deliveryConfig: DeliveryConfig,
environmentName: String,
startTime: TemporalAccessor,
endTime: TemporalAccessor
): Int =
(Instant.from(startTime) to Instant.from(endTime)).let { (start, end) ->
require(start < end) {
"Start time $startTime must be before end time $endTime"
}
jooq
.selectCount()
.from(ENVIRONMENT_ARTIFACT_VERSIONS)
.join(ENVIRONMENT).on(ENVIRONMENT.UID.eq(ENVIRONMENT_ARTIFACT_VERSIONS.ENVIRONMENT_UID))
.join(DELIVERY_CONFIG).on(DELIVERY_CONFIG.UID.eq(ENVIRONMENT.DELIVERY_CONFIG_UID))
.where(DELIVERY_CONFIG.NAME.eq(deliveryConfig.name))
.and(ENVIRONMENT.NAME.eq(environmentName))
.and(ENVIRONMENT_ARTIFACT_VERSIONS.DEPLOYED_AT.between(start, end))
.fetchSingleInto()
}
private fun priorVersionDeployedIn(
environmentId: String,
artifactId: String,
currentVersion: String
): String? {
return sqlRetry.withRetry(READ) {
jooq
.select(ENVIRONMENT_ARTIFACT_VERSIONS.ARTIFACT_VERSION)
.from(ENVIRONMENT_ARTIFACT_VERSIONS)
.where(ENVIRONMENT_ARTIFACT_VERSIONS.ENVIRONMENT_UID.eq(environmentId))
.and(ENVIRONMENT_ARTIFACT_VERSIONS.ARTIFACT_UID.eq(artifactId))
.and(ENVIRONMENT_ARTIFACT_VERSIONS.ARTIFACT_VERSION.ne(currentVersion))
.and(ENVIRONMENT_ARTIFACT_VERSIONS.DEPLOYED_AT.isNotNull)
.and(ENVIRONMENT_ARTIFACT_VERSIONS.PROMOTION_STATUS.ne(VETOED))
.orderBy(ENVIRONMENT_ARTIFACT_VERSIONS.DEPLOYED_AT.desc())
.limit(1)
.fetchOne(ENVIRONMENT_ARTIFACT_VERSIONS.ARTIFACT_VERSION)
}
}
private fun deletePin(envUid: String, artUid: String) {
// Deletes rows by primary key
jooq.deleteFrom(ENVIRONMENT_ARTIFACT_PIN)
.where(
ENVIRONMENT_ARTIFACT_PIN.ENVIRONMENT_UID.eq(envUid),
ENVIRONMENT_ARTIFACT_PIN.ARTIFACT_UID.eq(artUid)
)
.execute()
}
private fun DeliveryConfig.environmentNamed(name: String): Environment =
requireNotNull(environments.firstOrNull { it.name == name }) {
"No environment named $name exists in the configuration ${this.name}"
}
private fun DeliveryConfig.getUidFor(environment: Environment): Select> =
select(ACTIVE_ENVIRONMENT.UID)
.from(ACTIVE_ENVIRONMENT)
.where(ACTIVE_ENVIRONMENT.NAME.eq(environment.name))
.and(ACTIVE_ENVIRONMENT.DELIVERY_CONFIG_UID.eq(uid))
private fun DeliveryConfig.getUidFor(environmentName: String): Select> =
select(ACTIVE_ENVIRONMENT.UID)
.from(ACTIVE_ENVIRONMENT)
.where(ACTIVE_ENVIRONMENT.NAME.eq(environmentName))
.and(ACTIVE_ENVIRONMENT.DELIVERY_CONFIG_UID.eq(uid))
private fun DeliveryConfig.getUidStringFor(environment: Environment): String =
jooq.select(ACTIVE_ENVIRONMENT.UID)
.from(ACTIVE_ENVIRONMENT)
.where(ACTIVE_ENVIRONMENT.NAME.eq(environment.name))
.and(ACTIVE_ENVIRONMENT.DELIVERY_CONFIG_UID.eq(uid))
.fetchOne(ACTIVE_ENVIRONMENT.UID) ?: error("environment not found for $name / ${environment.name}")
private val DeliveryArtifact.uid: Select>
get() = select(DELIVERY_ARTIFACT.UID)
.from(DELIVERY_ARTIFACT)
.where(
DELIVERY_ARTIFACT.NAME.eq(name)
.and(DELIVERY_ARTIFACT.TYPE.eq(type))
.and(DELIVERY_ARTIFACT.DELIVERY_CONFIG_NAME.eq(deliveryConfigName))
.and(DELIVERY_ARTIFACT.REFERENCE.eq(reference))
)
private val DeliveryArtifact.uidString: String
get() = sqlRetry.withRetry(READ) {
jooq.select(DELIVERY_ARTIFACT.UID)
.from(DELIVERY_ARTIFACT)
.where(
DELIVERY_ARTIFACT.NAME.eq(name)
.and(DELIVERY_ARTIFACT.TYPE.eq(type))
.and(DELIVERY_ARTIFACT.DELIVERY_CONFIG_NAME.eq(deliveryConfigName))
.and(DELIVERY_ARTIFACT.REFERENCE.eq(reference))
)
.fetchOne(DELIVERY_ARTIFACT.UID) ?: error(
"artifact not found for " +
"name=$name, " +
"type=$type, " +
"deliveryConfig=$deliveryConfigName, " +
"reference=$reference"
)
}
private val DeliveryConfig.uid: Select>
get() = select(DELIVERY_CONFIG.UID)
.from(DELIVERY_CONFIG)
// TODO: currently this is unique but I feel like it should be a compound key with application name
.where(DELIVERY_CONFIG.NAME.eq(name))
// Generates a unique hash for an artifact
private fun DeliveryArtifact.fingerprint(): String {
return fingerprint(name, type, deliveryConfigName ?: "_pending", reference)
}
private fun fingerprint(name: String, type: String, deliveryConfigName: String, reference: String): String {
val data = name + type + deliveryConfigName + reference
val bytes = MessageDigest
.getInstance("SHA-1")
.digest(data.toByteArray())
return DatatypeConverter.printHexBinary(bytes).toUpperCase()
}
private fun vetoKey(envName: String, artifactId: String) = "$envName:$artifactId"
private val log by lazy { LoggerFactory.getLogger(javaClass) }
}