Skip to content

Make default region and credential loading lazy. #1068

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Feb 8, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changes/next-release/bugfix-AWSSDKforJavav2-d33383e.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"category": "AWS SDK for Java v2",
"type": "bugfix",
"description": "Defer all errors raised when creating `ProfileCredentialsProvider` to the `resolveCredentials()` call."
}
5 changes: 5 additions & 0 deletions .changes/next-release/feature-AWSSDKforJavav2-63c04be.json
Original file line number Diff line number Diff line change
@@ -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."
}
5 changes: 5 additions & 0 deletions .changes/next-release/feature-AWSSDKforJavav2-6c062f1.json
Original file line number Diff line number Diff line change
@@ -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."
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -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()
Expand All @@ -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();
});
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -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<AwsCredentialsProvider> delegateConstructor;
private volatile AwsCredentialsProvider delegate;

private LazyAwsCredentialsProvider(Supplier<AwsCredentialsProvider> delegateConstructor) {
this.delegateConstructor = delegateConstructor;
}

public static LazyAwsCredentialsProvider create(Supplier<AwsCredentialsProvider> 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();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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 =
Expand Down
Original file line number Diff line number Diff line change
@@ -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<AwsCredentialsProvider> 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();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;

/**
Expand All @@ -60,7 +61,8 @@ public abstract class AwsDefaultClientBuilder<BuilderT extends AwsClientBuilder<
extends SdkDefaultClientBuilder<BuilderT, ClientT>
implements AwsClientBuilder<BuilderT, ClientT> {
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();
Expand Down
Original file line number Diff line number Diff line change
@@ -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<AwsRegionProvider> delegateConstructor;
private volatile AwsRegionProvider delegate;

public LazyAwsRegionProvider(Supplier<AwsRegionProvider> delegateConstructor) {
this.delegateConstructor = delegateConstructor;
}

@Override
public Region getRegion() {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

resolveRegion()? just "region()"?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's a protected method.

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();
}
}
Loading