diff --git a/.changes/next-release/bugfix-AWSSDKforJavav2-d33383e.json b/.changes/next-release/bugfix-AWSSDKforJavav2-d33383e.json new file mode 100644 index 000000000000..7dc7df981a64 --- /dev/null +++ b/.changes/next-release/bugfix-AWSSDKforJavav2-d33383e.json @@ -0,0 +1,5 @@ +{ + "category": "AWS SDK for Java v2", + "type": "bugfix", + "description": "Defer all errors raised when creating `ProfileCredentialsProvider` to the `resolveCredentials()` call." +} diff --git a/.changes/next-release/feature-AWSSDKforJavav2-63c04be.json b/.changes/next-release/feature-AWSSDKforJavav2-63c04be.json new file mode 100644 index 000000000000..c63668511add --- /dev/null +++ b/.changes/next-release/feature-AWSSDKforJavav2-63c04be.json @@ -0,0 +1,5 @@ +{ + "category": "AWS SDK for Java v2", + "type": "feature", + "description": "Never initialize the default credentials provider chain if credentials are always specified in the client builder." +} diff --git a/.changes/next-release/feature-AWSSDKforJavav2-6c062f1.json b/.changes/next-release/feature-AWSSDKforJavav2-6c062f1.json new file mode 100644 index 000000000000..c7cc0825cbe0 --- /dev/null +++ b/.changes/next-release/feature-AWSSDKforJavav2-6c062f1.json @@ -0,0 +1,5 @@ +{ + "category": "AWS SDK for Java v2", + "type": "feature", + "description": "Never initialie the default region provider chain if the region is always specified in the client builder." +} diff --git a/core/auth/src/main/java/software/amazon/awssdk/auth/credentials/DefaultCredentialsProvider.java b/core/auth/src/main/java/software/amazon/awssdk/auth/credentials/DefaultCredentialsProvider.java index 91f428bdba9b..a8cbf236fbbb 100644 --- a/core/auth/src/main/java/software/amazon/awssdk/auth/credentials/DefaultCredentialsProvider.java +++ b/core/auth/src/main/java/software/amazon/awssdk/auth/credentials/DefaultCredentialsProvider.java @@ -16,6 +16,7 @@ package software.amazon.awssdk.auth.credentials; import software.amazon.awssdk.annotations.SdkPublicApi; +import software.amazon.awssdk.auth.credentials.internal.LazyAwsCredentialsProvider; import software.amazon.awssdk.utils.SdkAutoCloseable; import software.amazon.awssdk.utils.ToString; @@ -41,7 +42,7 @@ public final class DefaultCredentialsProvider implements AwsCredentialsProvider, private static final DefaultCredentialsProvider DEFAULT_CREDENTIALS_PROVIDER = new DefaultCredentialsProvider(builder()); - private final AwsCredentialsProviderChain providerChain; + private final LazyAwsCredentialsProvider providerChain; /** * @see #builder() @@ -61,23 +62,28 @@ public static DefaultCredentialsProvider create() { /** * Create the default credential chain using the configuration in the provided builder. */ - private static AwsCredentialsProviderChain createChain(Builder builder) { - AwsCredentialsProvider[] credentialsProviders = new AwsCredentialsProvider[] { - SystemPropertyCredentialsProvider.create(), - EnvironmentVariableCredentialsProvider.create(), - ProfileCredentialsProvider.create(), - ContainerCredentialsProvider.builder() - .asyncCredentialUpdateEnabled(builder.asyncCredentialUpdateEnabled) - .build(), - InstanceProfileCredentialsProvider.builder() - .asyncCredentialUpdateEnabled(builder.asyncCredentialUpdateEnabled) - .build() - }; - - return AwsCredentialsProviderChain.builder() - .reuseLastProviderEnabled(builder.reuseLastProviderEnabled) - .credentialsProviders(credentialsProviders) - .build(); + private static LazyAwsCredentialsProvider createChain(Builder builder) { + boolean asyncCredentialUpdateEnabled = builder.asyncCredentialUpdateEnabled; + boolean reuseLastProviderEnabled = builder.reuseLastProviderEnabled; + + return LazyAwsCredentialsProvider.create(() -> { + AwsCredentialsProvider[] credentialsProviders = new AwsCredentialsProvider[] { + SystemPropertyCredentialsProvider.create(), + EnvironmentVariableCredentialsProvider.create(), + ProfileCredentialsProvider.create(), + ContainerCredentialsProvider.builder() + .asyncCredentialUpdateEnabled(asyncCredentialUpdateEnabled) + .build(), + InstanceProfileCredentialsProvider.builder() + .asyncCredentialUpdateEnabled(asyncCredentialUpdateEnabled) + .build() + }; + + return AwsCredentialsProviderChain.builder() + .reuseLastProviderEnabled(reuseLastProviderEnabled) + .credentialsProviders(credentialsProviders) + .build(); + }); } /** diff --git a/core/auth/src/main/java/software/amazon/awssdk/auth/credentials/ProfileCredentialsProvider.java b/core/auth/src/main/java/software/amazon/awssdk/auth/credentials/ProfileCredentialsProvider.java index ae5a919bdd13..fe13174f2c31 100644 --- a/core/auth/src/main/java/software/amazon/awssdk/auth/credentials/ProfileCredentialsProvider.java +++ b/core/auth/src/main/java/software/amazon/awssdk/auth/credentials/ProfileCredentialsProvider.java @@ -52,28 +52,47 @@ public final class ProfileCredentialsProvider implements AwsCredentialsProvider, * @see #builder() */ private ProfileCredentialsProvider(BuilderImpl builder) { - this.profileName = builder.profileName != null ? builder.profileName - : ProfileFileSystemSetting.AWS_PROFILE.getStringValueOrThrow(); - - // Load the profiles file - this.profileFile = Optional.ofNullable(builder.profileFile) - .orElseGet(builder.defaultProfileFileLoader); - - // Load the profile and credentials provider - this.credentialsProvider = profileFile.profile(profileName) - .flatMap(p -> new ProfileCredentialsUtils(p, profileFile::profile) - .credentialsProvider()) - .orElse(null); - - // If we couldn't load the credentials provider for some reason, save an exception describing why. This exception will - // only be raised on calls to getCredentials. We don't want to raise an exception here because it may be expected (eg. in - // the default credential chain). - if (credentialsProvider == null) { - String loadError = String.format("Profile file contained no credentials for profile '%s': %s", - profileName, profileFile); - this.loadException = SdkClientException.builder().message(loadError).build(); + AwsCredentialsProvider credentialsProvider = null; + RuntimeException loadException = null; + ProfileFile profileFile = null; + String profileName = null; + + try { + profileName = builder.profileName != null ? builder.profileName + : ProfileFileSystemSetting.AWS_PROFILE.getStringValueOrThrow(); + + // Load the profiles file + profileFile = Optional.ofNullable(builder.profileFile) + .orElseGet(builder.defaultProfileFileLoader); + + // Load the profile and credentials provider + String finalProfileName = profileName; + ProfileFile finalProfileFile = profileFile; + credentialsProvider = + profileFile.profile(profileName) + .flatMap(p -> new ProfileCredentialsUtils(p, finalProfileFile::profile).credentialsProvider()) + .orElseThrow(() -> { + String errorMessage = String.format("Profile file contained no credentials for " + + "profile '%s': %s", finalProfileName, finalProfileFile); + return SdkClientException.builder().message(errorMessage).build(); + }); + } catch (RuntimeException e) { + // If we couldn't load the credentials provider for some reason, save an exception describing why. This exception + // will only be raised on calls to getCredentials. We don't want to raise an exception here because it may be + // expected (eg. in the default credential chain). + loadException = e; + } + + if (loadException != null) { + this.loadException = loadException; + this.credentialsProvider = null; + this.profileFile = null; + this.profileName = null; } else { this.loadException = null; + this.credentialsProvider = credentialsProvider; + this.profileFile = profileFile; + this.profileName = profileName; } } diff --git a/core/auth/src/main/java/software/amazon/awssdk/auth/credentials/internal/LazyAwsCredentialsProvider.java b/core/auth/src/main/java/software/amazon/awssdk/auth/credentials/internal/LazyAwsCredentialsProvider.java new file mode 100644 index 000000000000..54260eca2e8a --- /dev/null +++ b/core/auth/src/main/java/software/amazon/awssdk/auth/credentials/internal/LazyAwsCredentialsProvider.java @@ -0,0 +1,67 @@ +/* + * Copyright 2010-2019 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.auth.credentials.internal; + +import java.util.function.Supplier; +import software.amazon.awssdk.annotations.SdkInternalApi; +import software.amazon.awssdk.auth.credentials.AwsCredentials; +import software.amazon.awssdk.auth.credentials.AwsCredentialsProvider; +import software.amazon.awssdk.utils.IoUtils; +import software.amazon.awssdk.utils.SdkAutoCloseable; +import software.amazon.awssdk.utils.ToString; + +/** + * A wrapper for {@link AwsCredentialsProvider} that defers creation of the underlying provider until the first time the + * {@link AwsCredentialsProvider#resolveCredentials()} method is invoked. + */ +@SdkInternalApi +public class LazyAwsCredentialsProvider implements AwsCredentialsProvider, SdkAutoCloseable { + private final Supplier delegateConstructor; + private volatile AwsCredentialsProvider delegate; + + private LazyAwsCredentialsProvider(Supplier delegateConstructor) { + this.delegateConstructor = delegateConstructor; + } + + public static LazyAwsCredentialsProvider create(Supplier delegateConstructor) { + return new LazyAwsCredentialsProvider(delegateConstructor); + } + + @Override + public AwsCredentials resolveCredentials() { + if (delegate == null) { + synchronized (this) { + if (delegate == null) { + delegate = delegateConstructor.get(); + } + } + } + return delegate.resolveCredentials(); + } + + @Override + public void close() { + IoUtils.closeIfCloseable(delegate, null); + } + + @Override + public String toString() { + return ToString.builder("LazyAwsCredentialsProvider") + .add("delegateConstructor", delegateConstructor) + .add("delegate", delegate) + .build(); + } +} diff --git a/core/auth/src/test/java/software/amazon/awssdk/auth/credentials/ProfileCredentialsProviderTest.java b/core/auth/src/test/java/software/amazon/awssdk/auth/credentials/ProfileCredentialsProviderTest.java index b8dbd8c86338..a1f36c7ae0cb 100644 --- a/core/auth/src/test/java/software/amazon/awssdk/auth/credentials/ProfileCredentialsProviderTest.java +++ b/core/auth/src/test/java/software/amazon/awssdk/auth/credentials/ProfileCredentialsProviderTest.java @@ -27,6 +27,16 @@ * Verify functionality of {@link ProfileCredentialsProvider}. */ public class ProfileCredentialsProviderTest { + @Test + public void missingCredentialsFileThrowsExceptionInGetCredentials() { + ProfileCredentialsProvider provider = + new ProfileCredentialsProvider.BuilderImpl() + .defaultProfileFileLoader(() -> { throw new IllegalStateException(); }) + .build(); + + assertThatThrownBy(provider::resolveCredentials).isInstanceOf(IllegalStateException.class); + } + @Test public void missingProfileFileThrowsExceptionInGetCredentials() { ProfileCredentialsProvider provider = diff --git a/core/auth/src/test/java/software/amazon/awssdk/auth/credentials/internal/LazyAwsCredentialsProviderTest.java b/core/auth/src/test/java/software/amazon/awssdk/auth/credentials/internal/LazyAwsCredentialsProviderTest.java new file mode 100644 index 000000000000..f669402b1fac --- /dev/null +++ b/core/auth/src/test/java/software/amazon/awssdk/auth/credentials/internal/LazyAwsCredentialsProviderTest.java @@ -0,0 +1,51 @@ +/* + * Copyright 2010-2019 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.auth.credentials.internal; + +import java.util.function.Supplier; +import org.junit.Before; +import org.junit.Test; +import org.mockito.Mockito; +import software.amazon.awssdk.auth.credentials.AwsCredentialsProvider; + +public class LazyAwsCredentialsProviderTest { + @SuppressWarnings("unchecked") + private Supplier credentialsConstructor = Mockito.mock(Supplier.class); + + private AwsCredentialsProvider credentials = Mockito.mock(AwsCredentialsProvider.class); + + @Before + public void reset() { + Mockito.reset(credentials, credentialsConstructor); + Mockito.when(credentialsConstructor.get()).thenReturn(credentials); + } + + @Test + public void creationDoesntInvokeSupplier() { + LazyAwsCredentialsProvider.create(credentialsConstructor); + Mockito.verifyZeroInteractions(credentialsConstructor); + } + + @Test + public void resolveCredentialsInvokesSupplierExactlyOnce() { + LazyAwsCredentialsProvider credentialsProvider = LazyAwsCredentialsProvider.create(credentialsConstructor); + credentialsProvider.resolveCredentials(); + credentialsProvider.resolveCredentials(); + + Mockito.verify(credentialsConstructor, Mockito.times(1)).get(); + Mockito.verify(credentials, Mockito.times(2)).resolveCredentials(); + } +} diff --git a/core/aws-core/src/main/java/software/amazon/awssdk/awscore/client/builder/AwsDefaultClientBuilder.java b/core/aws-core/src/main/java/software/amazon/awssdk/awscore/client/builder/AwsDefaultClientBuilder.java index 466ddad904a0..98b1ae31d582 100644 --- a/core/aws-core/src/main/java/software/amazon/awssdk/awscore/client/builder/AwsDefaultClientBuilder.java +++ b/core/aws-core/src/main/java/software/amazon/awssdk/awscore/client/builder/AwsDefaultClientBuilder.java @@ -35,6 +35,7 @@ import software.amazon.awssdk.regions.ServiceMetadata; import software.amazon.awssdk.regions.providers.AwsRegionProvider; import software.amazon.awssdk.regions.providers.DefaultAwsRegionProviderChain; +import software.amazon.awssdk.regions.providers.LazyAwsRegionProvider; import software.amazon.awssdk.utils.AttributeMap; /** @@ -60,7 +61,8 @@ public abstract class AwsDefaultClientBuilder implements AwsClientBuilder { private static final String DEFAULT_ENDPOINT_PROTOCOL = "https"; - private static final AwsRegionProvider DEFAULT_REGION_PROVIDER = new DefaultAwsRegionProviderChain(); + private static final AwsRegionProvider DEFAULT_REGION_PROVIDER = + new LazyAwsRegionProvider(DefaultAwsRegionProviderChain::new); protected AwsDefaultClientBuilder() { super(); diff --git a/core/regions/src/main/java/software/amazon/awssdk/regions/providers/LazyAwsRegionProvider.java b/core/regions/src/main/java/software/amazon/awssdk/regions/providers/LazyAwsRegionProvider.java new file mode 100644 index 000000000000..85c239d559f9 --- /dev/null +++ b/core/regions/src/main/java/software/amazon/awssdk/regions/providers/LazyAwsRegionProvider.java @@ -0,0 +1,55 @@ +/* + * Copyright 2010-2019 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.regions.providers; + +import java.util.function.Supplier; +import software.amazon.awssdk.annotations.SdkProtectedApi; +import software.amazon.awssdk.regions.Region; +import software.amazon.awssdk.utils.ToString; + +/** + * A wrapper for {@link AwsRegionProvider} that defers creation of the underlying provider until the first time the + * {@link AwsRegionProvider#getRegion()} method is invoked. + */ +@SdkProtectedApi +public class LazyAwsRegionProvider implements AwsRegionProvider { + private final Supplier delegateConstructor; + private volatile AwsRegionProvider delegate; + + public LazyAwsRegionProvider(Supplier delegateConstructor) { + this.delegateConstructor = delegateConstructor; + } + + @Override + public Region getRegion() { + if (delegate == null) { + synchronized (this) { + if (delegate == null) { + delegate = delegateConstructor.get(); + } + } + } + return delegate.getRegion(); + } + + @Override + public String toString() { + return ToString.builder("LazyAwsRegionProvider") + .add("delegateConstructor", delegateConstructor) + .add("delegate", delegate) + .build(); + } +} diff --git a/core/regions/src/test/java/software/amazon/awssdk/regions/providers/LazyAwsRegionProviderTest.java b/core/regions/src/test/java/software/amazon/awssdk/regions/providers/LazyAwsRegionProviderTest.java new file mode 100644 index 000000000000..e2ff7246878f --- /dev/null +++ b/core/regions/src/test/java/software/amazon/awssdk/regions/providers/LazyAwsRegionProviderTest.java @@ -0,0 +1,50 @@ +/* + * Copyright 2010-2019 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.regions.providers; + +import java.util.function.Supplier; +import org.junit.Before; +import org.junit.Test; +import org.mockito.Mockito; + +public class LazyAwsRegionProviderTest { + @SuppressWarnings("unchecked") + private Supplier regionProviderConstructor = Mockito.mock(Supplier.class); + + private AwsRegionProvider regionProvider = Mockito.mock(AwsRegionProvider.class); + + @Before + public void reset() { + Mockito.reset(regionProvider, regionProviderConstructor); + Mockito.when(regionProviderConstructor.get()).thenReturn(regionProvider); + } + + @Test + public void creationDoesntInvokeSupplier() { + new LazyAwsRegionProvider(regionProviderConstructor); + Mockito.verifyZeroInteractions(regionProviderConstructor); + } + + @Test + public void getRegionInvokesSupplierExactlyOnce() { + LazyAwsRegionProvider lazyRegionProvider = new LazyAwsRegionProvider(regionProviderConstructor); + lazyRegionProvider.getRegion(); + lazyRegionProvider.getRegion(); + + Mockito.verify(regionProviderConstructor, Mockito.times(1)).get(); + Mockito.verify(regionProvider, Mockito.times(2)).getRegion(); + } +} \ No newline at end of file diff --git a/core/sdk-core/src/main/java/software/amazon/awssdk/core/client/config/SdkClientConfiguration.java b/core/sdk-core/src/main/java/software/amazon/awssdk/core/client/config/SdkClientConfiguration.java index 2ed45017c6d8..3372007d98aa 100644 --- a/core/sdk-core/src/main/java/software/amazon/awssdk/core/client/config/SdkClientConfiguration.java +++ b/core/sdk-core/src/main/java/software/amazon/awssdk/core/client/config/SdkClientConfiguration.java @@ -16,7 +16,6 @@ package software.amazon.awssdk.core.client.config; import java.util.function.Consumer; - import software.amazon.awssdk.annotations.SdkProtectedApi; import software.amazon.awssdk.utils.AttributeMap; import software.amazon.awssdk.utils.SdkAutoCloseable;