Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix get image auth digest #530

Merged
merged 2 commits into from
Jun 13, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -351,7 +351,7 @@ class ContainerController {
}

protected BuildTrack checkBuild(BuildRequest build, boolean dryRun) {
final digest = registryProxyService.getImageDigest(build.targetImage)
final digest = registryProxyService.getImageDigest(build)
// check for dry-run execution
if( dryRun ) {
log.debug "== Dry-run build request: $build"
Expand Down
24 changes: 7 additions & 17 deletions src/main/groovy/io/seqera/wave/core/RegistryProxyService.groovy
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ import io.seqera.wave.http.HttpClientFactory
import io.seqera.wave.model.ContainerCoordinates
import io.seqera.wave.proxy.ProxyClient
import io.seqera.wave.service.CredentialsService
import io.seqera.wave.service.builder.BuildRequest
import io.seqera.wave.service.persistence.PersistenceService
import io.seqera.wave.storage.DigestStore
import io.seqera.wave.storage.Storage
Expand All @@ -45,7 +46,6 @@ import jakarta.inject.Singleton
import reactor.core.publisher.Flux
import static io.seqera.wave.WaveDefault.HTTP_REDIRECT_CODES
import static io.seqera.wave.WaveDefault.HTTP_RETRYABLE_ERRORS

/**
* Proxy service that forwards incoming container request
* to the target repository, resolving credentials and augmentation
Expand Down Expand Up @@ -188,33 +188,23 @@ class RegistryProxyService {
}
}

@Deprecated
boolean isManifestPresent(String image){
try {
return getImageDigest0(image,false) != null
}
catch(Exception e) {
log.warn "Unable to check status for container image '$image' -- cause: ${e.message}"
return false
}
}

String getImageDigest(String image, boolean retryOnNotFound=false) {
String getImageDigest(BuildRequest request, boolean retryOnNotFound=false) {
try {
return getImageDigest0(image, retryOnNotFound)
return getImageDigest0(request, retryOnNotFound)
}
catch(Exception e) {
log.warn "Unable to retrieve digest for image '$image' -- cause: ${e.message}"
log.warn "Unable to retrieve digest for image '${request.getTargetImage()}' -- cause: ${e.message}"
return null
}
}

static private List<Integer> RETRY_ON_NOT_FOUND = HTTP_RETRYABLE_ERRORS + 404

@Cacheable(value = 'cache-20sec', atomic = true)
protected String getImageDigest0(String image, boolean retryOnNotFound) {
protected String getImageDigest0(BuildRequest request, boolean retryOnNotFound) {
final image = request.targetImage
final coords = ContainerCoordinates.parse(image)
final route = RoutePath.v2manifestPath(coords)
final route = RoutePath.v2manifestPath(coords, request.identity)
final proxyClient = client(route)
.withRetryableHttpErrors(retryOnNotFound ? RETRY_ON_NOT_FOUND : HTTP_RETRYABLE_ERRORS)
final resp = proxyClient.head(route.path, WaveDefault.ACCEPT_HEADERS)
Expand Down
5 changes: 3 additions & 2 deletions src/main/groovy/io/seqera/wave/core/RoutePath.groovy
Original file line number Diff line number Diff line change
Expand Up @@ -124,8 +124,9 @@ class RoutePath implements ContainerPath {
new RoutePath(type, registry ?: DOCKER_IO, image, ref, "/v2/$image/$type/$ref", request, token)
}

static RoutePath v2manifestPath(ContainerCoordinates container) {
new RoutePath('manifests', container.registry, container.image, container.reference, "/v2/${container.image}/manifests/${container.reference}")
static RoutePath v2manifestPath(ContainerCoordinates container, PlatformId identity=null) {
ContainerRequestData data = identity!=null ? new ContainerRequestData(identity) : null
return new RoutePath('manifests', container.registry, container.image, container.reference, "/v2/${container.image}/manifests/${container.reference}", data)
}

static RoutePath empty() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ class DockerBuildStrategy extends BuildStrategy {
final completed = proc.waitFor(buildConfig.buildTimeout.toSeconds(), TimeUnit.SECONDS)
final stdout = proc.inputStream.text
if( completed ) {
final digest = proc.exitValue()==0 ? proxyService.getImageDigest(req.targetImage,true) : null
final digest = proc.exitValue()==0 ? proxyService.getImageDigest(req, true) : null
return BuildResult.completed(req.buildId, proc.exitValue(), stdout, req.startTime, digest)
}
else {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@ class KubeBuildStrategy extends BuildStrategy {
final terminated = k8sService.waitPod(pod, buildConfig.buildTimeout.toMillis())
final stdout = k8sService.logsPod(name)
if( terminated ) {
final digest = terminated.exitCode==0 ? proxyService.getImageDigest(req.targetImage,true) : null
final digest = terminated.exitCode==0 ? proxyService.getImageDigest(req, true) : null
return BuildResult.completed(req.buildId, terminated.exitCode, stdout, req.startTime, digest)
}
else {
Expand Down
29 changes: 19 additions & 10 deletions src/test/groovy/io/seqera/wave/core/RegistryProxyServiceTest.groovy
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,14 @@

package io.seqera.wave.core

import spock.lang.Requires
import spock.lang.Shared
import spock.lang.Specification

import io.micronaut.context.ApplicationContext
import io.micronaut.test.extensions.spock.annotation.MicronautTest
import io.seqera.wave.service.builder.BuildRequest
import io.seqera.wave.tower.PlatformId
import jakarta.inject.Inject
/**
*
Expand All @@ -38,25 +41,31 @@ class RegistryProxyServiceTest extends Specification {
@Inject RegistryProxyService registryProxyService


def 'should check manifest exist' () {
def 'should retrieve image digest' () {
given:
def IMAGE = 'library/hello-world:latest'
def IMAGE = 'library/hello-world@sha256:6352af1ab4ba4b138648f8ee88e63331aae519946d3b67dae50c313c6fc8200f'
def request = Mock(BuildRequest)

when:
def resp1 = registryProxyService.isManifestPresent(IMAGE)

def resp1 = registryProxyService.getImageDigest(request)
then:
resp1
request.getTargetImage() >> IMAGE
then:
resp1 == 'sha256:6352af1ab4ba4b138648f8ee88e63331aae519946d3b67dae50c313c6fc8200f'
}

def 'should retrieve image digest' () {
@Requires({System.getenv('AWS_ACCESS_KEY_ID') && System.getenv('AWS_SECRET_ACCESS_KEY')})
def 'should retrive image digest on ECR' () {
given:
def IMAGE = 'library/hello-world@sha256:6352af1ab4ba4b138648f8ee88e63331aae519946d3b67dae50c313c6fc8200f'
def IMAGE = '195996028523.dkr.ecr.eu-west-1.amazonaws.com/wave/kaniko:0.1.0'
def request = Mock(BuildRequest)

when:
def resp1 = registryProxyService.getImageDigest(IMAGE)

def resp1 = registryProxyService.getImageDigest(request)
then:
resp1 == 'sha256:6352af1ab4ba4b138648f8ee88e63331aae519946d3b67dae50c313c6fc8200f'
request.getTargetImage() >> IMAGE
request.getIdentity() >> new PlatformId()
then:
resp1 == 'sha256:05f9dc67e6ec879773de726b800d4d5044f8bd8e67da728484fbdea56af1fdff'
}
}
17 changes: 16 additions & 1 deletion src/test/groovy/io/seqera/wave/core/RoutePathTest.groovy
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,12 @@
package io.seqera.wave.core

import spock.lang.Specification
import spock.lang.Unroll

import io.seqera.wave.model.ContainerCoordinates
import io.seqera.wave.service.ContainerRequestData
import io.seqera.wave.tower.PlatformId
import io.seqera.wave.tower.User

/**
*
* @author Paolo Di Tommaso <[email protected]>
Expand Down Expand Up @@ -72,6 +72,7 @@ class RoutePathTest extends Specification {

}

@Unroll
def 'should get manifest path'() {
expect:
RoutePath.v2manifestPath(ContainerCoordinates.parse(CONTAINER)).path == PATH
Expand All @@ -82,6 +83,20 @@ class RoutePathTest extends Specification {
'quay.io/foo/bar:v1.0' | '/v2/foo/bar/manifests/v1.0'
}

def 'should get manifest path with identity'() {
given:
def CONTAINER = ContainerCoordinates.parse('quay.io/foo/bar:v1.0')
def PATH = '/v2/foo/bar/manifests/v1.0'
def IDENTITY = new PlatformId(new User(id: 1, email: '[email protected]'), 2, 'xyz')

expect:
RoutePath.v2manifestPath(CONTAINER).path == PATH
RoutePath.v2manifestPath(CONTAINER).identity == PlatformId.NULL
and:
RoutePath.v2manifestPath(CONTAINER, IDENTITY).path == PATH
RoutePath.v2manifestPath(CONTAINER, IDENTITY).identity == IDENTITY
}

def 'should parse location' () {
expect:
RoutePath.parse(GIVEN) == RoutePath.v2path(TYPE, REG, IMAGE, REF)
Expand Down
21 changes: 21 additions & 0 deletions src/test/groovy/io/seqera/wave/proxy/ProxyClientTest.groovy
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,27 @@ class ProxyClientTest extends Specification {
resp.statusCode() == 200
}

@Requires({System.getenv('AWS_ACCESS_KEY_ID') && System.getenv('AWS_SECRET_ACCESS_KEY')})
def 'should call head manifest on amazon' () {
given:
def IMAGE = 'wave/kaniko'
def REG = '195996028523.dkr.ecr.eu-west-1.amazonaws.com'
def registry = lookupService.lookup(REG)
def creds = credentialsProvider.getDefaultCredentials(REG)
def httpClient = HttpClientFactory.neverRedirectsHttpClient()
and:
def proxy = new ProxyClient(httpClient, httpConfig)
.withImage(IMAGE)
.withRegistry(registry)
.withLoginService(loginService)
.withCredentials(creds)

when:
def resp = proxy.head("/v2/$IMAGE/manifests/0.1.0")
then:
resp.statusCode() == 200
}

@Requires({System.getenv('AWS_ACCESS_KEY_ID') && System.getenv('AWS_SECRET_ACCESS_KEY')})
def 'should call target manifest on ecr public' () {
given:
Expand Down