diff --git a/.changes/next-release/feature-AWSSKDforJavav2-bdf010f.json b/.changes/next-release/feature-AWSSKDforJavav2-bdf010f.json new file mode 100644 index 000000000000..467081baa914 --- /dev/null +++ b/.changes/next-release/feature-AWSSKDforJavav2-bdf010f.json @@ -0,0 +1,6 @@ +{ + "type": "feature", + "category": "AWS SDK for Java v2", + "contributor": "", + "description": "Added functionality to be able to configure an endpoint override through the [services] section in the aws config file for specific services. \nhttps://docs.aws.amazon.com/sdkref/latest/guide/feature-ss-endpoints.html" +} diff --git a/core/aws-core/src/main/java/software/amazon/awssdk/awscore/endpoint/AwsClientEndpointProvider.java b/core/aws-core/src/main/java/software/amazon/awssdk/awscore/endpoint/AwsClientEndpointProvider.java index 2257666f3324..fbee6e0ec47f 100644 --- a/core/aws-core/src/main/java/software/amazon/awssdk/awscore/endpoint/AwsClientEndpointProvider.java +++ b/core/aws-core/src/main/java/software/amazon/awssdk/awscore/endpoint/AwsClientEndpointProvider.java @@ -50,6 +50,7 @@ *
  • The service-agnostic endpoint override system property (i.e. 'aws.endpointUrl')
  • *
  • The service-specific endpoint override environment variable (e.g. 'AWS_ENDPOINT_URL_S3')
  • *
  • The service-agnostic endpoint override environment variable (i.e. 'AWS_ENDPOINT_URL')
  • + *
  • The service-specific endpoint override from services section (e.g. '[services dev] s3.endpoint_url')
  • *
  • The service-specific endpoint override profile property (e.g. 's3.endpoint_url')
  • *
  • The service-agnostic endpoint override profile property (i.e. 'endpoint_url')
  • *
  • The {@link ServiceMetadata} for the service
  • @@ -129,6 +130,15 @@ private Optional clientEndpointFromEnvironment(Builder builder) () -> systemProperty(GLOBAL_ENDPOINT_OVERRIDE_SYSTEM_PROPERTY), () -> environmentVariable(builder.serviceEndpointOverrideEnvironmentVariable), () -> environmentVariable(GLOBAL_ENDPOINT_OVERRIDE_ENVIRONMENT_VARIABLE), + () -> servicesProperty(builder), + + /* + * This is a deviation from the cross-SDK standard. + * There should not have been support for service-specific + * endpoint override under the [profile] section. + * It is in this order to maintain backwards compatibility, and to reflect that + * service-specific endpoint overrides from the [services] section should be preferred. + */ () -> profileProperty(builder, builder.serviceProfileProperty + "." + ProfileProperty.ENDPOINT_URL), @@ -156,6 +166,20 @@ private Optional profileProperty(Builder builder, String profileProperty) { .flatMap(p -> p.property(profileProperty))); } + private Optional servicesProperty(Builder builder) { + Optional profileFile = Optional.ofNullable(builder.profileFile.get()); + Optional servicesSectionName = profileFile + .flatMap(pf -> pf.profile(builder.profileName)) + .flatMap(p -> p.property("services")); + + Optional serviceEndpoint = servicesSectionName + .flatMap(name -> profileFile.flatMap(pf -> pf.getSection("services", name))) + .flatMap(p -> p.property(builder.serviceProfileProperty + + "." + ProfileProperty.ENDPOINT_URL)); + + return createUri("services section property", serviceEndpoint); + } + private Optional clientEndpointFromServiceMetadata(Builder builder) { // This value is generally overridden after endpoints 2.0. It seems to exist for backwards-compatibility // with older client versions or interceptors. diff --git a/core/profiles/src/main/java/software/amazon/awssdk/profiles/internal/ProfileSection.java b/core/profiles/src/main/java/software/amazon/awssdk/profiles/internal/ProfileSection.java index cfa47e02e75e..24cbe1d9fa81 100644 --- a/core/profiles/src/main/java/software/amazon/awssdk/profiles/internal/ProfileSection.java +++ b/core/profiles/src/main/java/software/amazon/awssdk/profiles/internal/ProfileSection.java @@ -29,7 +29,13 @@ public enum ProfileSection { * are part of a named collection of attributes. * This `sso-session` section is referenced by the user when configuring a profile to derive an SSO token. */ - SSO_SESSION("sso-session", "sso_session"); + SSO_SESSION("sso-session", "sso_session"), + + /** + * A `services` section declares a group of service-specific configurations that can be referenced by profiles. + * Service sub-sections use standardized names derived from the AWS service identifiers. + */ + SERVICES("services", "services"); private final String sectionTitle; private final String propertyKeyName; diff --git a/test/codegen-generated-classes-test/src/test/java/software/amazon/awssdk/services/EndpointSharedConfigTest.java b/test/codegen-generated-classes-test/src/test/java/software/amazon/awssdk/services/EndpointSharedConfigTest.java index 546810cf1c50..78662bb0dc59 100644 --- a/test/codegen-generated-classes-test/src/test/java/software/amazon/awssdk/services/EndpointSharedConfigTest.java +++ b/test/codegen-generated-classes-test/src/test/java/software/amazon/awssdk/services/EndpointSharedConfigTest.java @@ -76,6 +76,13 @@ public void resolvesCorrectEndpoint() { .append(" endpoint_url = ").append(testCase.serviceProfileSetting).append("\n"); } + if (testCase.serviceSectionProfileSetting != null) { + profileFileContent.append("services = dev\n\n"); + profileFileContent.append("[services dev] \n") + .append("amazonprotocolrestjson =\n") + .append(" endpoint_url = ").append(testCase.serviceSectionProfileSetting).append("\n"); + } + ProfileFile profileFile = ProfileFile.builder() .type(ProfileFile.Type.CONFIGURATION) @@ -119,6 +126,7 @@ public static Iterable testCases() { "Global system property", "Service environment variable", "Global environment variable", + "Services Section profile file", "Service profile file", "Global profile file"); @@ -209,6 +217,7 @@ public static class TestCase { "https://global-system-property-endpoint.com", "https://service-env-var-endpoint.com", "https://global-env-var-endpoint.com", + "https://service-section-endpoint.com", "https://service-profile-endpoint.com", "https://global-profile-endpoint.com"); @@ -219,12 +228,13 @@ public static class TestCase { private final String globalEnvVarSetting; private final String serviceProfileSetting; private final String globalProfileSetting; + private final String serviceSectionProfileSetting; private final String caseName; private final String expectedEndpoint; public TestCase(boolean[] settings, Integer expectedEndpointIndex, String caseName) { this(endpoint(settings, 0), endpoint(settings, 1), endpoint(settings, 2), endpoint(settings, 3), - endpoint(settings, 4), endpoint(settings, 5), endpoint(settings, 6), + endpoint(settings, 4), endpoint(settings, 5), endpoint(settings, 6), endpoint(settings, 7), endpointForIndex(expectedEndpointIndex), caseName); } @@ -244,6 +254,7 @@ private TestCase(String clientSetting, String globalSystemPropSetting, String serviceEnvVarSetting, String globalEnvVarSetting, + String serviceSectionProfileSetting, String serviceProfileSetting, String globalProfileSetting, String expectedEndpoint, @@ -255,6 +266,7 @@ private TestCase(String clientSetting, this.globalEnvVarSetting = globalEnvVarSetting; this.serviceProfileSetting = serviceProfileSetting; this.globalProfileSetting = globalProfileSetting; + this.serviceSectionProfileSetting = serviceSectionProfileSetting; this.expectedEndpoint = expectedEndpoint; this.caseName = caseName; } diff --git a/test/codegen-generated-classes-test/src/test/java/software/amazon/awssdk/services/ProfileFileServicesTest.java b/test/codegen-generated-classes-test/src/test/java/software/amazon/awssdk/services/ProfileFileServicesTest.java new file mode 100644 index 000000000000..ae8314a8b0d4 --- /dev/null +++ b/test/codegen-generated-classes-test/src/test/java/software/amazon/awssdk/services/ProfileFileServicesTest.java @@ -0,0 +1,127 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file 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 software.amazon.awssdk.services; + +import java.util.Optional; +import org.junit.jupiter.api.Test; +import software.amazon.awssdk.auth.credentials.AnonymousCredentialsProvider; +import software.amazon.awssdk.awscore.endpoint.AwsClientEndpointProvider; +import software.amazon.awssdk.profiles.Profile; +import software.amazon.awssdk.profiles.ProfileFile; +import software.amazon.awssdk.regions.Region; +import software.amazon.awssdk.services.protocolrestjson.ProtocolRestJsonClient; +import software.amazon.awssdk.services.protocolrestjson.ProtocolRestJsonClientBuilder; + +import static org.assertj.core.api.Assertions.assertThat; + +public class ProfileFileServicesTest { + + @Test + public void servicesSection_shouldRecognizeServicesSection() { + String profileContent = + "[services dev]\n" + + "s3 = \n" + + " endpoint_url = https://foo.bar:9000\n"; + + ProfileFile profileFile = ProfileFile.builder() + .content(profileContent) + .type(ProfileFile.Type.CONFIGURATION) + .build(); + + Optional servicesSection = profileFile.getSection("services", "dev"); + assertThat(servicesSection).isPresent(); + } + + @Test + public void servicesSection_canParseServicesSectionProperties() { + String profileContent = + "[services dev]\n" + + "s3 = \n" + + " endpoint_url = https://foo.bar:9000\n" + + " foo = bar\n" + + "\n" + + "[profile test-profile]\n" + + "services = dev"; + + ProfileFile profileFile = ProfileFile.builder() + .content(profileContent) + .type(ProfileFile.Type.CONFIGURATION) + .build(); + + Optional servicesSection = profileFile.getSection("services", "dev"); + assertThat(servicesSection).isPresent(); + + Profile services = servicesSection.get(); + assertThat(services.properties()) + .containsEntry("s3.endpoint_url", "https://foo.bar:9000"); + } + + + @Test + public void servicesSection_canParseMultipleServicesInSection() { + String profileContent = + "[services testing-s3-and-eb]\n" + + "s3 = \n" + + " endpoint_url = http://localhost:4567\n" + + "elastic_beanstalk = \n" + + " endpoint_url = http://localhost:8000\n" + + "\n" + + "[profile dev]\n" + + "services = testing-s3-and-eb"; + + ProfileFile profileFile = ProfileFile.builder() + .content(profileContent) + .type(ProfileFile.Type.CONFIGURATION) + .build(); + + Optional servicesSection = profileFile.getSection("services", "testing-s3-and-eb"); + assertThat(servicesSection).isPresent(); + + Profile services = servicesSection.get(); + assertThat(services.properties()) + .containsEntry("s3.endpoint_url", "http://localhost:4567") + .containsEntry("elastic_beanstalk.endpoint_url", "http://localhost:8000"); + } + + @org.junit.Test(expected = EndpointCapturingInterceptor.CaptureCompletedException.class) + public void invalidNestedBlockFormat_shouldThrowCaptureCompletedException() { + StringBuilder profileFileContent = new StringBuilder(); + profileFileContent.append("[default] \n") + .append("services = dev \n") + .append("\n") + .append("[services dev] \n") + .append("amazonprotocolrestjson =\n") + .append("endpoint_url ="); + + ProfileFile profileFile = ProfileFile.builder() + .type(ProfileFile.Type.CONFIGURATION) + .content(profileFileContent.toString()) + .build(); + + ProtocolRestJsonClientBuilder builder = ProtocolRestJsonClient.builder() + .region(Region.US_WEST_2) + .credentialsProvider(AnonymousCredentialsProvider.create()) + .overrideConfiguration(c -> c.defaultProfileFile(profileFile) + .defaultProfileName("default")); + + EndpointCapturingInterceptor interceptor = new EndpointCapturingInterceptor(); + builder.overrideConfiguration(b -> b.addExecutionInterceptor(interceptor)); + + ProtocolRestJsonClient client = builder.build(); + + client.allTypes(); + } +} \ No newline at end of file