From 51adc6fed4ae853730b1571f3bae5786aca8ed7f Mon Sep 17 00:00:00 2001 From: kirangodishala Date: Wed, 29 Jan 2025 22:20:50 +0530 Subject: [PATCH] refactor(retrofit2): Upgrade DockerRegistryService client from retrofit to retrofit2 --- clouddriver-docker/clouddriver-docker.gradle | 6 +- .../v2/auth/DockerBearerTokenService.groovy | 4 - .../api/v2/client/DockerRegistryClient.groovy | 149 ++++++++++-------- .../client/DefaultDockerOkClientProvider.java | 5 +- .../api/v2/client/DockerOkClientProvider.java | 6 +- .../v2/client/DockerRegistryClientSpec.groovy | 82 +++++++--- ...erRegistryNamedAccountCredentialsTest.java | 118 +++++++------- 7 files changed, 214 insertions(+), 156 deletions(-) diff --git a/clouddriver-docker/clouddriver-docker.gradle b/clouddriver-docker/clouddriver-docker.gradle index 3ed98c905c..647dc64f30 100644 --- a/clouddriver-docker/clouddriver-docker.gradle +++ b/clouddriver-docker/clouddriver-docker.gradle @@ -9,10 +9,10 @@ dependencies { implementation "org.springframework.cloud:spring-cloud-context" implementation "org.apache.groovy:groovy" implementation "com.google.guava:guava" - implementation "com.jakewharton.retrofit:retrofit1-okhttp3-client" +// implementation "com.jakewharton.retrofit:retrofit1-okhttp3-client" implementation "com.netflix.spectator:spectator-api" - implementation "com.squareup.retrofit:converter-jackson" - implementation "com.squareup.retrofit:retrofit" + implementation "com.squareup.retrofit2:converter-jackson" +// implementation "com.squareup.retrofit:retrofit" implementation "org.apache.commons:commons-compress:1.21" implementation "commons-io:commons-io" implementation "io.spinnaker.fiat:fiat-api:$fiatVersion" diff --git a/clouddriver-docker/src/main/groovy/com/netflix/spinnaker/clouddriver/docker/registry/api/v2/auth/DockerBearerTokenService.groovy b/clouddriver-docker/src/main/groovy/com/netflix/spinnaker/clouddriver/docker/registry/api/v2/auth/DockerBearerTokenService.groovy index c933df6ed2..ec4595f326 100644 --- a/clouddriver-docker/src/main/groovy/com/netflix/spinnaker/clouddriver/docker/registry/api/v2/auth/DockerBearerTokenService.groovy +++ b/clouddriver-docker/src/main/groovy/com/netflix/spinnaker/clouddriver/docker/registry/api/v2/auth/DockerBearerTokenService.groovy @@ -21,11 +21,8 @@ import com.netflix.spinnaker.clouddriver.docker.registry.api.v2.exception.Docker import com.netflix.spinnaker.config.DefaultServiceEndpoint import com.netflix.spinnaker.kork.client.ServiceClientProvider import com.netflix.spinnaker.kork.retrofit.Retrofit2SyncCall -import com.netflix.spinnaker.kork.retrofit.exceptions.SpinnakerRetrofitErrorHandler import groovy.util.logging.Slf4j import org.apache.commons.io.IOUtils -import retrofit.RestAdapter -import retrofit.converter.JacksonConverter import retrofit2.Call import retrofit2.http.GET import retrofit2.http.Header @@ -33,7 +30,6 @@ import retrofit2.http.Headers import retrofit2.http.Path import retrofit2.http.Query -import java.nio.charset.Charset import java.nio.charset.StandardCharsets @Slf4j diff --git a/clouddriver-docker/src/main/groovy/com/netflix/spinnaker/clouddriver/docker/registry/api/v2/client/DockerRegistryClient.groovy b/clouddriver-docker/src/main/groovy/com/netflix/spinnaker/clouddriver/docker/registry/api/v2/client/DockerRegistryClient.groovy index 37ec78f701..29b209a2cc 100644 --- a/clouddriver-docker/src/main/groovy/com/netflix/spinnaker/clouddriver/docker/registry/api/v2/client/DockerRegistryClient.groovy +++ b/clouddriver-docker/src/main/groovy/com/netflix/spinnaker/clouddriver/docker/registry/api/v2/client/DockerRegistryClient.groovy @@ -16,6 +16,7 @@ package com.netflix.spinnaker.clouddriver.docker.registry.api.v2.client +import com.fasterxml.jackson.databind.ObjectMapper import com.google.gson.Gson import com.google.gson.GsonBuilder import com.netflix.spinnaker.clouddriver.docker.registry.api.v2.DockerUserAgent @@ -23,22 +24,28 @@ import com.netflix.spinnaker.clouddriver.docker.registry.api.v2.auth.DockerBeare import com.netflix.spinnaker.clouddriver.docker.registry.api.v2.auth.DockerBearerTokenService import com.netflix.spinnaker.clouddriver.docker.registry.api.v2.exception.DockerRegistryAuthenticationException import com.netflix.spinnaker.clouddriver.docker.registry.api.v2.exception.DockerRegistryOperationException +import com.netflix.spinnaker.config.DefaultServiceEndpoint import com.netflix.spinnaker.kork.client.ServiceClientProvider +import com.netflix.spinnaker.kork.retrofit.ErrorHandlingExecutorCallAdapterFactory +import com.netflix.spinnaker.kork.retrofit.Retrofit2SyncCall import com.netflix.spinnaker.kork.retrofit.exceptions.SpinnakerHttpException +import com.netflix.spinnaker.kork.retrofit.exceptions.SpinnakerNetworkException import com.netflix.spinnaker.kork.retrofit.exceptions.SpinnakerRetrofitErrorHandler import com.netflix.spinnaker.kork.retrofit.exceptions.SpinnakerServerException import groovy.util.logging.Slf4j +import okhttp3.ResponseBody import org.slf4j.Logger import org.slf4j.LoggerFactory -import retrofit.RestAdapter -import retrofit.client.Response import retrofit.converter.GsonConverter -import retrofit.converter.JacksonConverter -import retrofit.http.GET -import retrofit.http.Header -import retrofit.http.Headers -import retrofit.http.Path -import retrofit.http.Query +import retrofit2.Response +import retrofit2.converter.jackson.JacksonConverterFactory; +import retrofit2.Call +import retrofit2.Retrofit +import retrofit2.http.GET +import retrofit2.http.Header +import retrofit2.http.Headers +import retrofit2.http.Path +import retrofit2.http.Query import java.time.Instant @@ -177,15 +184,13 @@ class DockerRegistryClient { this.paginateSize = paginateSize this.tokenService = new DockerBearerTokenService(serviceClientProvider) - - this.registryService = new RestAdapter.Builder() - .setEndpoint(address) - .setClient(okClientProvider.provide(address, clientTimeoutMillis, insecureRegistry)) - .setConverter(new JacksonConverter()) - .setLogLevel(RestAdapter.LogLevel.NONE) - .setErrorHandler(SpinnakerRetrofitErrorHandler.getInstance()) + this.registryService = new Retrofit.Builder() + .baseUrl(address) + .client(okClientProvider.provide(address, clientTimeoutMillis, insecureRegistry)) + .addCallAdapterFactory(ErrorHandlingExecutorCallAdapterFactory.getInstance()) + .addConverterFactory(JacksonConverterFactory.create()) .build() - .create(DockerRegistryService) + .create(DockerRegistryService); this.converter = new GsonConverter(new GsonBuilder().create()) this.address = address this.catalogFile = catalogFile @@ -245,45 +250,45 @@ class DockerRegistryClient { @Headers([ "Docker-Distribution-API-Version: registry/2.0" ]) - Response getTags(@Path(value="repository", encode=false) String repository, @Header("Authorization") String token, @Header("User-Agent") String agent) + Call getTags(@Path(value="repository", encoded=true) String repository, @Header("Authorization") String token, @Header("User-Agent") String agent) @GET("/v2/{name}/manifests/{reference}") @Headers([ "Docker-Distribution-API-Version: registry/2.0" ]) - Response getManifest(@Path(value="name", encode=false) String name, @Path(value="reference", encode=false) String reference, @Header("Authorization") String token, @Header("User-Agent") String agent) + Call getManifest(@Path(value="name", encoded=true) String name, @Path(value="reference", encoded=true) String reference, @Header("Authorization") String token, @Header("User-Agent") String agent) @GET("/v2/{name}/manifests/{reference}") @Headers([ "Docker-Distribution-API-Version: registry/2.0", "Accept: application/vnd.docker.distribution.manifest.v2+json" ]) - Response getSchemaV2Manifest(@Path(value="name", encode=false) String name, @Path(value="reference", encode=false) String reference, @Header("Authorization") String token, @Header("User-Agent") String agent) + Call getSchemaV2Manifest(@Path(value="name", encoded=true) String name, @Path(value="reference", encoded=true) String reference, @Header("Authorization") String token, @Header("User-Agent") String agent) @GET("/v2/_catalog") @Headers([ "Docker-Distribution-API-Version: registry/2.0" ]) - Response getCatalog(@Query(value="n") int paginateSize, @Header("Authorization") String token, @Header("User-Agent") String agent) + Call getCatalog(@Query(value="n") int paginateSize, @Header("Authorization") String token, @Header("User-Agent") String agent) @GET("/{path}") @Headers([ "Docker-Distribution-API-Version: registry/2.0" ]) - Response get(@Path(value="path", encode=false) String path, @Header("Authorization") String token, @Header("User-Agent") String agent) + Call get(@Path(value="path", encoded=true) String path, @Header("Authorization") String token, @Header("User-Agent") String agent) @GET("/v2/") @Headers([ "User-Agent: Spinnaker-Clouddriver", "Docker-Distribution-API-Version: registry/2.0" ]) - Response checkVersion(@Header("Authorization") String token, @Header("User-Agent") String agent) + Call checkVersion(@Header("Authorization") String token, @Header("User-Agent") String agent) @GET("/v2/{repository}/blobs/{digest}") @Headers([ "Docker-Distribution-API-Version: registry/2.0" ]) - Response getDigestContent(@Path(value="repository", encode=false) String repository, @Path(value="digest", encode=false) String digest, @Header("Authorization") String token, @Header("User-Agent") String agent) + Call getDigestContent(@Path(value="repository", encoded=true) String repository, @Path(value="digest", encoded=true) String digest, @Header("Authorization") String token, @Header("User-Agent") String agent) } public String getDigest(String name, String tag) { @@ -297,17 +302,31 @@ class DockerRegistryClient { public String getConfigDigest(String name, String tag) { def response = getSchemaV2Manifest(name, tag) - def manifestMap = converter.fromBody(response.body, Map) as Map + def manifestMap = convertResponseBody(response.body(), Map) return manifestMap?.config?.digest } public Map getDigestContent(String name, String digest) { def response = request({ - registryService.getDigestContent(name, digest, tokenService.basicAuthHeader, userAgent) + Retrofit2SyncCall.executeCall(registryService.getDigestContent(name, digest, tokenService.basicAuthHeader, userAgent)) }, { token -> - registryService.getDigestContent(name, digest, token, userAgent) + Retrofit2SyncCall.executeCall(registryService.getDigestContent(name, digest, token, userAgent)) }, name) - return converter.fromBody(response.body, Map) + + return convertResponseBody(response.body(), Map) + } + + static def convertResponseBody(ResponseBody responseBody, Class aClass) { + if (responseBody == null) { + throw new DockerRegistryOperationException("ResponseBody cannot be null") + } + try { + def objectMapper = new ObjectMapper() + def jsonString = responseBody.string() + return objectMapper.readValue(jsonString, aClass) + } catch (Exception e) { + throw new DockerRegistryOperationException("Failed to parse ResponseBody : ${e.message}", e) + } } private Map tagDateCache = [:] @@ -317,7 +336,7 @@ class DockerRegistryClient { if(tagDateCache.containsKey(key) && tag !='latest'){ return tagDateCache[key] } - Map manifest = converter.fromBody(getManifest(name, tag).body, Map) + Map manifest = convertResponseBody(getManifest(name, tag).body(), Map) Instant dateCreated = Instant.parse(new Gson().fromJson(manifest.history[0].v1Compatibility, Map).created) tagDateCache[key] = dateCreated dateCreated @@ -325,26 +344,24 @@ class DockerRegistryClient { private getManifest(String name, String tag) { request({ - registryService.getManifest(name, tag, tokenService.basicAuthHeader, userAgent) + Retrofit2SyncCall.executeCall(registryService.getManifest(name, tag, tokenService.basicAuthHeader, userAgent)) }, { token -> - registryService.getManifest(name, tag, token, userAgent) + Retrofit2SyncCall.executeCall(registryService.getManifest(name, tag, token, userAgent)) }, name) } private getSchemaV2Manifest(String name, String tag) { request({ - registryService.getSchemaV2Manifest(name, tag, tokenService.basicAuthHeader, userAgent) + Retrofit2SyncCall.executeCall(registryService.getSchemaV2Manifest(name, tag, tokenService.basicAuthHeader, userAgent)) }, { token -> - registryService.getSchemaV2Manifest(name, tag, token, userAgent) + Retrofit2SyncCall.executeCall(registryService.getSchemaV2Manifest(name, tag, token, userAgent)) }, name) } - private static String parseLink(retrofit.client.Header header) { - if (!header.name.equalsIgnoreCase("link")) { - return null - } - def links = header.value.split(";").collect { it.trim() } + private static String parseLink(String headerValue) { + + def links = headerValue.split(";").collect { it.trim() } if (!(links.findAll { String tok -> tok.replace(" ", "").equalsIgnoreCase("rel=\"next\"") @@ -370,23 +387,27 @@ class DockerRegistryClient { return link.startsWith('/') ? link.replaceFirst('/', '') : link } - private static String findNextLink(List headers) { + private static String findNextLink(okhttp3.Headers headers) { if (!headers) { return null } - def paths = headers.collect { header -> - parseLink(header) - }.findAll { it } + def caseInsensitiveHeaders = [:].withDefault { [] } + headers.names().each { name -> + caseInsensitiveHeaders[name.toLowerCase()] += headers.values(name) + } + + def headerValues = caseInsensitiveHeaders["link"] + headers.values("link") // We are at the end of the pagination. - if (!paths || paths.size() == 0) { + if (!headerValues || headerValues.size() == 0) { return null - } else if (paths.size() > 1) { - throw new DockerRegistryOperationException("Ambiguous number of Link headers provided, the following paths were identified: $paths") + } else if (headerValues.size() > 1) { + throw new DockerRegistryOperationException("Ambiguous number of Link headers provided, the following paths were identified: $headerValues") } - return paths[0] + return parseLink(headerValues[0] as String) } /* @@ -407,19 +428,19 @@ class DockerRegistryClient { def response try { response = request({ - path ? registryService.get(path, tokenService.basicAuthHeader, userAgent) : - registryService.getCatalog(paginateSize, tokenService.basicAuthHeader, userAgent) + path ? Retrofit2SyncCall.executeCall(registryService.get(path, tokenService.basicAuthHeader, userAgent)) : + Retrofit2SyncCall.executeCall(registryService.getCatalog(paginateSize, tokenService.basicAuthHeader, userAgent)) }, { token -> - path ? registryService.get(path, token, userAgent) : - registryService.getCatalog(paginateSize, token, userAgent) + path ? Retrofit2SyncCall.executeCall(registryService.get(path, token, userAgent)) : + Retrofit2SyncCall.executeCall(registryService.getCatalog(paginateSize, token, userAgent)) }, "_catalog") } catch (Exception e) { log.warn("Error encountered during catalog of $path", e) return new DockerRegistryCatalog(repositories: []) } - def nextPath = findNextLink(response?.headers) - def catalog = (DockerRegistryCatalog) converter.fromBody(response.body, DockerRegistryCatalog) + def nextPath = findNextLink(response?.headers()) + def catalog = convertResponseBody(response.body(), DockerRegistryCatalog) if(repositoriesRegex) { catalog.repositories = catalog.repositories.findAll { it ==~ repositoriesRegex } @@ -434,15 +455,15 @@ class DockerRegistryClient { public DockerRegistryTags getTags(String repository, String path = null) { def response = request({ - path ? registryService.get(path, tokenService.basicAuthHeader, userAgent) : - registryService.getTags(repository, tokenService.basicAuthHeader, userAgent) + path ? Retrofit2SyncCall.executeCall(registryService.get(path, tokenService.basicAuthHeader, userAgent)) : + Retrofit2SyncCall.executeCall(registryService.getTags(repository, tokenService.basicAuthHeader, userAgent)) }, { token -> - path ? registryService.get(path, token, userAgent) : - registryService.getTags(repository, token, userAgent) + path ? Retrofit2SyncCall.executeCall(registryService.get(path, token, userAgent)) : + Retrofit2SyncCall.executeCall(registryService.getTags(repository, token, userAgent)) }, repository) - def nextPath = findNextLink(response?.headers) - def tags = (DockerRegistryTags) converter.fromBody(response.body, DockerRegistryTags) + def nextPath = findNextLink(response?.headers()) + def tags = convertResponseBody(response.body(), DockerRegistryTags) if (nextPath) { def nextTags = getTags(repository, nextPath) @@ -465,8 +486,8 @@ class DockerRegistryClient { if (!tokenService.basicAuthHeader && error instanceof SpinnakerHttpException && ((SpinnakerHttpException)error).getResponseCode() == 401) { return } - Response response = doCheckV2Availability(tokenService.basicAuthHeader) - if (!response){ + def response = doCheckV2Availability(tokenService.basicAuthHeader) + if (!response.body()){ LOG.error "checkV2Availability", error throw error } @@ -475,11 +496,11 @@ class DockerRegistryClient { null } - private Response doCheckV2Availability(String basicAuthHeader = null) { + private Response doCheckV2Availability(String basicAuthHeader = null) { request({ - registryService.checkVersion(basicAuthHeader, userAgent) + Retrofit2SyncCall.executeCall(registryService.checkVersion(basicAuthHeader, userAgent)) }, { token -> - registryService.checkVersion(token, userAgent) + Retrofit2SyncCall.executeCall(registryService.checkVersion(token, userAgent)) }, "v2 version check") } @@ -487,7 +508,7 @@ class DockerRegistryClient { * Implements token request flow described here https://docs.docker.com/registry/spec/auth/token/ * The tokenService also caches tokens for us, so it will attempt to use an old token before retrying. */ - public Response request(Closure withoutToken, Closure withToken, String target) { + public Response request(Closure> withoutToken, Closure> withToken, String target) { try { DockerBearerToken dockerToken = tokenService.getToken(target) String token @@ -495,7 +516,7 @@ class DockerRegistryClient { token = "Bearer ${(dockerToken.bearer_token ?: dockerToken.token) ?: dockerToken.access_token}" } - Response response + Response response try { if (token) { response = withToken(token) diff --git a/clouddriver-docker/src/main/java/com/netflix/spinnaker/clouddriver/docker/registry/api/v2/client/DefaultDockerOkClientProvider.java b/clouddriver-docker/src/main/java/com/netflix/spinnaker/clouddriver/docker/registry/api/v2/client/DefaultDockerOkClientProvider.java index b32e7ad394..7d27c0fd5a 100644 --- a/clouddriver-docker/src/main/java/com/netflix/spinnaker/clouddriver/docker/registry/api/v2/client/DefaultDockerOkClientProvider.java +++ b/clouddriver-docker/src/main/java/com/netflix/spinnaker/clouddriver/docker/registry/api/v2/client/DefaultDockerOkClientProvider.java @@ -15,7 +15,6 @@ */ package com.netflix.spinnaker.clouddriver.docker.registry.api.v2.client; -import com.jakewharton.retrofit.Ok3Client; import com.netflix.spinnaker.clouddriver.docker.registry.security.TrustAllX509TrustManager; import java.security.KeyManagementException; import java.security.NoSuchAlgorithmException; @@ -29,7 +28,7 @@ public class DefaultDockerOkClientProvider implements DockerOkClientProvider { @Override - public Ok3Client provide(String address, long timeoutMs, boolean insecure) { + public OkHttpClient provide(String address, long timeoutMs, boolean insecure) { OkHttpClient.Builder clientBuilder = new OkHttpClient.Builder().readTimeout(timeoutMs, TimeUnit.MILLISECONDS); @@ -46,6 +45,6 @@ public Ok3Client provide(String address, long timeoutMs, boolean insecure) { sslContext.getSocketFactory(), (X509TrustManager) trustManagers[0]); } - return new Ok3Client(clientBuilder.build()); + return clientBuilder.build(); } } diff --git a/clouddriver-docker/src/main/java/com/netflix/spinnaker/clouddriver/docker/registry/api/v2/client/DockerOkClientProvider.java b/clouddriver-docker/src/main/java/com/netflix/spinnaker/clouddriver/docker/registry/api/v2/client/DockerOkClientProvider.java index d111453009..47b4b34d93 100644 --- a/clouddriver-docker/src/main/java/com/netflix/spinnaker/clouddriver/docker/registry/api/v2/client/DockerOkClientProvider.java +++ b/clouddriver-docker/src/main/java/com/netflix/spinnaker/clouddriver/docker/registry/api/v2/client/DockerOkClientProvider.java @@ -15,7 +15,7 @@ */ package com.netflix.spinnaker.clouddriver.docker.registry.api.v2.client; -import com.jakewharton.retrofit.Ok3Client; +import okhttp3.OkHttpClient; /** Allows custom configuration of the Docker Registry OkHttpClient. */ public interface DockerOkClientProvider { @@ -26,7 +26,7 @@ public interface DockerOkClientProvider { * @param timeoutMs The client timeout in milliseconds * @param insecure Whether or not the registry should be configured to trust all SSL certificates. * If this is true, you may want to fallback to {@code DefaultDockerOkClientProvider} - * @return An Ok3Client + * @return OkHttpClient */ - Ok3Client provide(String address, long timeoutMs, boolean insecure); + OkHttpClient provide(String address, long timeoutMs, boolean insecure); } diff --git a/clouddriver-docker/src/test/groovy/com/netflix/spinnaker/clouddriver/docker/registry/api/v2/client/DockerRegistryClientSpec.groovy b/clouddriver-docker/src/test/groovy/com/netflix/spinnaker/clouddriver/docker/registry/api/v2/client/DockerRegistryClientSpec.groovy index 31b5e31fee..1a5e2c47ad 100644 --- a/clouddriver-docker/src/test/groovy/com/netflix/spinnaker/clouddriver/docker/registry/api/v2/client/DockerRegistryClientSpec.groovy +++ b/clouddriver-docker/src/test/groovy/com/netflix/spinnaker/clouddriver/docker/registry/api/v2/client/DockerRegistryClientSpec.groovy @@ -19,12 +19,15 @@ package com.netflix.spinnaker.clouddriver.docker.registry.api.v2.client import com.netflix.spinnaker.clouddriver.docker.registry.api.v2.auth.DockerBearerToken import com.netflix.spinnaker.clouddriver.docker.registry.api.v2.auth.DockerBearerTokenService import com.netflix.spinnaker.kork.retrofit.exceptions.SpinnakerHttpException +import okhttp3.MediaType +import okhttp3.Protocol +import okhttp3.Request +import okhttp3.ResponseBody import org.springframework.http.HttpStatus -import retrofit.RetrofitError -import retrofit.client.Header -import retrofit.client.Response -import retrofit.mime.TypedByteArray -import retrofit.mime.TypedInput +import retrofit2.Response +import retrofit2.Retrofit +import retrofit2.converter.jackson.JacksonConverterFactory +import retrofit2.mock.Calls; import spock.lang.Shared import spock.lang.Specification @@ -44,19 +47,22 @@ class DockerRegistryClientSpec extends Specification { def stubbedRegistryService = Stub(DockerRegistryClient.DockerRegistryService){ String tagsJson = "{\"name\":\"library/ubuntu\",\"tags\":[\"latest\",\"xenial\",\"rolling\"]}" - TypedInput tagsTypedInput = new TypedByteArray("application/json", tagsJson.getBytes()) - Response tagsResponse = new Response("/v2/{repository}/tags/list",200, "nothing", Collections.EMPTY_LIST, tagsTypedInput) - getTags(_,_,_) >> tagsResponse +// TypedInput tagsTypedInput = new TypedByteArray("application/json", tagsJson.getBytes()) + Response tagsResponse = Response.success(200, ResponseBody.create(MediaType.parse("application/json"), tagsJson)) +// Response tagsResponse = new Response("/v2/{repository}/tags/list",200, "nothing", Collections.EMPTY_LIST, tagsTypedInput) + getTags(_,_,_) >> Calls.response(tagsResponse) String checkJson = "{}" - TypedInput checkTypedInput = new TypedByteArray("application/json", checkJson.getBytes()) - Response checkResponse = new Response("/v2/",200, "nothing", Collections.EMPTY_LIST, checkTypedInput) - checkVersion(_,_) >> checkResponse +// TypedInput checkTypedInput = new TypedByteArray("application/json", checkJson.getBytes()) + Response checkResponse = Response.success(200, ResponseBody.create(MediaType.parse("application/json"), checkJson)) +// Response checkResponse = new Response("/v2/",200, "nothing", Collections.EMPTY_LIST, checkTypedInput) + checkVersion(_,_) >> Calls.response(checkResponse) String json = "{\"repositories\":[\"armory-io/armorycommons\",\"armory/aquascan\",\"other/keel\"]}" - TypedInput catalogTypedInput = new TypedByteArray("application/json", json.getBytes()) - Response catalogResponse = new Response("/v2/_catalog/",200, "nothing", Collections.EMPTY_LIST, catalogTypedInput) - getCatalog(_,_,_) >> catalogResponse +// TypedInput catalogTypedInput = new TypedByteArray("application/json", json.getBytes()) + Response catalogResponse = Response.success(200, ResponseBody.create(MediaType.parse("application/json"), json)) +// Response catalogResponse = new Response("/v2/_catalog/",200, "nothing", Collections.EMPTY_LIST, catalogTypedInput) + getCatalog(_,_,_) >> Calls.response(catalogResponse) String schemaJson = '''{ "schemaVersion": 2, @@ -74,9 +80,10 @@ class DockerRegistryClientSpec extends Specification { } ] }''' - TypedInput schemaV2Input = new TypedByteArray("application/json", schemaJson.getBytes()) - Response schemaV2Response = new Response("/v2/{name}/manifests/{reference}",200, "nothing", Collections.EMPTY_LIST, schemaV2Input) - getSchemaV2Manifest(_,_,_,_) >> schemaV2Response +// TypedInput schemaV2Input = new TypedByteArray("application/json", schemaJson.getBytes()) + Response schemaV2Response = Response.success(200, ResponseBody.create(MediaType.parse("application/json"), schemaJson)) +// Response schemaV2Response = new Response("/v2/{name}/manifests/{reference}",200, "nothing", Collections.EMPTY_LIST, schemaV2Input) + getSchemaV2Manifest(_,_,_,_) >> Calls.response(schemaV2Response) String configDigestContentJson = '''{ "architecture": "amd64", @@ -106,9 +113,10 @@ class DockerRegistryClientSpec extends Specification { "os": "linux", "rootfs": {} }''' - TypedInput configDigestContentInput = new TypedByteArray("application/json", configDigestContentJson.getBytes()) - Response contentDigestResponse = new Response("/v2/{repository}/blobs/{digest}",200, "nothing", Collections.EMPTY_LIST, configDigestContentInput) - getDigestContent(_,_,_,_) >> contentDigestResponse +// TypedInput configDigestContentInput = new TypedByteArray("application/json", configDigestContentJson.getBytes()) + Response contentDigestResponse = Response.success(200, ResponseBody.create(MediaType.parse("application/json"), configDigestContentJson)) +// Response contentDigestResponse = new Response("/v2/{repository}/blobs/{digest}",200, "nothing", Collections.EMPTY_LIST, configDigestContentInput) + getDigestContent(_,_,_,_) >> Calls.response(contentDigestResponse) } def setupSpec() { @@ -155,7 +163,7 @@ class DockerRegistryClientSpec extends Specification { then: userAgent.startsWith("Spinnaker") - 1 * mockService.checkVersion(_,_) + 1 * mockService.checkVersion(_,_) >> Calls.response(null) } void "DockerRegistryClient should filter repositories by regular expression."() { @@ -191,17 +199,41 @@ class DockerRegistryClientSpec extends Specification { void "DockerRegistryClient should honor the www-authenticate header"() { setup: def authenticateDetails = "realm=\"https://auth.docker.io/token\",service=\"registry.docker.io\",scope=\"repository:${REPOSITORY1}:pull\"" - def unauthorizedRetroFitError = RetrofitError.httpError("url", - new Response("url", HttpStatus.UNAUTHORIZED.value(), "authentication required", [new Header("www-authenticate", "Bearer ${authenticateDetails}")], null), - null, null) DockerBearerToken token = new DockerBearerToken() token.bearer_token = "bearer-token" when: client = new DockerRegistryClient("https://index.docker.io", 100, "", "", stubbedRegistryService, dockerBearerTokenService) - client.request(() -> {throw new SpinnakerHttpException(unauthorizedRetroFitError)}, (_) -> null, REPOSITORY1) + client.request(() -> {throw makeSpinnakerHttpException(authenticateDetails)}, (_) -> null, REPOSITORY1) then: 1 * dockerBearerTokenService.getToken(REPOSITORY1, authenticateDetails) >> token } + public static SpinnakerHttpException makeSpinnakerHttpException(String authenticateDetails) { + String url = "https://some-url"; + + okhttp3.Headers headers = new okhttp3.Headers.Builder() + .add("www-authenticate", "Bearer ${authenticateDetails}") + .build(); + + + Response retrofit2Response = + Response.error( + ResponseBody.create(MediaType.parse("application/json"), "{ \"message\": \"arbitrary message\" }"), + new okhttp3.Response.Builder() + .code(HttpStatus.UNAUTHORIZED.value()) + .message("authentication required") + .protocol(Protocol.HTTP_1_1) + .request(new Request.Builder().url(url).build()) + .headers(headers) + .build()) + + Retrofit retrofit = + new Retrofit.Builder() + .baseUrl(url) + .addConverterFactory(JacksonConverterFactory.create()) + .build(); + + return new SpinnakerHttpException(retrofit2Response, retrofit); + } } diff --git a/clouddriver-docker/src/test/java/com/netflix/spinnaker/clouddriver/docker/registry/security/DockerRegistryNamedAccountCredentialsTest.java b/clouddriver-docker/src/test/java/com/netflix/spinnaker/clouddriver/docker/registry/security/DockerRegistryNamedAccountCredentialsTest.java index 3d4c39d233..ebcc8671cb 100644 --- a/clouddriver-docker/src/test/java/com/netflix/spinnaker/clouddriver/docker/registry/security/DockerRegistryNamedAccountCredentialsTest.java +++ b/clouddriver-docker/src/test/java/com/netflix/spinnaker/clouddriver/docker/registry/security/DockerRegistryNamedAccountCredentialsTest.java @@ -19,13 +19,12 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.argThat; import static org.mockito.Mockito.doAnswer; -import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; import com.fasterxml.jackson.databind.ObjectMapper; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; -import com.jakewharton.retrofit.Ok3Client; import com.netflix.spinnaker.clouddriver.docker.registry.api.v2.client.DockerOkClientProvider; import com.netflix.spinnaker.clouddriver.docker.registry.api.v2.client.DockerRegistryTags; import com.netflix.spinnaker.kork.client.ServiceClientProvider; @@ -33,19 +32,17 @@ import java.time.Duration; import java.time.Instant; import java.time.format.DateTimeFormatter; -import java.util.Collections; import java.util.HashMap; import java.util.Map; import java.util.regex.Matcher; import java.util.regex.Pattern; import lombok.Getter; import lombok.RequiredArgsConstructor; +import okhttp3.MediaType; +import okhttp3.OkHttpClient; +import okhttp3.Request; +import okhttp3.ResponseBody; import org.junit.jupiter.api.Test; -import org.mockito.invocation.InvocationOnMock; -import org.mockito.stubbing.Answer; -import retrofit.client.Request; -import retrofit.client.Response; -import retrofit.mime.TypedString; final class DockerRegistryNamedAccountCredentialsTest { private static final ObjectMapper objectMapper = new ObjectMapper(); @@ -56,12 +53,12 @@ final class DockerRegistryNamedAccountCredentialsTest { @Test void getTags() throws IOException { ImmutableList tags = ImmutableList.of("latest", "other", "something"); - Ok3Client ok3Client = mockDockerOkClient(tags, ImmutableMap.of()); + OkHttpClient okHttpClient = mockDockerOkClient(tags, ImmutableMap.of()); DockerRegistryNamedAccountCredentials credentials = new DockerRegistryNamedAccountCredentials.Builder() .accountName(ACCOUNT_NAME) .address("https://gcr.io") - .dockerOkClientProvider(new MockDockerOkClientProvider(ok3Client)) + .dockerOkClientProvider(new MockDockerOkClientProvider(okHttpClient)) .build(); assertThat(credentials.getTags(REPO_NAME)).containsExactlyInAnyOrderElementsOf(tags); } @@ -78,13 +75,13 @@ void getTagsInOrder() throws IOException { "oldest", LATEST_DATE.minus(Duration.ofDays(1))); - Ok3Client ok3Client = mockDockerOkClient(tags, creationDates); + OkHttpClient okHttpClient = mockDockerOkClient(tags, creationDates); DockerRegistryNamedAccountCredentials credentials = new DockerRegistryNamedAccountCredentials.Builder() .accountName(ACCOUNT_NAME) .address("https://gcr.io") .sortTagsByDate(true) - .dockerOkClientProvider(new MockDockerOkClientProvider(ok3Client)) + .dockerOkClientProvider(new MockDockerOkClientProvider(okHttpClient)) .serviceClientProvider(mock(ServiceClientProvider.class)) .build(); assertThat(credentials.getTags(REPO_NAME)) @@ -92,54 +89,67 @@ void getTagsInOrder() throws IOException { } /** - * Generates a mock Ok3Client that simulates responses from a docker registry with the supplied + * Generates a mock OkHttpClient that simulates responses from a docker registry with the supplied * tags and supplied creation dates for each tag. Tags that are not present in the map of creation * dates will return null as their creation date. */ - private static Ok3Client mockDockerOkClient( + private static OkHttpClient mockDockerOkClient( Iterable tags, Map creationDates) throws IOException { - Ok3Client ok3Client = mock(Ok3Client.class); - doReturn( - new Response( - "https://gcr.io/v2/myrepo/tags/list", - 200, - "", - Collections.emptyList(), - new TypedString(objectMapper.writeValueAsString(getTagsResponse(tags))))) - .when(ok3Client) - .execute(argThat(r -> r.getUrl().equals("https://gcr.io/v2/myrepo/tags/list"))); + OkHttpClient okHttpClient = mock(OkHttpClient.class); + okhttp3.Call mockTagListCall = mock(okhttp3.Call.class); + okhttp3.Response tagListResponse = + new okhttp3.Response.Builder() + .request(new Request.Builder().url("https://gcr.io/v2/myrepo/tags/list").build()) + .protocol(okhttp3.Protocol.HTTP_1_1) + .code(200) + .message("OK") + .body( + ResponseBody.create( + MediaType.parse("application/json"), + objectMapper.writeValueAsString(getTagsResponse(tags)))) + .build(); + when(mockTagListCall.execute()).thenReturn(tagListResponse); + when(okHttpClient.newCall( + argThat(r -> r.url().toString().equals("https://gcr.io/v2/myrepo/tags/list")))) + .thenReturn(mockTagListCall); doAnswer( - new Answer() { - @Override - public Object answer(InvocationOnMock invocation) throws Throwable { - Object[] args = invocation.getArguments(); - Request request = (Request) args[0]; - String tag = getTag(request.getUrl()); - Instant optionalDate = creationDates.get(tag); - return new Response( - "https://gcr.io/v2/myrepo/manifests/latest", - 200, - "", - Collections.emptyList(), - new TypedString( - objectMapper.writeValueAsString( - DockerManifestResponse.withCreationDate(optionalDate)))); - } - - private String getTag(String url) { - Matcher matcher = - Pattern.compile("https://gcr.io/v2/myrepo/manifests/(.*)").matcher(url); - if (matcher.matches()) { - return matcher.group(1); - } - throw new IllegalArgumentException(); - } + invocation -> { + Request request = invocation.getArgument(0); + String tag = extractTag(request.url().toString()); + Instant optionalDate = creationDates.get(tag); + + okhttp3.Response manifestResponse = + new okhttp3.Response.Builder() + .request(request) + .protocol(okhttp3.Protocol.HTTP_1_1) + .code(200) + .message("OK") + .body( + ResponseBody.create( + MediaType.parse("application/json"), + objectMapper.writeValueAsString( + DockerManifestResponse.withCreationDate(optionalDate)))) + .build(); + + okhttp3.Call mockManifestCall = mock(okhttp3.Call.class); + when(mockManifestCall.execute()).thenReturn(manifestResponse); + + return mockManifestCall; }) - .when(ok3Client) - .execute(argThat(r -> r.getUrl().matches("https://gcr.io/v2/myrepo/manifests/.*"))); + .when(okHttpClient) + .newCall( + argThat(r -> r.url().toString().matches("https://gcr\\.io/v2/myrepo/manifests/.*"))); + + return okHttpClient; + } - return ok3Client; + private static String extractTag(String url) { + Matcher matcher = Pattern.compile("https://gcr.io/v2/myrepo/manifests/(.*)").matcher(url); + if (matcher.matches()) { + return matcher.group(1); + } + throw new IllegalArgumentException(); } private static DockerRegistryTags getTagsResponse(Iterable tags) { @@ -179,10 +189,10 @@ static HistoryEntry withCreationDate(Instant instant) throws IOException { @RequiredArgsConstructor private static class MockDockerOkClientProvider implements DockerOkClientProvider { - private final Ok3Client mockClient; + private final OkHttpClient mockClient; @Override - public Ok3Client provide(String address, long timeoutMs, boolean insecure) { + public OkHttpClient provide(String address, long timeoutMs, boolean insecure) { return mockClient; } }