Skip to content

Commit 627f107

Browse files
authored
Fix memory leak and enable OkHttp support for ACR blob updates. (Azure#28024)
* ACR blob updates. * Disable playback tests on non-windows devices. * Update the changelog * Disable client tests for playback mode.
1 parent 74ceb51 commit 627f107

File tree

64 files changed

+6435
-2097
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

64 files changed

+6435
-2097
lines changed

sdk/containerregistry/azure-containers-containerregistry/CHANGELOG.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
# Release History
22

3-
## 1.1.0-beta.1 (Unreleased)
3+
## 1.1.0-beta.1 (2022-04-08)
44

55
### Features Added
66
- Added interfaces from `com.azure.core.client.traits` to `ContainerRegistryClientBuilder`.
7+
- Added support for `ContainerRegistryBlobAsyncClient`.
78

89
### Breaking Changes
910

sdk/containerregistry/azure-containers-containerregistry/src/main/java/com/azure/containers/containerregistry/implementation/ContainerRegistriesImpl.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -298,7 +298,8 @@ public Mono<Response<Void>> checkDockerV2SupportWithResponseAsync(Context contex
298298
@ServiceMethod(returns = ReturnType.SINGLE)
299299
public Mono<Response<ManifestWrapper>> getManifestWithResponseAsync(
300300
String name, String reference, String accept, Context context) {
301-
final String acceptParam = "application/json";
301+
// TODO: Bug in autorest that we do not allow picking up acceptParam from the parameters.
302+
final String acceptParam = accept;
302303
return service.getManifest(this.client.getUrl(), name, reference, accept, acceptParam, context);
303304
}
304305

sdk/containerregistry/azure-containers-containerregistry/src/main/java/com/azure/containers/containerregistry/implementation/ContainerRegistryBlobsImpl.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,7 @@ Mono<ContainerRegistryBlobsCheckBlobExistsResponse> checkBlobExists(
8686
Context context);
8787

8888
@Delete("/v2/{name}/blobs/{digest}")
89-
@ExpectedResponses({202})
89+
@ExpectedResponses({202, 404})
9090
@UnexpectedResponseExceptionType(HttpResponseException.class)
9191
Mono<StreamResponse> deleteBlob(
9292
@HostParam("url") String url,
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
4+
package com.azure.containers.containerregistry.implementation;
5+
6+
import com.azure.core.http.HttpHeaders;
7+
import com.azure.core.http.HttpMethod;
8+
import com.azure.core.http.HttpPipelineCallContext;
9+
import com.azure.core.http.HttpPipelineNextPolicy;
10+
import com.azure.core.http.HttpRequest;
11+
import com.azure.core.http.HttpResponse;
12+
import com.azure.core.http.policy.HttpPipelinePolicy;
13+
import com.azure.core.util.CoreUtils;
14+
import com.azure.core.util.logging.ClientLogger;
15+
import reactor.core.publisher.Mono;
16+
17+
import java.util.Arrays;
18+
import java.util.HashSet;
19+
import java.util.Set;
20+
21+
import static com.azure.containers.containerregistry.implementation.UtilsImpl.DOCKER_DIGEST_HEADER_NAME;
22+
23+
/**
24+
* <p> Redirect policy for the container registry.</p>
25+
*
26+
* <p> This reads some of the headers that are returned from the redirect call that core redirect policy does not handle.</p>
27+
*/
28+
public final class ContainerRegistryRedirectPolicy implements HttpPipelinePolicy {
29+
private static final ClientLogger LOGGER = new ClientLogger(com.azure.core.http.policy.DefaultRedirectStrategy.class);
30+
private static final int MAX_REDIRECT_ATTEMPTS;
31+
private static final String REDIRECT_LOCATION_HEADER_NAME;
32+
private static final int PERMANENT_REDIRECT_STATUS_CODE;
33+
private static final int TEMPORARY_REDIRECT_STATUS_CODE;
34+
private static final Set<HttpMethod> REDIRECT_ALLOWED_METHODS;
35+
private static final String AUTHORIZATION;
36+
37+
static {
38+
REDIRECT_ALLOWED_METHODS = new HashSet<>(Arrays.asList(HttpMethod.GET, HttpMethod.HEAD));
39+
PERMANENT_REDIRECT_STATUS_CODE = 308;
40+
TEMPORARY_REDIRECT_STATUS_CODE = 307;
41+
REDIRECT_LOCATION_HEADER_NAME = "Location";
42+
MAX_REDIRECT_ATTEMPTS = 3;
43+
AUTHORIZATION = "Authorization";
44+
}
45+
46+
@Override
47+
public Mono<HttpResponse> process(HttpPipelineCallContext context, HttpPipelineNextPolicy next) {
48+
return this.attemptRedirect(context, next, context.getHttpRequest(), 1, new HashSet<>());
49+
}
50+
51+
/**
52+
* Function to process through the HTTP Response received in the pipeline
53+
* and redirect sending the request with new redirect url.
54+
*/
55+
private Mono<HttpResponse> attemptRedirect(HttpPipelineCallContext context, HttpPipelineNextPolicy next, HttpRequest originalHttpRequest, int redirectAttempt, Set<String> attemptedRedirectUrls) {
56+
context.setHttpRequest(originalHttpRequest.copy());
57+
return next.clone().process().flatMap((httpResponse) -> {
58+
if (this.shouldAttemptRedirect(context, httpResponse, redirectAttempt, attemptedRedirectUrls)) {
59+
HttpRequest redirectRequestCopy = this.createRedirectRequest(httpResponse);
60+
return httpResponse.getBody().ignoreElements()
61+
.then(this.attemptRedirect(context, next, redirectRequestCopy, redirectAttempt + 1, attemptedRedirectUrls))
62+
.flatMap(newResponse -> {
63+
String digest = httpResponse.getHeaders().getValue(DOCKER_DIGEST_HEADER_NAME);
64+
if (digest != null) {
65+
newResponse.getHeaders().add(DOCKER_DIGEST_HEADER_NAME, digest);
66+
}
67+
return Mono.just(newResponse);
68+
});
69+
} else {
70+
return Mono.just(httpResponse);
71+
}
72+
});
73+
}
74+
75+
public boolean shouldAttemptRedirect(HttpPipelineCallContext context, HttpResponse httpResponse, int tryCount, Set<String> attemptedRedirectUrls) {
76+
if (this.isValidRedirectStatusCode(httpResponse.getStatusCode()) && this.isValidRedirectCount(tryCount) && this.isAllowedRedirectMethod(httpResponse.getRequest().getHttpMethod())) {
77+
String redirectUrl = this.tryGetRedirectHeader(httpResponse.getHeaders(), REDIRECT_LOCATION_HEADER_NAME);
78+
if (redirectUrl != null && !this.alreadyAttemptedRedirectUrl(redirectUrl, attemptedRedirectUrls)) {
79+
LOGGER.verbose("[Redirecting] Try count: {}, Attempted Redirect URLs: {}", tryCount, String.join(",", attemptedRedirectUrls));
80+
attemptedRedirectUrls.add(redirectUrl);
81+
return true;
82+
} else {
83+
return false;
84+
}
85+
} else {
86+
return false;
87+
}
88+
}
89+
90+
private HttpRequest createRedirectRequest(HttpResponse httpResponse) {
91+
String responseLocation = this.tryGetRedirectHeader(httpResponse.getHeaders(), REDIRECT_LOCATION_HEADER_NAME);
92+
HttpRequest request = httpResponse.getRequest();
93+
request.setUrl(responseLocation);
94+
request.getHeaders().remove(AUTHORIZATION);
95+
return httpResponse.getRequest().setUrl(responseLocation);
96+
}
97+
98+
private boolean alreadyAttemptedRedirectUrl(String redirectUrl, Set<String> attemptedRedirectUrls) {
99+
if (attemptedRedirectUrls.contains(redirectUrl)) {
100+
LOGGER.error("Request was redirected more than once to: {}", new Object[]{redirectUrl});
101+
return true;
102+
} else {
103+
return false;
104+
}
105+
}
106+
107+
private boolean isValidRedirectCount(int tryCount) {
108+
if (tryCount >= MAX_REDIRECT_ATTEMPTS) {
109+
LOGGER.error("Request has been redirected more than {} times.", new Object[]{MAX_REDIRECT_ATTEMPTS});
110+
return false;
111+
} else {
112+
return true;
113+
}
114+
}
115+
116+
private boolean isAllowedRedirectMethod(HttpMethod httpMethod) {
117+
if (REDIRECT_ALLOWED_METHODS.contains(httpMethod)) {
118+
return true;
119+
} else {
120+
LOGGER.error("Request was redirected from an invalid redirect allowed method: {}", new Object[]{httpMethod});
121+
return false;
122+
}
123+
}
124+
125+
private boolean isValidRedirectStatusCode(int statusCode) {
126+
return statusCode == PERMANENT_REDIRECT_STATUS_CODE || statusCode == TEMPORARY_REDIRECT_STATUS_CODE;
127+
}
128+
129+
String tryGetRedirectHeader(HttpHeaders headers, String headerName) {
130+
String headerValue = headers.getValue(headerName);
131+
if (CoreUtils.isNullOrEmpty(headerValue)) {
132+
LOGGER.error("Redirect url was null for header name: {}, request redirect was terminated.", headerName);
133+
return null;
134+
} else {
135+
return headerValue;
136+
}
137+
}
138+
}
139+

sdk/containerregistry/azure-containers-containerregistry/src/main/java/com/azure/containers/containerregistry/implementation/UtilsImpl.java

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -62,11 +62,11 @@ public final class UtilsImpl {
6262
private static final int HTTP_STATUS_CODE_ACCEPTED;
6363
private static final String CONTINUATION_LINK_HEADER_NAME;
6464
private static final Pattern CONTINUATION_LINK_PATTERN;
65+
private static final ClientLogger LOGGER;
6566

66-
public static final String OCI_MANIFEST_MEDIA_TYPE;
6767
public static final String DOCKER_DIGEST_HEADER_NAME;
68+
public static final String OCI_MANIFEST_MEDIA_TYPE;
6869
public static final String CONTAINER_REGISTRY_TRACING_NAMESPACE_VALUE;
69-
private static final ClientLogger LOGGER;
7070

7171
static {
7272
LOGGER = new ClientLogger(UtilsImpl.class);
@@ -76,7 +76,7 @@ public final class UtilsImpl {
7676
HTTP_STATUS_CODE_NOT_FOUND = 404;
7777
HTTP_STATUS_CODE_ACCEPTED = 202;
7878
OCI_MANIFEST_MEDIA_TYPE = "application/vnd.oci.image.manifest.v1+json";
79-
DOCKER_DIGEST_HEADER_NAME = "Docker-Content-Digest";
79+
DOCKER_DIGEST_HEADER_NAME = "docker-content-digest";
8080
CONTINUATION_LINK_HEADER_NAME = "Link";
8181
CONTINUATION_LINK_PATTERN = Pattern.compile("<(.+)>;.*");
8282
CONTAINER_REGISTRY_TRACING_NAMESPACE_VALUE = "Microsoft.ContainerRegistry";
@@ -126,6 +126,7 @@ public static HttpPipeline buildHttpPipeline(
126126
policies.add(ClientBuilderUtil.validateAndGetRetryPolicy(retryPolicy, retryOptions));
127127
policies.add(new CookiePolicy());
128128
policies.add(new AddDatePolicy());
129+
policies.add(new ContainerRegistryRedirectPolicy());
129130

130131
policies.addAll(perRetryPolicies);
131132
HttpPolicyProviders.addAfterRetryPolicies(policies);
@@ -222,7 +223,7 @@ public static <T> Mono<Response<Void>> deleteResponseToSuccess(Response<T> respo
222223
return getAcceptedDeleteResponse(responseT, responseT.getStatusCode());
223224
}
224225

225-
// In case of 400, we still convert it to success i.e. no-op.
226+
// In case of 404, we still convert it to success i.e. no-op.
226227
return getAcceptedDeleteResponse(responseT, HTTP_STATUS_CODE_ACCEPTED);
227228
}
228229

@@ -335,4 +336,13 @@ public static <T, R> PagedResponse<T> getPagedResponseWithContinuationToken(Page
335336
null
336337
);
337338
}
339+
340+
/**
341+
* Get the digest from the response header if available.
342+
* @param response The HttpResponse to parse.
343+
* @return The digest value.
344+
*/
345+
public static <T> String getDigestFromHeader(Response<T> response) {
346+
return response.getHeaders().getValue(DOCKER_DIGEST_HEADER_NAME);
347+
}
338348
}

sdk/containerregistry/azure-containers-containerregistry/src/main/java/com/azure/containers/containerregistry/implementation/authentication/ContainerRegistryCredentialsPolicy.java

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
import com.azure.core.credential.TokenRequestContext;
77
import com.azure.core.http.HttpPipelineCallContext;
8+
import com.azure.core.http.HttpPipelineNextPolicy;
89
import com.azure.core.http.HttpResponse;
910
import com.azure.core.http.policy.BearerTokenAuthenticationPolicy;
1011
import com.azure.core.util.logging.ClientLogger;
@@ -84,6 +85,45 @@ public Mono<Void> setAuthorizationHeader(HttpPipelineCallContext context, TokenR
8485
});
8586
}
8687

88+
@Override
89+
public Mono<HttpResponse> process(HttpPipelineCallContext context, HttpPipelineNextPolicy next) {
90+
if ("http".equals(context.getHttpRequest().getUrl().getProtocol())) {
91+
return Mono.error(new RuntimeException("token credentials require a URL using the HTTPS protocol scheme"));
92+
}
93+
94+
// Since we will need to replay this call, adding duplicate to make this replayable.
95+
if (context.getHttpRequest().getBody() != null) {
96+
context.getHttpRequest().setBody(context.getHttpRequest().getBody().map(buffer -> buffer.duplicate()));
97+
}
98+
99+
HttpPipelineNextPolicy nextPolicy = next.clone();
100+
return authorizeRequest(context)
101+
.then(Mono.defer(() -> next.process()))
102+
.flatMap(httpResponse -> {
103+
String authHeader = httpResponse.getHeaderValue(WWW_AUTHENTICATE);
104+
if (httpResponse.getStatusCode() == 401 && authHeader != null) {
105+
return authorizeRequestOnChallenge(context, httpResponse).flatMap(retry -> {
106+
if (retry) {
107+
return nextPolicy.process()
108+
.doFinally(ignored -> {
109+
// Both Netty and OkHttp expect the requestBody to be closed after the connection is closed.
110+
// Failure to do so results in memory leak.
111+
// In case of StreamResponse (or other scenarios where we do not eagerly read the response)
112+
// we let the client close the connection after the stream read.
113+
// This can cause potential leaks in the scenarios like above, where the policy
114+
// may intercept the response and prevent it from reaching the client.
115+
// Hence, the policy needs to ensure that the connection is closed.
116+
httpResponse.close();
117+
});
118+
} else {
119+
return Mono.just(httpResponse);
120+
}
121+
});
122+
}
123+
return Mono.just(httpResponse);
124+
});
125+
}
126+
87127
/**
88128
* Handles the authentication challenge in the event a 401 response with a WWW-Authenticate authentication
89129
* challenge header is received after the initial request and returns appropriate {@link TokenRequestContext} to

0 commit comments

Comments
 (0)