Skip to content

Commit dd4791f

Browse files
fix(docker): resolve the issue of double encoding while making the next call to ECR's tags list (#6357) (#6358)
* test(docker): add test to demonstrate the issue of double encoding of query parameter when the next link from a docker registry like ECR responds with an encoded url. * fix(docker): resolve the issue of double encoding contained in the ECR's next `link` header causing the Retrofit2 client to fail due to error code `405` and `Message: Method Not Allowed`. (cherry picked from commit 06c98c0) Co-authored-by: Kiran Godishala <[email protected]>
1 parent fd29d63 commit dd4791f

File tree

3 files changed

+45
-30
lines changed

3 files changed

+45
-30
lines changed

clouddriver-docker/src/main/groovy/com/netflix/spinnaker/clouddriver/docker/registry/api/v2/client/DockerRegistryClient.groovy

+4-4
Original file line numberDiff line numberDiff line change
@@ -243,7 +243,7 @@ class DockerRegistryClient {
243243
@Headers([
244244
"Docker-Distribution-API-Version: registry/2.0"
245245
])
246-
Call<ResponseBody> getTags(@Path(value="repository", encoded=true) String repository, @Header("Authorization") String token, @Header("User-Agent") String agent, @QueryMap Map<String, String> queryParams)
246+
Call<ResponseBody> getTags(@Path(value="repository", encoded=true) String repository, @Header("Authorization") String token, @Header("User-Agent") String agent, @QueryMap(encoded=true) Map<String, String> queryParams)
247247

248248

249249
@GET("/v2/{name}/manifests/{reference}")
@@ -263,14 +263,14 @@ class DockerRegistryClient {
263263
@Headers([
264264
"Docker-Distribution-API-Version: registry/2.0"
265265
])
266-
Call<ResponseBody> getCatalog(@Header("Authorization") String token, @Header("User-Agent") String agent, @QueryMap Map<String, String> queryParams)
266+
Call<ResponseBody> getCatalog(@Header("Authorization") String token, @Header("User-Agent") String agent, @QueryMap(encoded=true) Map<String, String> queryParams)
267267

268268

269269
@GET("/{path}")
270270
@Headers([
271271
"Docker-Distribution-API-Version: registry/2.0"
272272
])
273-
Call<ResponseBody> get(@Path(value="path", encoded=true) String path, @Header("Authorization") String token, @Header("User-Agent") String agent, @QueryMap Map<String, String> queryParams)
273+
Call<ResponseBody> get(@Path(value="path", encoded=true) String path, @Header("Authorization") String token, @Header("User-Agent") String agent, @QueryMap(encoded=true) Map<String, String> queryParams)
274274

275275

276276
@GET("/v2/")
@@ -394,7 +394,6 @@ class DockerRegistryClient {
394394
}
395395

396396
def headerValues = caseInsensitiveHeaders["link"]
397-
headers.values("link")
398397

399398
// We are at the end of the pagination.
400399
if (!headerValues || headerValues.size() == 0) {
@@ -453,6 +452,7 @@ class DockerRegistryClient {
453452
}
454453

455454
public DockerRegistryTags getTags(String repository, String path = null, Map<String, String> queryParams = [:]) {
455+
queryParams.computeIfAbsent("n", { paginateSize.toString() })
456456
def response = request({
457457
path ? Retrofit2SyncCall.executeCall(registryService.get(path, tokenService.basicAuthHeader, userAgent, queryParams)) :
458458
Retrofit2SyncCall.executeCall(registryService.getTags(repository, tokenService.basicAuthHeader, userAgent, queryParams))

clouddriver-docker/src/test/java/com/netflix/spinnaker/clouddriver/docker/registry/api/v2/client/DockerRegistryClientTest.java

+40-25
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,10 @@
1717
package com.netflix.spinnaker.clouddriver.docker.registry.api.v2.client;
1818

1919
import static com.github.tomakehurst.wiremock.client.WireMock.aResponse;
20+
import static com.github.tomakehurst.wiremock.client.WireMock.getRequestedFor;
2021
import static com.github.tomakehurst.wiremock.client.WireMock.urlMatching;
2122
import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.wireMockConfig;
23+
import static org.assertj.core.api.AssertionsForInterfaceTypes.assertThat;
2224
import static org.junit.jupiter.api.Assertions.assertEquals;
2325
import static org.junit.jupiter.api.Assertions.assertIterableEquals;
2426
import static org.mockito.ArgumentMatchers.anyString;
@@ -29,41 +31,21 @@
2931
import com.github.tomakehurst.wiremock.junit5.WireMockExtension;
3032
import com.netflix.spinnaker.clouddriver.docker.registry.api.v2.auth.DockerBearerToken;
3133
import com.netflix.spinnaker.clouddriver.docker.registry.api.v2.auth.DockerBearerTokenService;
32-
import com.netflix.spinnaker.config.DefaultServiceClientProvider;
33-
import com.netflix.spinnaker.config.okhttp3.DefaultOkHttpClientBuilderProvider;
34-
import com.netflix.spinnaker.config.okhttp3.OkHttpClientProvider;
35-
import com.netflix.spinnaker.kork.client.ServiceClientProvider;
3634
import com.netflix.spinnaker.kork.retrofit.ErrorHandlingExecutorCallAdapterFactory;
37-
import com.netflix.spinnaker.kork.retrofit.Retrofit2ServiceFactory;
38-
import com.netflix.spinnaker.kork.retrofit.Retrofit2ServiceFactoryAutoConfiguration;
39-
import com.netflix.spinnaker.okhttp.OkHttpClientConfigurationProperties;
4035
import java.util.Arrays;
4136
import java.util.Map;
4237
import okhttp3.OkHttpClient;
4338
import org.junit.jupiter.api.BeforeEach;
4439
import org.junit.jupiter.api.Test;
4540
import org.junit.jupiter.api.extension.RegisterExtension;
4641
import org.mockito.Mockito;
47-
import org.springframework.beans.factory.annotation.Autowired;
4842
import org.springframework.boot.test.context.SpringBootTest;
4943
import org.springframework.boot.test.mock.mockito.MockBean;
5044
import org.springframework.http.HttpStatus;
5145
import retrofit2.Retrofit;
5246
import retrofit2.converter.jackson.JacksonConverterFactory;
5347

54-
@SpringBootTest(
55-
classes = {
56-
OkHttpClientConfigurationProperties.class,
57-
Retrofit2ServiceFactory.class,
58-
ServiceClientProvider.class,
59-
OkHttpClientProvider.class,
60-
OkHttpClient.class,
61-
DefaultServiceClientProvider.class,
62-
DefaultOkHttpClientBuilderProvider.class,
63-
Retrofit2ServiceFactoryAutoConfiguration.class,
64-
ObjectMapper.class
65-
},
66-
webEnvironment = SpringBootTest.WebEnvironment.NONE)
48+
@SpringBootTest(classes = {DockerBearerTokenService.class})
6749
public class DockerRegistryClientTest {
6850

6951
@RegisterExtension
@@ -73,7 +55,6 @@ public class DockerRegistryClientTest {
7355
static DockerRegistryClient.DockerRegistryService dockerRegistryService;
7456
@MockBean DockerBearerTokenService dockerBearerTokenService;
7557
static DockerRegistryClient dockerRegistryClient;
76-
@Autowired ServiceClientProvider serviceClientProvider;
7758
ObjectMapper objectMapper = new ObjectMapper();
7859
Map<String, Object> tagsResponse;
7960
String tagsResponseString;
@@ -134,7 +115,7 @@ private static <T> T buildService(Class<T> type, String baseUrl) {
134115
@Test
135116
public void getTagsWithoutNextLink() {
136117
wmDockerRegistry.stubFor(
137-
WireMock.get(urlMatching("/v2/library/nginx/tags/list"))
118+
WireMock.get(urlMatching("/v2/library/nginx/tags/list\\?n=5"))
138119
.willReturn(
139120
aResponse().withStatus(HttpStatus.OK.value()).withBody(tagsResponseString)));
140121

@@ -146,7 +127,7 @@ public void getTagsWithoutNextLink() {
146127
@Test
147128
public void getTagsWithNextLink() {
148129
wmDockerRegistry.stubFor(
149-
WireMock.get(urlMatching("/v2/library/nginx/tags/list"))
130+
WireMock.get(urlMatching("/v2/library/nginx/tags/list\\?n=5"))
150131
.willReturn(
151132
aResponse()
152133
.withStatus(HttpStatus.OK.value())
@@ -165,7 +146,7 @@ public void getTagsWithNextLink() {
165146
"</v2/library/nginx/tags/list1>; rel=\"next\"")
166147
.withBody(tagsSecondResponseString)));
167148
wmDockerRegistry.stubFor(
168-
WireMock.get(urlMatching("/v2/library/nginx/tags/list1"))
149+
WireMock.get(urlMatching("/v2/library/nginx/tags/list1\\?n=5"))
169150
.willReturn(
170151
aResponse().withStatus(HttpStatus.OK.value()).withBody(tagsThirdResponseString)));
171152

@@ -202,4 +183,38 @@ public void getCatalogWithNextLink() {
202183
DockerRegistryCatalog dockerRegistryCatalog = dockerRegistryClient.getCatalog();
203184
assertEquals(15, dockerRegistryCatalog.getRepositories().size());
204185
}
186+
187+
@Test
188+
public void getTagsWithNextLinkEncryptedAndEncoded() {
189+
String tagsListEndPointMinusQueryParams = "/v2/library/nginx/tags/list";
190+
String expectedEncodedParam = "Md1Woj%2FNOhjepFq7kPAr%2FEw%2FYxfcJoH9N52%2Blo3qAQ%3D%3D";
191+
192+
wmDockerRegistry.stubFor(
193+
WireMock.get(urlMatching(tagsListEndPointMinusQueryParams + "\\?n=5"))
194+
.willReturn(
195+
aResponse()
196+
.withStatus(HttpStatus.OK.value())
197+
.withHeader(
198+
"link",
199+
"</v2/library/nginx/tags/list?last=Md1Woj%2FNOhjepFq7kPAr%2FEw%2FYxfcJoH9N52%2Blo3qAQ%3D%3D&n=5>; rel=\"next\"")
200+
.withBody(tagsResponseString)));
201+
202+
wmDockerRegistry.stubFor(
203+
WireMock.get(
204+
urlMatching(
205+
tagsListEndPointMinusQueryParams + "\\?last=" + expectedEncodedParam + "&n=5"))
206+
.willReturn(
207+
aResponse().withStatus(HttpStatus.OK.value()).withBody(tagsSecondResponseString)));
208+
209+
DockerRegistryTags dockerRegistryTags = dockerRegistryClient.getTags("library/nginx");
210+
assertThat(dockerRegistryTags.getTags()).hasSize(10);
211+
212+
wmDockerRegistry.verify(
213+
1, getRequestedFor(urlMatching(tagsListEndPointMinusQueryParams + "\\?n=5")));
214+
wmDockerRegistry.verify(
215+
1,
216+
getRequestedFor(
217+
urlMatching(
218+
tagsListEndPointMinusQueryParams + "\\?last=" + expectedEncodedParam + "&n=5")));
219+
}
205220
}

clouddriver-docker/src/test/java/com/netflix/spinnaker/clouddriver/docker/registry/security/DockerRegistryNamedAccountCredentialsTest.java

+1-1
Original file line numberDiff line numberDiff line change
@@ -110,7 +110,7 @@ private static OkHttpClient mockDockerOkClient(
110110
.build();
111111
when(mockTagListCall.execute()).thenReturn(tagListResponse);
112112
when(okHttpClient.newCall(
113-
argThat(r -> r.url().toString().equals("https://gcr.io/v2/myrepo/tags/list"))))
113+
argThat(r -> r.url().toString().equals("https://gcr.io/v2/myrepo/tags/list?n=100"))))
114114
.thenReturn(mockTagListCall);
115115

116116
doAnswer(

0 commit comments

Comments
 (0)