diff --git a/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/Constants.java b/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/Constants.java index 271e9302a3d74d..26877c9558b921 100644 --- a/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/Constants.java +++ b/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/Constants.java @@ -1353,16 +1353,21 @@ private Constants() { */ public static final String CUSTOM_HEADERS_POSTFIX = ".custom.headers"; + /** Custom per-request headers postfix. + * value: {@value} + */ + public static final String CUSTOM_PER_REQUEST_HEADERS_POSTFIX = ".request"; + /** * List of custom headers to be set on the service client. * Multiple parameters can be used to specify custom headers. *
    * Usage:
-   * fs.s3a.client.s3.custom.headers - Headers to add on all the S3 requests.
-   * fs.s3a.client.sts.custom.headers - Headers to add on all the STS requests.
+   * fs.s3a.client.s3.custom.headers - Headers to add to all S3 requests.
+   * fs.s3a.client.sts.custom.headers - Headers to add to all STS requests.
    *
    * Examples:
-   * CustomHeader {@literal ->} 'Header1:Value1'
+   * CustomHeader {@literal ->} 'Header1=Value1'
    * CustomHeaders {@literal ->} 'Header1=Value1;Value2,Header2=Value1'
    * 
*/ @@ -1374,6 +1379,31 @@ private Constants() { FS_S3A_CLIENT_PREFIX + AWS_SERVICE_IDENTIFIER_S3.toLowerCase(Locale.ROOT) + CUSTOM_HEADERS_POSTFIX; + /** + * List of custom per-request-type headers to be set on the service client. + * Multiple parameters can be used to specify custom headers. + *
+   * Usage:
+   * fs.s3a.client.s3.custom.headers.request.REQUEST - Headers to add to all S3 REQUEST requests.
+   * fs.s3a.client.sts.custom.headers.request.REQUEST - Headers to add to all STS REQUEST requests.
+   *
+   * Note: REQUEST refers to the AWS S3 request name.
+   * See subclasses of S3Request
+   * and subclasses of StsRequest
+   * for all existing requests.
+   *
+   * Examples:
+   * fs.s3a.client.s3.custom.headers.request.DeleteObjectRequest
+   * CustomHeader {@literal ->} 'Header1=Value1'
+   * CustomHeaders {@literal ->} 'Header1=Value1;Value2,Header2=Value1'
+   * 
+ */ + public static final String CUSTOM_REQUEST_HEADERS_STS_PREFIX = + CUSTOM_HEADERS_STS + CUSTOM_PER_REQUEST_HEADERS_POSTFIX + "."; + + public static final String CUSTOM_REQUEST_HEADERS_S3_PREFIX = + CUSTOM_HEADERS_S3 + CUSTOM_PER_REQUEST_HEADERS_POSTFIX + "."; + /** * How long to wait for the thread pool to terminate when cleaning up. * Value: {@value} seconds. diff --git a/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/impl/AWSClientConfig.java b/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/impl/AWSClientConfig.java index 2627b90037c45f..59613a14f7159c 100644 --- a/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/impl/AWSClientConfig.java +++ b/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/impl/AWSClientConfig.java @@ -23,9 +23,12 @@ import java.net.URISyntaxException; import java.time.Duration; import java.util.Arrays; +import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.concurrent.TimeUnit; +import java.util.function.BiConsumer; +import java.util.function.Consumer; import java.util.stream.Collectors; import org.slf4j.Logger; @@ -82,6 +85,8 @@ import static org.apache.hadoop.fs.s3a.Constants.USER_AGENT_PREFIX; import static org.apache.hadoop.fs.s3a.Constants.CUSTOM_HEADERS_S3; import static org.apache.hadoop.fs.s3a.Constants.CUSTOM_HEADERS_STS; +import static org.apache.hadoop.fs.s3a.Constants.CUSTOM_REQUEST_HEADERS_S3_PREFIX; +import static org.apache.hadoop.fs.s3a.Constants.CUSTOM_REQUEST_HEADERS_STS_PREFIX; import static org.apache.hadoop.fs.s3a.impl.ConfigurationHelper.enforceMinimumDuration; import static org.apache.hadoop.fs.s3a.impl.ConfigurationHelper.getDuration; import static org.apache.hadoop.util.Preconditions.checkArgument; @@ -420,6 +425,32 @@ private static void initSigner(Configuration conf, } } + /** + * Parses header configuration at the given key. Calls apply callback for headers with + * non-empty header values list. Calls ignore callback for headers with empty header values list. + * + * @param conf hadoop configuration + * @param configKey configuration key + * @param apply apply callback + * @param ignore ignore callback + */ + private static void applyHeaders(Configuration conf, String configKey, + BiConsumer> apply, Consumer ignore) { + Map awsClientCustomHeadersMap = + S3AUtils.getTrimmedStringCollectionSplitByEquals(conf, configKey); + awsClientCustomHeadersMap.forEach((header, valueString) -> { + List headerValues = Arrays.stream(valueString.split(";")) + .map(String::trim) + .filter(v -> !v.isEmpty()) + .collect(Collectors.toList()); + if (!headerValues.isEmpty()) { + apply.accept(header, headerValues); + } else { + ignore.accept(header); + } + }); + } + /** * Initialize custom request headers for AWS clients. * @param conf hadoop configuration @@ -429,32 +460,44 @@ private static void initSigner(Configuration conf, private static void initRequestHeaders(Configuration conf, ClientOverrideConfiguration.Builder clientConfig, String awsServiceIdentifier) { String configKey = null; + String configKeyPrefix = null; switch (awsServiceIdentifier) { case AWS_SERVICE_IDENTIFIER_S3: configKey = CUSTOM_HEADERS_S3; + configKeyPrefix = CUSTOM_REQUEST_HEADERS_S3_PREFIX; break; case AWS_SERVICE_IDENTIFIER_STS: configKey = CUSTOM_HEADERS_STS; + configKeyPrefix = CUSTOM_REQUEST_HEADERS_STS_PREFIX; break; default: // No known service. } if (configKey != null) { - Map awsClientCustomHeadersMap = - S3AUtils.getTrimmedStringCollectionSplitByEquals(conf, configKey); - awsClientCustomHeadersMap.forEach((header, valueString) -> { - List headerValues = Arrays.stream(valueString.split(";")) - .map(String::trim) - .filter(v -> !v.isEmpty()) - .collect(Collectors.toList()); - if (!headerValues.isEmpty()) { - clientConfig.putHeader(header, headerValues); - } else { - LOG.warn("Ignoring header '{}' for {} client because no values were provided", - header, awsServiceIdentifier); - } - }); + // headers for all requests are provided via clientConfig.putHeader + applyHeaders(conf, configKey, clientConfig::putHeader, + (header) -> LOG.warn("Ignoring header '{}' for {} client because no values were provided", + header, awsServiceIdentifier)); LOG.debug("headers for {} client = {}", awsServiceIdentifier, clientConfig.headers()); + + // per-request headers are provided via AddRequestHeaderInterceptor + String keyPrefix = configKeyPrefix; + Map>> requestHeaders = new HashMap<>(); + Map requestHeaderConfs = conf.getPropsWithPrefix(keyPrefix); + requestHeaderConfs.keySet().forEach((request) -> + applyHeaders(conf, keyPrefix + request, + (header, headerValues) -> + requestHeaders.computeIfAbsent(request, c -> new HashMap<>()).put(header, headerValues), + (header) -> LOG.warn("Ignoring {} request header '{}' for {} client because " + + "no values were provided", request, header, awsServiceIdentifier) + ) + ); + if (!requestHeaders.isEmpty()) { + clientConfig.addExecutionInterceptor(new AddRequestHeaderInterceptor(requestHeaders)); + requestHeaders.forEach((request, headers) -> + LOG.debug("{} request headers for {} client = {}", request, awsServiceIdentifier, headers) + ); + } } } diff --git a/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/impl/AddRequestHeaderInterceptor.java b/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/impl/AddRequestHeaderInterceptor.java new file mode 100644 index 00000000000000..3bfa69484b1733 --- /dev/null +++ b/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/impl/AddRequestHeaderInterceptor.java @@ -0,0 +1,62 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.hadoop.fs.s3a.impl; + +import software.amazon.awssdk.awscore.AwsRequest; +import software.amazon.awssdk.awscore.AwsRequestOverrideConfiguration; +import software.amazon.awssdk.core.SdkRequest; +import software.amazon.awssdk.core.interceptor.Context; +import software.amazon.awssdk.core.interceptor.ExecutionAttributes; +import software.amazon.awssdk.core.interceptor.ExecutionInterceptor; + +import java.util.HashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.function.Consumer; + +public class AddRequestHeaderInterceptor implements ExecutionInterceptor { + private final Map> appliers = new HashMap<>(); + + public AddRequestHeaderInterceptor(Map>> requestHeaders) { + requestHeaders.forEach((request, headers) -> appliers + .put(request.toLowerCase(Locale.ROOT), (b) -> headers.forEach(b::putHeader)) + ); + } + + public SdkRequest modifyRequest(Context.ModifyRequest context, ExecutionAttributes executionAttributes) { + assert context.request() instanceof AwsRequest; + + AwsRequest request = (AwsRequest) context.request(); + String requestName = request.getClass().getSimpleName().toLowerCase(Locale.ROOT); + Consumer applier = appliers.get(requestName); + + if (applier != null) { + AwsRequestOverrideConfiguration overrideConfiguration = + request.overrideConfiguration() + .map(AwsRequestOverrideConfiguration::toBuilder) + .orElseGet(AwsRequestOverrideConfiguration::builder) + .applyMutation(applier) + .build(); + return request.toBuilder().overrideConfiguration(overrideConfiguration).build(); + } else { + return request; + } + } +} diff --git a/hadoop-tools/hadoop-aws/src/site/markdown/tools/hadoop-aws/index.md b/hadoop-tools/hadoop-aws/src/site/markdown/tools/hadoop-aws/index.md index 88fcd2db7fbf48..8d899602b99d71 100644 --- a/hadoop-tools/hadoop-aws/src/site/markdown/tools/hadoop-aws/index.md +++ b/hadoop-tools/hadoop-aws/src/site/markdown/tools/hadoop-aws/index.md @@ -951,10 +951,14 @@ The switch to turn S3A auditing on or off. ### Configuring Custom Headers for AWS Service Clients -You can set custom headers for S3 and STS requests. These headers are set on client level, and will be sent for all requests made to these services. +You can set custom headers for S3 and STS requests. Headers can be set on client level and request type level. +Client level headers are sent for all requests made through the client. Request type level headers are sent for +requests of the respective type only. + +#### Client Level Headers **Configuration Properties:** -- `fs.s3a.client.s3.custom.headers`: Custom headers for S3 service requests. +- `fs.s3a.client.s3.custom.headers`: Sets custom headers for S3 service requests. - `fs.s3a.client.sts.custom.headers`: Sets custom headers for all requests to AWS STS. **Header Format:** @@ -973,6 +977,33 @@ Custom headers should be specified as key-value pairs, separated by `=`. Multipl ``` +#### Request-type Level Headers + +**Configuration Properties:** +- `fs.s3a.client.s3.custom.headers.request.REQUEST`: Sets custom headers for S3 service requests of type `REQUEST`. +- `fs.s3a.client.sts.custom.headers.request.REQUEST`: Sets custom headers for all requests to AWS STS of type `REQUEST`. + +Note: `REQUEST` refers to the AWS S3 and STS request name. These request type names are case-insensitive. +See subclasses of S3Request +and subclasses of StsRequest +for all existing requests. + +**Header Format:** +Custom headers should be specified as key-value pairs, separated by `=`. Multiple values for a single header can be separated by `;`. Multiple headers can be separated by `,`. + + +```xml + + fs.s3a.client.s3.custom.headers.request.ListObjectsV2Request + Header1=Value1 + + + + fs.s3a.client.sts.custom.headers.request.deleteobjectrequest + Header1=Value1;Value2,Header2=Value1 + +``` + ## Retry and Recovery The S3A client makes a best-effort attempt at recovering from network failures; diff --git a/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/impl/TestAwsClientConfig.java b/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/impl/TestAwsClientConfig.java index a0437a81817919..bd82146b6f73b9 100644 --- a/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/impl/TestAwsClientConfig.java +++ b/hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/impl/TestAwsClientConfig.java @@ -21,6 +21,8 @@ import java.io.IOException; import java.time.Duration; import java.util.Arrays; +import java.util.HashSet; +import java.util.List; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.Test; @@ -30,6 +32,15 @@ import org.apache.hadoop.conf.Configuration; import org.apache.hadoop.test.AbstractHadoopTestBase; import org.apache.hadoop.util.Lists; +import software.amazon.awssdk.core.SdkRequest; +import software.amazon.awssdk.core.interceptor.Context; +import software.amazon.awssdk.core.interceptor.InterceptorContext; +import software.amazon.awssdk.services.s3.model.CreateBucketRequest; +import software.amazon.awssdk.services.s3.model.DeleteObjectRequest; +import software.amazon.awssdk.services.s3.model.ListObjectsV2Request; +import software.amazon.awssdk.services.sts.model.AssumeRoleRequest; +import software.amazon.awssdk.services.sts.model.DecodeAuthorizationMessageRequest; +import software.amazon.awssdk.services.sts.model.GetSessionTokenRequest; import static org.apache.hadoop.fs.s3a.Constants.AWS_SERVICE_IDENTIFIER_S3; import static org.apache.hadoop.fs.s3a.Constants.AWS_SERVICE_IDENTIFIER_STS; @@ -39,6 +50,8 @@ import static org.apache.hadoop.fs.s3a.Constants.CONNECTION_TTL; import static org.apache.hadoop.fs.s3a.Constants.CUSTOM_HEADERS_S3; import static org.apache.hadoop.fs.s3a.Constants.CUSTOM_HEADERS_STS; +import static org.apache.hadoop.fs.s3a.Constants.CUSTOM_REQUEST_HEADERS_S3_PREFIX; +import static org.apache.hadoop.fs.s3a.Constants.CUSTOM_REQUEST_HEADERS_STS_PREFIX; import static org.apache.hadoop.fs.s3a.Constants.DEFAULT_CONNECTION_ACQUISITION_TIMEOUT_DURATION; import static org.apache.hadoop.fs.s3a.Constants.DEFAULT_CONNECTION_IDLE_TIME_DURATION; import static org.apache.hadoop.fs.s3a.Constants.DEFAULT_CONNECTION_KEEPALIVE; @@ -217,15 +230,26 @@ private void setOptionsToValue(String value, Configuration conf, String... keys) public void testInitRequestHeadersForSTS() throws IOException { final Configuration conf = new Configuration(); conf.set(CUSTOM_HEADERS_STS, "header1=value1;value2,header2=value3"); + conf.set(CUSTOM_REQUEST_HEADERS_STS_PREFIX + "GetSessionTokenRequest", "header3=value4;value5,header4=value6"); + conf.set(CUSTOM_REQUEST_HEADERS_STS_PREFIX + "assumerolerequest", "header5=value7"); assertThat(conf.get(CUSTOM_HEADERS_S3)) .describedAs("Custom client headers for s3 %s", CUSTOM_HEADERS_S3) .isNull(); + assertThat(conf.getPropsWithPrefix(CUSTOM_REQUEST_HEADERS_S3_PREFIX)) + .describedAs("Custom per-request client headers for s3 %s", CUSTOM_REQUEST_HEADERS_S3_PREFIX) + .isEmpty(); assertThat(createClientConfigBuilder(conf, AWS_SERVICE_IDENTIFIER_S3) .headers().size()) .describedAs("Count of S3 client headers") .isEqualTo(0); + assertThat(createClientConfigBuilder(conf, AWS_SERVICE_IDENTIFIER_S3) + .executionInterceptors().stream() + .filter(ei -> ei instanceof AddRequestHeaderInterceptor) + .count()) + .describedAs("Count of request header interceptors of S3 client") + .isEqualTo(0); assertThat(createClientConfigBuilder(conf, AWS_SERVICE_IDENTIFIER_STS) .headers().size()) @@ -241,6 +265,55 @@ public void testInitRequestHeadersForSTS() throws IOException { .headers().get("header2")) .describedAs("STS client 'header2' header value") .isEqualTo(Lists.newArrayList("value3")); + + List interceptors = + createClientConfigBuilder(conf, AWS_SERVICE_IDENTIFIER_STS) + .executionInterceptors().stream() + .filter(ei -> ei instanceof AddRequestHeaderInterceptor) + .map(ie -> (AddRequestHeaderInterceptor) ie) + .toList(); + + assertThat(interceptors.size()) + .describedAs("Count of request header interceptors of STS client") + .isEqualTo(1); + + AddRequestHeaderInterceptor interceptor = interceptors.get(0); + + SdkRequest request = DecodeAuthorizationMessageRequest.builder().build(); + Context.ModifyRequest modifyRequest = InterceptorContext.builder().request(request).build(); + SdkRequest modifiedRequest = interceptor.modifyRequest(modifyRequest, null); + assertThat(modifiedRequest.overrideConfiguration().isPresent()) + .describedAs("STS list request has override configuration") + .isFalse(); + + SdkRequest getRequest = GetSessionTokenRequest.builder().build(); + Context.ModifyRequest modifyGetRequest = InterceptorContext.builder().request(getRequest).build(); + SdkRequest modifiedGetRequest = interceptor.modifyRequest(modifyGetRequest, null); + assertThat(modifiedGetRequest.overrideConfiguration().isPresent()) + .describedAs("STS list request has override configuration") + .isTrue(); + assertThat(modifiedGetRequest.overrideConfiguration().get().headers().keySet()) + .describedAs("STS client request headers") + .isEqualTo(new HashSet<>(Lists.newArrayList("header3", "header4"))); + assertThat(modifiedGetRequest.overrideConfiguration().get().headers().get("header3")) + .describedAs("STS client request 'header3' header value") + .isEqualTo(Lists.newArrayList("value4", "value5")); + assertThat(modifiedGetRequest.overrideConfiguration().get().headers().get("header4")) + .describedAs("STS client request 'header4' header value") + .isEqualTo(Lists.newArrayList("value6")); + + SdkRequest assumeRequest = AssumeRoleRequest.builder().build(); + Context.ModifyRequest modifyAssumeRequest = InterceptorContext.builder().request(assumeRequest).build(); + SdkRequest modifiedAssumeRequest = interceptor.modifyRequest(modifyAssumeRequest, null); + assertThat(modifiedAssumeRequest.overrideConfiguration().isPresent()) + .describedAs("STS delete request has override configuration") + .isTrue(); + assertThat(modifiedAssumeRequest.overrideConfiguration().get().headers().keySet()) + .describedAs("STS client request headers") + .isEqualTo(new HashSet<>(Lists.newArrayList("header5"))); + assertThat(modifiedAssumeRequest.overrideConfiguration().get().headers().get("header5")) + .describedAs("STS client request 'header3' header value") + .isEqualTo(Lists.newArrayList("value7")); } /** @@ -251,15 +324,26 @@ public void testInitRequestHeadersForSTS() throws IOException { public void testInitRequestHeadersForS3() throws IOException { final Configuration conf = new Configuration(); conf.set(CUSTOM_HEADERS_S3, "header1=value1;value2,header2=value3"); + conf.set(CUSTOM_REQUEST_HEADERS_S3_PREFIX + "ListObjectsV2Request", "header3=value4;value5,header4=value6"); + conf.set(CUSTOM_REQUEST_HEADERS_S3_PREFIX + "deleteobjectrequest", "header5=value7"); assertThat(conf.get(CUSTOM_HEADERS_STS)) .describedAs("Custom client headers for STS %s", CUSTOM_HEADERS_STS) .isNull(); + assertThat(conf.getPropsWithPrefix(CUSTOM_REQUEST_HEADERS_STS_PREFIX)) + .describedAs("Custom per-request client headers for STS %s", CUSTOM_REQUEST_HEADERS_STS_PREFIX) + .isEmpty(); assertThat(createClientConfigBuilder(conf, AWS_SERVICE_IDENTIFIER_STS) .headers().size()) .describedAs("Count of STS client headers") .isEqualTo(0); + assertThat(createClientConfigBuilder(conf, AWS_SERVICE_IDENTIFIER_STS) + .executionInterceptors().stream() + .filter(ei -> ei instanceof AddRequestHeaderInterceptor) + .count()) + .describedAs("Count of request header interceptors of STS client") + .isEqualTo(0); assertThat(createClientConfigBuilder(conf, AWS_SERVICE_IDENTIFIER_S3) .headers().size()) @@ -275,6 +359,55 @@ public void testInitRequestHeadersForS3() throws IOException { .headers().get("header2")) .describedAs("S3 client 'header2' header value") .isEqualTo(Lists.newArrayList("value3")); + + List interceptors = + createClientConfigBuilder(conf, AWS_SERVICE_IDENTIFIER_S3) + .executionInterceptors().stream() + .filter(ei -> ei instanceof AddRequestHeaderInterceptor) + .map(ie -> (AddRequestHeaderInterceptor) ie) + .toList(); + + assertThat(interceptors.size()) + .describedAs("Count of request header interceptors of S3 client") + .isEqualTo(1); + + AddRequestHeaderInterceptor interceptor = interceptors.get(0); + + SdkRequest request = CreateBucketRequest.builder().build(); + Context.ModifyRequest modifyRequest = InterceptorContext.builder().request(request).build(); + SdkRequest modifiedRequest = interceptor.modifyRequest(modifyRequest, null); + assertThat(modifiedRequest.overrideConfiguration().isPresent()) + .describedAs("S3 list request has override configuration") + .isFalse(); + + SdkRequest listRequest = ListObjectsV2Request.builder().build(); + Context.ModifyRequest modifyListRequest = InterceptorContext.builder().request(listRequest).build(); + SdkRequest modifiedListRequest = interceptor.modifyRequest(modifyListRequest, null); + assertThat(modifiedListRequest.overrideConfiguration().isPresent()) + .describedAs("S3 list request has override configuration") + .isTrue(); + assertThat(modifiedListRequest.overrideConfiguration().get().headers().keySet()) + .describedAs("S3 client request headers") + .isEqualTo(new HashSet<>(Lists.newArrayList("header3", "header4"))); + assertThat(modifiedListRequest.overrideConfiguration().get().headers().get("header3")) + .describedAs("S3 client request 'header3' header value") + .isEqualTo(Lists.newArrayList("value4", "value5")); + assertThat(modifiedListRequest.overrideConfiguration().get().headers().get("header4")) + .describedAs("S3 client request 'header4' header value") + .isEqualTo(Lists.newArrayList("value6")); + + SdkRequest deleteRequest = DeleteObjectRequest.builder().build(); + Context.ModifyRequest modifyDeleteRequest = InterceptorContext.builder().request(deleteRequest).build(); + SdkRequest modifiedDeleteRequest = interceptor.modifyRequest(modifyDeleteRequest, null); + assertThat(modifiedDeleteRequest.overrideConfiguration().isPresent()) + .describedAs("S3 delete request has override configuration") + .isTrue(); + assertThat(modifiedDeleteRequest.overrideConfiguration().get().headers().keySet()) + .describedAs("S3 client request headers") + .isEqualTo(new HashSet<>(Lists.newArrayList("header5"))); + assertThat(modifiedDeleteRequest.overrideConfiguration().get().headers().get("header5")) + .describedAs("S3 client request 'header3' header value") + .isEqualTo(Lists.newArrayList("value7")); } /**