Skip to content

Commit

Permalink
feat(titus): Improve error message when image is too old
Browse files Browse the repository at this point in the history
(cherry picked from commit dfc06ba9055c33075c2e2ab3b3047a8409d6a7e3)
  • Loading branch information
luispollo committed Sep 1, 2021
1 parent d9eeff1 commit c143cdc
Show file tree
Hide file tree
Showing 5 changed files with 108 additions and 11 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ import com.netflix.spinnaker.kork.exceptions.ConfigurationException
* Implement the methods in this interface for each cloud provider.
*/
abstract class DockerImageResolver<T : ResourceSpec>(
val repository: KeelRepository
open val repository: KeelRepository
) : Resolver<T> {

/**
Expand Down Expand Up @@ -68,7 +68,7 @@ abstract class DockerImageResolver<T : ResourceSpec>(
/**De
* Get the digest for a specific image
*/
abstract fun getDigest(account: String, organization: String, image: String, tag: String): String
abstract fun getDigest(account: String, artifact: DockerArtifact, tag: String): String

override fun invoke(resource: Resource<T>): Resource<T> {
val container = getContainerFromSpec(resource)
Expand Down Expand Up @@ -134,7 +134,7 @@ abstract class DockerImageResolver<T : ResourceSpec>(
artifact: DockerArtifact,
tag: String
): DigestProvider {
val digest = getDigest(account, artifact.organization, artifact.image, tag)
val digest = getDigest(account, artifact, tag)
return DigestProvider(
organization = artifact.organization,
image = artifact.image,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ class SampleDockerImageResolver(
listOf("latest", "v0.0.1", "v0.0.2", "v0.0.4", "v0.1.1", "v0.1.0")

// this would normally call out to clouddriver
override fun getDigest(account: String, organization: String, image: String, tag: String) =
override fun getDigest(account: String, artifact: DockerArtifact, tag: String) =
when (tag) {
"v0.0.1" -> "sha256:2763a2b9d53e529c62b326b7331d1b44aae344be0b79ff64c74559c5c96b76b7"
"v0.0.2" -> "sha256:b4857d7596462aeb1977e6e5d1e31b20a5b5eecf890cd64ac62f145b3839ee97"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,18 +26,22 @@ import com.netflix.spinnaker.keel.clouddriver.CloudDriverService
import com.netflix.spinnaker.keel.docker.ContainerProvider
import com.netflix.spinnaker.keel.docker.DockerImageResolver
import com.netflix.spinnaker.keel.persistence.KeelRepository
import com.netflix.spinnaker.keel.titus.exceptions.ImageTooOld
import com.netflix.spinnaker.keel.titus.exceptions.NoDigestFound
import com.netflix.spinnaker.keel.titus.exceptions.RegistryNotFound
import kotlinx.coroutines.runBlocking
import org.slf4j.LoggerFactory
import org.springframework.stereotype.Component
import java.time.Clock
import java.time.Duration

/**
* Assumption: docker container digest is the same in all regions
*/
@Component
class TitusImageResolver(
repository: KeelRepository,
override val repository: KeelRepository,
private val clock: Clock,
private val cloudDriverCache: CloudDriverCache,
private val cloudDriverService: CloudDriverService
) : DockerImageResolver<TitusClusterSpec>(
Expand Down Expand Up @@ -73,15 +77,28 @@ class TitusImageResolver(
cloudDriverService.findDockerTagsForImage(account, repository)
}

override fun getDigest(account: String, organization: String, image: String, tag: String) =
override fun getDigest(account: String, artifact: DockerArtifact, tag: String) =
runBlocking {
val repository = "$organization/$image"
val images = cloudDriverService.findDockerImages(account, repository, tag)
images.firstOrNull()?.digest
?: throw NoDigestFound(repository, tag) // sha should be the same in all accounts for titus
val images = cloudDriverService.findDockerImages(account, artifact.name, tag)
val digest = images.firstOrNull()?.digest

if (digest == null) {
val publishedArtifact = repository.getArtifactVersion(artifact, tag)
if (publishedArtifact?.createdAt?.isBefore(clock.instant() - TITUS_REGISTRY_IMAGE_TTL) == true) {
throw ImageTooOld(artifact.name, tag, publishedArtifact.createdAt!!)
} else {
throw NoDigestFound(artifact.name, tag) // sha should be the same in all accounts for titus
}
}

digest
}

protected fun TitusClusterSpec.deriveRegistry(): String =
cloudDriverCache.credentialBy(locations.account).attributes["registry"]?.toString()
?: throw RegistryNotFound(locations.account)

companion object {
val TITUS_REGISTRY_IMAGE_TTL: Duration = Duration.ofDays(60)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,13 @@ package com.netflix.spinnaker.keel.titus.exceptions

import com.netflix.spinnaker.keel.core.ResourceCurrentlyUnresolvable
import com.netflix.spinnaker.kork.exceptions.IntegrationException
import java.time.Instant

class NoDigestFound(repository: String, tag: String) :
ResourceCurrentlyUnresolvable("No digest found for docker image $repository:$tag in any registry")

class RegistryNotFound(titusAccount: String) :
IntegrationException("Unable to find docker registry for titus account $titusAccount")
IntegrationException("Unable to find docker registry for Titus account $titusAccount")

class ImageTooOld(repository: String, tag: String, createdAt: Instant) :
ResourceCurrentlyUnresolvable("The docker image $repository:$tag (created at $createdAt) is too old. To fix this, please publish a new image.")
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
package com.netflix.spinnaker.keel.titus

import com.netflix.spinnaker.keel.clouddriver.CloudDriverCache
import com.netflix.spinnaker.keel.clouddriver.CloudDriverService
import com.netflix.spinnaker.keel.persistence.KeelRepository
import com.netflix.spinnaker.keel.test.dockerArtifact
import com.netflix.spinnaker.keel.titus.TitusImageResolver.Companion.TITUS_REGISTRY_IMAGE_TTL
import com.netflix.spinnaker.keel.titus.exceptions.ImageTooOld
import com.netflix.spinnaker.keel.titus.exceptions.NoDigestFound
import com.netflix.spinnaker.time.MutableClock
import io.mockk.coEvery as every
import io.mockk.mockk
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
import strikt.api.expectCatching
import strikt.assertions.isA
import strikt.assertions.isFailure
import java.time.Duration

class TitusImageResolverTests {
private val repository: KeelRepository = mockk()
private val cloudDriverService: CloudDriverService = mockk()
private val cloudDriverCache: CloudDriverCache = mockk()
private val clock = MutableClock()
private val subject = TitusImageResolver(repository, clock, cloudDriverCache, cloudDriverService)
private val dockerArtifact = dockerArtifact()

@BeforeEach
fun setup() {
every {
cloudDriverService.findDockerImages(any(), any(), any())
} returns emptyList()
}

@Test
fun `throws NoDigestFound if digest not found and published version not known`() {
every {
repository.getArtifactVersion(dockerArtifact, any())
} returns null

expectCatching {
subject.getDigest("test", dockerArtifact, "1.0.0")
}.isFailure()
.isA<NoDigestFound>()
}

@Test
fun `throws NoDigestFound if digest not found and published version within TTL`() {
every {
repository.getArtifactVersion(dockerArtifact, any())
} returns dockerArtifact.toArtifactVersion(
version = "1.0.0",
createdAt = clock.instant() - TITUS_REGISTRY_IMAGE_TTL + Duration.ofDays(5)
)

expectCatching {
subject.getDigest("test", dockerArtifact, "1.0.0")
}.isFailure()
.isA<NoDigestFound>()
}

@Test
fun `throws ImageTooOld if digest not found and published version is too old`() {
every {
repository.getArtifactVersion(dockerArtifact, any())
} returns dockerArtifact.toArtifactVersion(
version = "1.0.0",
createdAt = clock.instant() - TITUS_REGISTRY_IMAGE_TTL - Duration.ofDays(5)
)

expectCatching {
subject.getDigest("test", dockerArtifact, "1.0.0")
}.isFailure()
.isA<ImageTooOld>()
}
}

0 comments on commit c143cdc

Please sign in to comment.