diff --git a/.changes/next-release/feature-AWSSDKforJavav2-688d30e.json b/.changes/next-release/feature-AWSSDKforJavav2-688d30e.json
new file mode 100644
index 000000000000..3fb53dd0ab26
--- /dev/null
+++ b/.changes/next-release/feature-AWSSDKforJavav2-688d30e.json
@@ -0,0 +1,6 @@
+{
+ "category": "AWS SDK for Java v2",
+ "contributor": "",
+ "type": "feature",
+ "description": "- ProfileCredentialsProvider and ProfileTokenProvider can reload credentials when disk profile changes\n- Updated DefaultCredentialsProvider chain for reloading credentials\n- Service support classes store ProfileFile as a Supplier interface\n- SdkClientOption and SdkExecutionAttributes constants for PROFILE_FILE have been deprecated in favor of PROFILE_FILE_SUPPLIER"
+}
diff --git a/codegen/src/main/java/software/amazon/awssdk/codegen/poet/builder/BaseClientBuilderClass.java b/codegen/src/main/java/software/amazon/awssdk/codegen/poet/builder/BaseClientBuilderClass.java
index df7e6d3b2895..2bca22172f0c 100644
--- a/codegen/src/main/java/software/amazon/awssdk/codegen/poet/builder/BaseClientBuilderClass.java
+++ b/codegen/src/main/java/software/amazon/awssdk/codegen/poet/builder/BaseClientBuilderClass.java
@@ -365,15 +365,13 @@ private MethodSpec finalizeServiceConfigurationMethod() {
private void mergeServiceConfiguration(MethodSpec.Builder builder, String clientConfigClassName) {
ClassName clientConfigClass = ClassName.bestGuess(clientConfigClassName);
- builder.addCode("$1T.Builder serviceConfigBuilder = (($1T) config.option($2T.SERVICE_CONFIGURATION)).toBuilder();" +
- "serviceConfigBuilder.profileFile(serviceConfigBuilder.profileFile() "
- + "!= null ? serviceConfigBuilder.profileFile() : config.option($2T.PROFILE_FILE));" +
- "serviceConfigBuilder.profileName(serviceConfigBuilder.profileName() "
+ builder.addCode("$1T.Builder serviceConfigBuilder = (($1T) config.option($2T.SERVICE_CONFIGURATION)).toBuilder();"
+ + "serviceConfigBuilder.profileFile(serviceConfigBuilder.profileFileSupplier() != null ? "
+ + "serviceConfigBuilder.profileFileSupplier() : config.option($2T.PROFILE_FILE_SUPPLIER));"
+ + "serviceConfigBuilder.profileName(serviceConfigBuilder.profileName() "
+ "!= null ? serviceConfigBuilder.profileName() : config.option($2T.PROFILE_NAME));",
clientConfigClass, SdkClientOption.class);
-
-
if (model.getCustomizationConfig().getServiceConfig().hasDualstackProperty()) {
builder.addCode("if (serviceConfigBuilder.dualstackEnabled() != null) {")
.addCode(" $T.validState(config.option($T.DUALSTACK_ENDPOINT_ENABLED) == null, \"Dualstack has been "
diff --git a/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/builder/test-client-builder-class.java b/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/builder/test-client-builder-class.java
index face91772800..7d0152632f34 100644
--- a/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/builder/test-client-builder-class.java
+++ b/codegen/src/test/resources/software/amazon/awssdk/codegen/poet/builder/test-client-builder-class.java
@@ -60,8 +60,9 @@ protected final SdkClientConfiguration finalizeServiceConfiguration(SdkClientCon
interceptors = CollectionUtils.mergeLists(interceptors, config.option(SdkClientOption.EXECUTION_INTERCEPTORS));
ServiceConfiguration.Builder serviceConfigBuilder = ((ServiceConfiguration) config
.option(SdkClientOption.SERVICE_CONFIGURATION)).toBuilder();
- serviceConfigBuilder.profileFile(serviceConfigBuilder.profileFile() != null ? serviceConfigBuilder.profileFile() : config
- .option(SdkClientOption.PROFILE_FILE));
+ serviceConfigBuilder.profileFile(serviceConfigBuilder.profileFileSupplier() != null ?
+ serviceConfigBuilder.profileFileSupplier() :
+ config.option(SdkClientOption.PROFILE_FILE_SUPPLIER));
serviceConfigBuilder.profileName(serviceConfigBuilder.profileName() != null ? serviceConfigBuilder.profileName() : config
.option(SdkClientOption.PROFILE_NAME));
if (serviceConfigBuilder.dualstackEnabled() != null) {
diff --git a/core/auth/pom.xml b/core/auth/pom.xml
index 8273a9bf5ac7..e5e15fdee807 100644
--- a/core/auth/pom.xml
+++ b/core/auth/pom.xml
@@ -152,6 +152,12 @@
commons-lang3test
+
+ com.google.jimfs
+ jimfs
+ ${jimfs.version}
+ test
+
diff --git a/core/auth/src/main/java/software/amazon/awssdk/auth/credentials/AwsCredentialsProvider.java b/core/auth/src/main/java/software/amazon/awssdk/auth/credentials/AwsCredentialsProvider.java
index 0e8ddb4aad3b..3c797cf9905c 100644
--- a/core/auth/src/main/java/software/amazon/awssdk/auth/credentials/AwsCredentialsProvider.java
+++ b/core/auth/src/main/java/software/amazon/awssdk/auth/credentials/AwsCredentialsProvider.java
@@ -30,7 +30,7 @@
public interface AwsCredentialsProvider {
/**
* Returns {@link AwsCredentials} that can be used to authorize an AWS request. Each implementation of AWSCredentialsProvider
- * can chose its own strategy for loading credentials. For example, an implementation might load credentials from an existing
+ * can choose its own strategy for loading credentials. For example, an implementation might load credentials from an existing
* key management system, or load new credentials when credentials are rotated.
*
*
If an error occurs during the loading of credentials or credentials could not be found, a runtime exception will be
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 2486723c3b86..248a130c5864 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
@@ -15,9 +15,12 @@
package software.amazon.awssdk.auth.credentials;
+import java.util.Optional;
+import java.util.function.Supplier;
import software.amazon.awssdk.annotations.SdkPublicApi;
import software.amazon.awssdk.auth.credentials.internal.LazyAwsCredentialsProvider;
import software.amazon.awssdk.profiles.ProfileFile;
+import software.amazon.awssdk.profiles.ProfileFileSupplier;
import software.amazon.awssdk.utils.SdkAutoCloseable;
import software.amazon.awssdk.utils.ToString;
import software.amazon.awssdk.utils.builder.CopyableBuilder;
@@ -26,8 +29,8 @@
/**
* AWS credentials provider chain that looks for credentials in this order:
*
- *
Java System Properties - aws.accessKeyId and aws.secretAccessKey
- *
Environment Variables - AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY
+ *
Java System Properties - {@code aws.accessKeyId} and {@code aws.secretAccessKey}
+ *
Environment Variables - {@code AWS_ACCESS_KEY_ID} and {@code AWS_SECRET_ACCESS_KEY}
*
Web Identity Token credentials from system properties or environment variables
*
Credential profiles file at the default location (~/.aws/credentials) shared by all AWS SDKs and the AWS CLI
*
Credentials delivered through the Amazon EC2 container service if AWS_CONTAINER_CREDENTIALS_RELATIVE_URI" environment
@@ -51,7 +54,7 @@ public final class DefaultCredentialsProvider
private final LazyAwsCredentialsProvider providerChain;
- private final ProfileFile profileFile;
+ private final Supplier profileFile;
private final String profileName;
@@ -87,21 +90,21 @@ private static LazyAwsCredentialsProvider createChain(Builder builder) {
return LazyAwsCredentialsProvider.create(() -> {
AwsCredentialsProvider[] credentialsProviders = new AwsCredentialsProvider[] {
- SystemPropertyCredentialsProvider.create(),
- EnvironmentVariableCredentialsProvider.create(),
- WebIdentityTokenFileCredentialsProvider.create(),
- ProfileCredentialsProvider.builder()
- .profileFile(builder.profileFile)
- .profileName(builder.profileName)
- .build(),
- ContainerCredentialsProvider.builder()
- .asyncCredentialUpdateEnabled(asyncCredentialUpdateEnabled)
- .build(),
- InstanceProfileCredentialsProvider.builder()
- .asyncCredentialUpdateEnabled(asyncCredentialUpdateEnabled)
- .profileFile(builder.profileFile)
- .profileName(builder.profileName)
- .build()
+ SystemPropertyCredentialsProvider.create(),
+ EnvironmentVariableCredentialsProvider.create(),
+ WebIdentityTokenFileCredentialsProvider.create(),
+ ProfileCredentialsProvider.builder()
+ .profileFile(builder.profileFile)
+ .profileName(builder.profileName)
+ .build(),
+ ContainerCredentialsProvider.builder()
+ .asyncCredentialUpdateEnabled(asyncCredentialUpdateEnabled)
+ .build(),
+ InstanceProfileCredentialsProvider.builder()
+ .asyncCredentialUpdateEnabled(asyncCredentialUpdateEnabled)
+ .profileFile(builder.profileFile)
+ .profileName(builder.profileName)
+ .build()
};
return AwsCredentialsProviderChain.builder()
@@ -144,7 +147,7 @@ public Builder toBuilder() {
* Configuration that defines the {@link DefaultCredentialsProvider}'s behavior.
*/
public static final class Builder implements CopyableBuilder {
- private ProfileFile profileFile;
+ private Supplier profileFile;
private String profileName;
private Boolean reuseLastProviderEnabled = true;
private Boolean asyncCredentialUpdateEnabled = false;
@@ -163,7 +166,13 @@ private Builder(DefaultCredentialsProvider credentialsProvider) {
}
public Builder profileFile(ProfileFile profileFile) {
- this.profileFile = profileFile;
+ return profileFile(Optional.ofNullable(profileFile)
+ .map(ProfileFileSupplier::fixedProfileFile)
+ .orElse(null));
+ }
+
+ public Builder profileFile(Supplier profileFileSupplier) {
+ this.profileFile = profileFileSupplier;
return this;
}
@@ -198,6 +207,7 @@ public Builder asyncCredentialUpdateEnabled(Boolean asyncCredentialUpdateEnabled
/**
* Create a {@link DefaultCredentialsProvider} using the configuration defined in this builder.
*/
+ @Override
public DefaultCredentialsProvider build() {
return new DefaultCredentialsProvider(this);
}
diff --git a/core/auth/src/main/java/software/amazon/awssdk/auth/credentials/InstanceProfileCredentialsProvider.java b/core/auth/src/main/java/software/amazon/awssdk/auth/credentials/InstanceProfileCredentialsProvider.java
index 04abbbc56569..3fd3e7368c19 100644
--- a/core/auth/src/main/java/software/amazon/awssdk/auth/credentials/InstanceProfileCredentialsProvider.java
+++ b/core/auth/src/main/java/software/amazon/awssdk/auth/credentials/InstanceProfileCredentialsProvider.java
@@ -26,6 +26,8 @@
import java.time.Instant;
import java.util.Collections;
import java.util.Map;
+import java.util.Optional;
+import java.util.function.Supplier;
import software.amazon.awssdk.annotations.SdkPublicApi;
import software.amazon.awssdk.annotations.SdkTestInternalApi;
import software.amazon.awssdk.auth.credentials.internal.Ec2MetadataConfigProvider;
@@ -36,6 +38,7 @@
import software.amazon.awssdk.core.exception.SdkClientException;
import software.amazon.awssdk.core.exception.SdkServiceException;
import software.amazon.awssdk.profiles.ProfileFile;
+import software.amazon.awssdk.profiles.ProfileFileSupplier;
import software.amazon.awssdk.profiles.ProfileFileSystemSetting;
import software.amazon.awssdk.regions.util.HttpResourcesUtils;
import software.amazon.awssdk.regions.util.ResourcesEndpointProvider;
@@ -77,7 +80,7 @@ public final class InstanceProfileCredentialsProvider
private final String asyncThreadName;
- private final ProfileFile profileFile;
+ private final Supplier profileFile;
private final String profileName;
@@ -95,8 +98,8 @@ private InstanceProfileCredentialsProvider(BuilderImpl builder) {
this.httpCredentialsLoader = HttpCredentialsLoader.create();
this.configProvider =
Ec2MetadataConfigProvider.builder()
- .profileFile(builder.profileFile == null ? null : () -> builder.profileFile)
- .profileName(builder.profileName == null ? null : builder.profileName)
+ .profileFile(builder.profileFile)
+ .profileName(builder.profileName)
.build();
if (Boolean.TRUE.equals(builder.asyncCredentialUpdateEnabled)) {
@@ -279,9 +282,19 @@ public interface Builder extends HttpCredentialsProvider.BuilderBy default, {@link ProfileFile#defaultProfileFile()} is used.
+ *
+ * @see #profileFile(Supplier)
*/
Builder profileFile(ProfileFile profileFile);
+ /**
+ * Define the mechanism for loading profile files.
+ *
+ * @param profileFileSupplier Supplier interface for generating a ProfileFile instance.
+ * @see #profileFile(ProfileFile)
+ */
+ Builder profileFile(Supplier profileFileSupplier);
+
/**
* Configure the profile name used for loading IMDS-related configuration, like the endpoint mode (IPv4 vs IPv6).
*
@@ -292,6 +305,7 @@ public interface Builder extends HttpCredentialsProvider.Builder profileFile;
private String profileName;
private BuilderImpl() {
@@ -354,14 +368,25 @@ public void setAsyncThreadName(String asyncThreadName) {
@Override
public Builder profileFile(ProfileFile profileFile) {
- this.profileFile = profileFile;
- return this;
+ return profileFile(Optional.ofNullable(profileFile)
+ .map(ProfileFileSupplier::fixedProfileFile)
+ .orElse(null));
}
public void setProfileFile(ProfileFile profileFile) {
profileFile(profileFile);
}
+ @Override
+ public Builder profileFile(Supplier profileFileSupplier) {
+ this.profileFile = profileFileSupplier;
+ return this;
+ }
+
+ public void setProfileFile(Supplier profileFileSupplier) {
+ profileFile(profileFileSupplier);
+ }
+
@Override
public Builder profileName(String profileName) {
this.profileName = profileName;
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 37ccb8a636bf..c35bc5ed40c7 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
@@ -15,6 +15,7 @@
package software.amazon.awssdk.auth.credentials;
+import java.util.Objects;
import java.util.Optional;
import java.util.function.Consumer;
import java.util.function.Supplier;
@@ -23,6 +24,7 @@
import software.amazon.awssdk.auth.credentials.internal.ProfileCredentialsUtils;
import software.amazon.awssdk.core.exception.SdkClientException;
import software.amazon.awssdk.profiles.ProfileFile;
+import software.amazon.awssdk.profiles.ProfileFileSupplier;
import software.amazon.awssdk.profiles.ProfileFileSystemSetting;
import software.amazon.awssdk.utils.IoUtils;
import software.amazon.awssdk.utils.SdkAutoCloseable;
@@ -46,55 +48,41 @@ public final class ProfileCredentialsProvider
implements AwsCredentialsProvider,
SdkAutoCloseable,
ToCopyableBuilder {
- private final AwsCredentialsProvider credentialsProvider;
- private final RuntimeException loadException;
- private final ProfileFile profileFile;
+ private volatile AwsCredentialsProvider credentialsProvider;
+ private final RuntimeException loadException;
+ private final Supplier profileFile;
+ private volatile ProfileFile currentProfileFile;
private final String profileName;
-
private final Supplier defaultProfileFileLoader;
/**
* @see #builder()
*/
private ProfileCredentialsProvider(BuilderImpl builder) {
- AwsCredentialsProvider credentialsProvider = null;
- RuntimeException loadException = null;
- ProfileFile profileFile = null;
- String profileName = null;
+ this.defaultProfileFileLoader = builder.defaultProfileFileLoader;
+
+ RuntimeException thrownException = null;
+ String selectedProfileName = null;
+ Supplier selectedProfileSupplier = 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(finalProfileFile, 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();
- });
+ selectedProfileName = Optional.ofNullable(builder.profileName)
+ .orElseGet(ProfileFileSystemSetting.AWS_PROFILE::getStringValueOrThrow);
+
+ selectedProfileSupplier = Optional.ofNullable(builder.profileFile)
+ .orElseGet(() -> builder.defaultProfileFileLoader);
+
} 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
+ // will only be raised on calls to resolveCredentials. We don't want to raise an exception here because it may be
// expected (eg. in the default credential chain).
- loadException = e;
+ thrownException = e;
}
- this.loadException = loadException;
- this.credentialsProvider = credentialsProvider;
- this.profileFile = profileFile;
- this.profileName = profileName;
- this.defaultProfileFileLoader = builder.defaultProfileFileLoader;
+ this.loadException = thrownException;
+ this.profileName = selectedProfileName;
+ this.profileFile = selectedProfileSupplier;
}
/**
@@ -127,14 +115,37 @@ public AwsCredentials resolveCredentials() {
if (loadException != null) {
throw loadException;
}
+
+ ProfileFile cachedOrRefreshedProfileFile = refreshProfileFile();
+ if (isNewProfileFile(cachedOrRefreshedProfileFile)) {
+ synchronized (this) {
+ if (isNewProfileFile(cachedOrRefreshedProfileFile)) {
+ currentProfileFile = cachedOrRefreshedProfileFile;
+ handleProfileFileReload(cachedOrRefreshedProfileFile);
+ }
+ }
+ }
+
return credentialsProvider.resolveCredentials();
}
+ private void handleProfileFileReload(ProfileFile profileFile) {
+ credentialsProvider = createCredentialsProvider(profileFile, profileName);
+ }
+
+ private ProfileFile refreshProfileFile() {
+ return profileFile.get();
+ }
+
+ private boolean isNewProfileFile(ProfileFile profileFile) {
+ return !Objects.equals(currentProfileFile, profileFile);
+ }
+
@Override
public String toString() {
return ToString.builder("ProfileCredentialsProvider")
.add("profileName", profileName)
- .add("profileFile", profileFile)
+ .add("profileFile", currentProfileFile)
.build();
}
@@ -150,6 +161,17 @@ public Builder toBuilder() {
return new BuilderImpl(this);
}
+ private AwsCredentialsProvider createCredentialsProvider(ProfileFile profileFile, String profileName) {
+ // Load the profile and credentials provider
+ return profileFile.profile(profileName)
+ .flatMap(p -> new ProfileCredentialsUtils(profileFile, p, profileFile::profile).credentialsProvider())
+ .orElseThrow(() -> {
+ String errorMessage = String.format("Profile file contained no credentials for " +
+ "profile '%s': %s", profileName, profileFile);
+ return SdkClientException.builder().message(errorMessage).build();
+ });
+ }
+
/**
* A builder for creating a custom {@link ProfileCredentialsProvider}.
*/
@@ -158,6 +180,7 @@ public interface Builder extends CopyableBuilder profileFile);
+ /**
+ * Define the mechanism for loading profile files.
+ *
+ * @param profileFileSupplier Supplier interface for generating a ProfileFile instance.
+ * @see #profileFile(ProfileFile)
+ */
+ Builder profileFile(Supplier profileFileSupplier);
+
/**
* Define the name of the profile that should be used by this credentials provider. By default, the value in
* {@link ProfileFileSystemSetting#AWS_PROFILE} is used.
@@ -176,11 +207,12 @@ public interface Builder extends CopyableBuilder profileFile;
private String profileName;
private Supplier defaultProfileFileLoader = ProfileFile::defaultProfileFile;
@@ -188,15 +220,16 @@ static final class BuilderImpl implements Builder {
}
BuilderImpl(ProfileCredentialsProvider provider) {
- this.profileFile = provider.profileFile;
this.profileName = provider.profileName;
this.defaultProfileFileLoader = provider.defaultProfileFileLoader;
+ this.profileFile = provider.profileFile;
}
@Override
public Builder profileFile(ProfileFile profileFile) {
- this.profileFile = profileFile;
- return this;
+ return profileFile(Optional.ofNullable(profileFile)
+ .map(ProfileFileSupplier::fixedProfileFile)
+ .orElse(null));
}
public void setProfileFile(ProfileFile profileFile) {
@@ -208,6 +241,16 @@ public Builder profileFile(Consumer profileFile) {
return profileFile(ProfileFile.builder().applyMutation(profileFile).build());
}
+ @Override
+ public Builder profileFile(Supplier profileFileSupplier) {
+ this.profileFile = profileFileSupplier;
+ return this;
+ }
+
+ public void setProfileFile(Supplier supplier) {
+ profileFile(supplier);
+ }
+
@Override
public Builder profileName(String profileName) {
this.profileName = profileName;
@@ -225,8 +268,9 @@ public ProfileCredentialsProvider build() {
/**
* Override the default configuration file to be used when the customer does not explicitly set
- * profileName(profileName);
- * {@link #profileFile(ProfileFile)}. Use of this method is only useful for testing the default behavior.
+ * profileFile(ProfileFile) or profileFileSupplier(supplier);
+ * {@link #profileFile(ProfileFile)}. Use of this method is
+ * only useful for testing the default behavior.
*/
@SdkTestInternalApi
Builder defaultProfileFileLoader(Supplier defaultProfileFileLoader) {
@@ -234,4 +278,5 @@ Builder defaultProfileFileLoader(Supplier defaultProfileFileLoader)
return this;
}
}
+
}
diff --git a/core/auth/src/main/java/software/amazon/awssdk/auth/credentials/ProfileProviderCredentialsContext.java b/core/auth/src/main/java/software/amazon/awssdk/auth/credentials/ProfileProviderCredentialsContext.java
index 7f8a8efdb4d2..065f8b61da7c 100644
--- a/core/auth/src/main/java/software/amazon/awssdk/auth/credentials/ProfileProviderCredentialsContext.java
+++ b/core/auth/src/main/java/software/amazon/awssdk/auth/credentials/ProfileProviderCredentialsContext.java
@@ -22,7 +22,7 @@
import software.amazon.awssdk.profiles.ProfileFile;
/**
- * Context class that defines the required properties for creation of a Credentials provider*
+ * Context class that defines the required properties for creation of a Credentials provider.
*/
@SdkProtectedApi
public final class ProfileProviderCredentialsContext {
diff --git a/core/auth/src/main/java/software/amazon/awssdk/auth/token/credentials/ProfileTokenProvider.java b/core/auth/src/main/java/software/amazon/awssdk/auth/token/credentials/ProfileTokenProvider.java
index 698955eb84e5..bd15f28ff2e6 100644
--- a/core/auth/src/main/java/software/amazon/awssdk/auth/token/credentials/ProfileTokenProvider.java
+++ b/core/auth/src/main/java/software/amazon/awssdk/auth/token/credentials/ProfileTokenProvider.java
@@ -40,53 +40,41 @@ public final class ProfileTokenProvider implements SdkTokenProvider, SdkAutoClos
private final SdkTokenProvider tokenProvider;
private final RuntimeException loadException;
- private final ProfileFile profileFile;
private final String profileName;
/**
* @see #builder()
*/
private ProfileTokenProvider(BuilderImpl builder) {
- SdkTokenProvider tokenProvider = null;
- RuntimeException loadException = null;
- ProfileFile profileFile = null;
- String profileName = null;
+ SdkTokenProvider sdkTokenProvider = null;
+ RuntimeException thrownException = null;
+ Supplier selectedProfileFile = null;
+ String selectedProfileName = null;
try {
- profileName = builder.profileName != null ? builder.profileName
- : ProfileFileSystemSetting.AWS_PROFILE.getStringValueOrThrow();
+ selectedProfileName = Optional.ofNullable(builder.profileName)
+ .orElseGet(ProfileFileSystemSetting.AWS_PROFILE::getStringValueOrThrow);
// Load the profiles file
- profileFile = Optional.ofNullable(builder.profileFile)
- .orElse(builder.defaultProfileFileLoader)
- .get();
+ selectedProfileFile = Optional.ofNullable(builder.profileFile)
+ .orElse(builder.defaultProfileFileLoader);
// Load the profile and token provider
- String finalProfileName = profileName;
- ProfileFile finalProfileFile = profileFile;
- tokenProvider =
- profileFile.profile(profileName)
- .flatMap(p -> new ProfileTokenProviderLoader(finalProfileFile, p).tokenProvider())
- .orElseThrow(() -> {
- String errorMessage = String.format("Profile file contained no information for " +
- "profile '%s': %s", finalProfileName, finalProfileFile);
- return SdkClientException.builder().message(errorMessage).build();
- });
+ sdkTokenProvider = createTokenProvider(selectedProfileFile, selectedProfileName);
+
} catch (RuntimeException e) {
// If we couldn't load the provider for some reason, save an exception describing why.
- loadException = e;
+ thrownException = e;
}
- if (loadException != null) {
- this.loadException = loadException;
+ if (thrownException != null) {
+ this.loadException = thrownException;
this.tokenProvider = null;
- this.profileFile = null;
this.profileName = null;
} else {
this.loadException = null;
- this.tokenProvider = tokenProvider;
- this.profileFile = profileFile;
- this.profileName = profileName;
+ this.tokenProvider = sdkTokenProvider;
+ this.profileName = selectedProfileName;
}
}
@@ -127,7 +115,6 @@ public SdkToken resolveToken() {
public String toString() {
return ToString.builder("ProfileTokenProvider")
.add("profileName", profileName)
- .add("profileFile", profileFile)
.build();
}
@@ -137,6 +124,16 @@ public void close() {
IoUtils.closeIfCloseable(tokenProvider, null);
}
+ private SdkTokenProvider createTokenProvider(Supplier profileFile, String profileName) {
+ return new ProfileTokenProviderLoader(profileFile, profileName)
+ .tokenProvider()
+ .orElseThrow(() -> {
+ String errorMessage = String.format("Profile file contained no information for " +
+ "profile '%s'", profileName);
+ return SdkClientException.builder().message(errorMessage).build();
+ });
+ }
+
/**
* A builder for creating a custom {@link ProfileTokenProvider}.
*/
diff --git a/core/auth/src/main/java/software/amazon/awssdk/auth/token/internal/ProfileTokenProviderLoader.java b/core/auth/src/main/java/software/amazon/awssdk/auth/token/internal/ProfileTokenProviderLoader.java
index 87e00046947e..4545477d9fce 100644
--- a/core/auth/src/main/java/software/amazon/awssdk/auth/token/internal/ProfileTokenProviderLoader.java
+++ b/core/auth/src/main/java/software/amazon/awssdk/auth/token/internal/ProfileTokenProviderLoader.java
@@ -17,15 +17,19 @@
import java.lang.reflect.InvocationTargetException;
import java.util.Arrays;
+import java.util.Objects;
import java.util.Optional;
+import java.util.function.Supplier;
import software.amazon.awssdk.annotations.SdkInternalApi;
import software.amazon.awssdk.auth.token.credentials.ChildProfileTokenProviderFactory;
import software.amazon.awssdk.auth.token.credentials.SdkTokenProvider;
+import software.amazon.awssdk.core.exception.SdkClientException;
import software.amazon.awssdk.core.internal.util.ClassLoaderHelper;
import software.amazon.awssdk.profiles.Profile;
import software.amazon.awssdk.profiles.ProfileFile;
import software.amazon.awssdk.profiles.ProfileProperty;
import software.amazon.awssdk.profiles.internal.ProfileSection;
+import software.amazon.awssdk.utils.Lazy;
import software.amazon.awssdk.utils.Validate;
/**
@@ -36,12 +40,17 @@ public final class ProfileTokenProviderLoader {
private static final String SSO_OIDC_TOKEN_PROVIDER_FACTORY =
"software.amazon.awssdk.services.ssooidc.SsoOidcProfileTokenProviderFactory";
- private final Profile profile;
- private final ProfileFile profileFile;
+ private final Supplier profileFileSupplier;
+ private final String profileName;
+ private volatile ProfileFile currentProfileFile;
+ private volatile SdkTokenProvider currentTokenProvider;
- public ProfileTokenProviderLoader(ProfileFile profileFile, Profile profile) {
- this.profile = Validate.paramNotNull(profile, "profile");
- this.profileFile = Validate.paramNotNull(profileFile, "profileFile");
+ private final Lazy factory;
+
+ public ProfileTokenProviderLoader(Supplier profileFile, String profileName) {
+ this.profileFileSupplier = Validate.paramNotNull(profileFile, "profileFile");
+ this.profileName = Validate.paramNotNull(profileName, "profileName");
+ this.factory = new Lazy<>(this::ssoTokenProviderFactory);
}
/**
@@ -55,19 +64,53 @@ public Optional tokenProvider() {
* Create the SSO credentials provider based on the related profile properties.
*/
private SdkTokenProvider ssoProfileCredentialsProvider() {
+ return () -> ssoProfileCredentialsProvider(profileFileSupplier, profileName).resolveToken();
+ }
+
+ private SdkTokenProvider ssoProfileCredentialsProvider(ProfileFile profileFile, Profile profile) {
+ String profileSsoSectionName = profileSsoSectionName(profile);
+ Profile ssoProfile = ssoProfile(profileFile, profileSsoSectionName);
+
+ validateRequiredProperties(ssoProfile, ProfileProperty.SSO_REGION, ProfileProperty.SSO_START_URL);
+
+ return factory.getValue().create(profileFile, profile);
+ }
- String profileSsoSectionName = profile.property(ProfileSection.SSO_SESSION.getPropertyKeyName())
- .orElseThrow(() -> new IllegalArgumentException(
- "Profile " + profile.name() + " does not have sso_session property"));
+ private SdkTokenProvider ssoProfileCredentialsProvider(Supplier profileFile, String profileName) {
+ ProfileFile profileFileInstance = profileFile.get();
+ if (!Objects.equals(profileFileInstance, currentProfileFile)) {
+ synchronized (this) {
+ if (!Objects.equals(profileFileInstance, currentProfileFile)) {
+ Profile profileInstance = resolveProfile(profileFileInstance, profileName);
+ currentProfileFile = profileFileInstance;
+ currentTokenProvider = ssoProfileCredentialsProvider(profileFileInstance, profileInstance);
+ }
+ }
+ }
+
+ return currentTokenProvider;
+ }
- Profile ssoProfile = profileFile.getSection(ProfileSection.SSO_SESSION.getSectionTitle(), profileSsoSectionName)
- .orElseThrow(() -> new IllegalArgumentException(
- "Sso-session section not found with sso-session title " + profileSsoSectionName));
+ private Profile resolveProfile(ProfileFile profileFile, String profileName) {
+ return profileFile.profile(profileName)
+ .orElseThrow(() -> {
+ String errorMessage = String.format("Profile file contained no information for profile '%s': %s",
+ profileName, profileFile);
+ return SdkClientException.builder().message(errorMessage).build();
+ });
+ }
+
+ private String profileSsoSectionName(Profile profile) {
+ return Optional.ofNullable(profile)
+ .flatMap(p -> p.property(ProfileSection.SSO_SESSION.getPropertyKeyName()))
+ .orElseThrow(() -> new IllegalArgumentException(
+ "Profile " + profileName + " does not have sso_session property"));
+ }
- validateRequiredProperties(ssoProfile,
- ProfileProperty.SSO_REGION,
- ProfileProperty.SSO_START_URL);
- return ssoTokenProviderFactory().create(profileFile, profile);
+ private Profile ssoProfile(ProfileFile profileFile, String profileSsoSectionName) {
+ return profileFile.getSection(ProfileSection.SSO_SESSION.getSectionTitle(), profileSsoSectionName)
+ .orElseThrow(() -> new IllegalArgumentException(
+ "Sso-session section not found with sso-session title " + profileSsoSectionName));
}
/**
@@ -76,7 +119,8 @@ private SdkTokenProvider ssoProfileCredentialsProvider() {
private void validateRequiredProperties(Profile ssoProfile, String... requiredProperties) {
Arrays.stream(requiredProperties)
.forEach(p -> Validate.isTrue(ssoProfile.properties().containsKey(p),
- "Property '%s' was not configured for profile '%s'.", p, this.profile.name()));
+ "Property '%s' was not configured for profile '%s'.",
+ p, profileName));
}
/**
@@ -88,10 +132,10 @@ private ChildProfileTokenProviderFactory ssoTokenProviderFactory() {
getClass());
return (ChildProfileTokenProviderFactory) ssoOidcTokenProviderFactory.getConstructor().newInstance();
} catch (ClassNotFoundException e) {
- throw new IllegalStateException("To use SSO OIDC related properties in the '" + profile.name() + "' profile, "
+ throw new IllegalStateException("To use SSO OIDC related properties in the '" + profileName + "' profile, "
+ "the 'ssooidc' service module must be on the class path.", e);
} catch (NoSuchMethodException | InvocationTargetException | InstantiationException | IllegalAccessException e) {
- throw new IllegalStateException("Failed to create the '" + profile.name() + "' token provider factory.", e);
+ throw new IllegalStateException("Failed to create the '%s" + profileName + "' token provider factory.", e);
}
}
}
diff --git a/core/auth/src/test/java/software/amazon/awssdk/auth/credentials/DefaultCredentialsProviderTest.java b/core/auth/src/test/java/software/amazon/awssdk/auth/credentials/DefaultCredentialsProviderTest.java
new file mode 100644
index 000000000000..1c1c44bd140c
--- /dev/null
+++ b/core/auth/src/test/java/software/amazon/awssdk/auth/credentials/DefaultCredentialsProviderTest.java
@@ -0,0 +1,117 @@
+/*
+ * 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.auth.credentials;
+
+import static org.assertj.core.api.AssertionsForClassTypes.assertThat;
+
+import java.util.Arrays;
+import java.util.List;
+import java.util.function.Supplier;
+import org.junit.jupiter.api.Test;
+import software.amazon.awssdk.profiles.ProfileFile;
+import software.amazon.awssdk.profiles.ProfileFileSupplier;
+import software.amazon.awssdk.utils.StringInputStream;
+
+class DefaultCredentialsProviderTest {
+
+ @Test
+ void resolveCredentials_ProfileCredentialsProviderWithProfileFile_returnsCredentials() {
+ DefaultCredentialsProvider provider = DefaultCredentialsProvider
+ .builder()
+ .profileFile(credentialFile("test", "access", "secret"))
+ .profileName("test")
+ .build();
+
+ assertThat(provider.resolveCredentials()).satisfies(awsCredentials -> {
+ assertThat(awsCredentials.accessKeyId()).isEqualTo("access");
+ assertThat(awsCredentials.secretAccessKey()).isEqualTo("secret");
+ });
+ }
+
+ @Test
+ void resolveCredentials_ProfileCredentialsProviderWithProfileFileSupplier_resolvesCredentialsPerCall() {
+ List profileFileList = Arrays.asList(credentialFile("test", "access", "secret"),
+ credentialFile("test", "modified", "update"));
+ ProfileFileSupplier profileFileSupplier = supply(profileFileList);
+
+ DefaultCredentialsProvider provider = DefaultCredentialsProvider
+ .builder()
+ .profileFile(profileFileSupplier)
+ .profileName("test")
+ .build();
+
+ assertThat(provider.resolveCredentials()).satisfies(awsCredentials -> {
+ assertThat(awsCredentials.accessKeyId()).isEqualTo("access");
+ assertThat(awsCredentials.secretAccessKey()).isEqualTo("secret");
+ });
+
+ assertThat(provider.resolveCredentials()).satisfies(awsCredentials -> {
+ assertThat(awsCredentials.accessKeyId()).isEqualTo("modified");
+ assertThat(awsCredentials.secretAccessKey()).isEqualTo("update");
+ });
+ }
+
+ @Test
+ void resolveCredentials_ProfileCredentialsProviderWithProfileFileSupplier_returnsCredentials() {
+ ProfileFile profileFile = credentialFile("test", "access", "secret");
+ ProfileFileSupplier profileFileSupplier = ProfileFileSupplier.fixedProfileFile(profileFile);
+
+ DefaultCredentialsProvider provider = DefaultCredentialsProvider
+ .builder()
+ .profileFile(profileFileSupplier)
+ .profileName("test")
+ .build();
+
+ assertThat(provider.resolveCredentials()).satisfies(awsCredentials -> {
+ assertThat(awsCredentials.accessKeyId()).isEqualTo("access");
+ assertThat(awsCredentials.secretAccessKey()).isEqualTo("secret");
+ });
+ }
+
+ @Test
+ void resolveCredentials_ProfileCredentialsProviderWithSupplierProfileFile_returnsCredentials() {
+ Supplier supplier = () -> credentialFile("test", "access", "secret");
+
+ DefaultCredentialsProvider provider = DefaultCredentialsProvider
+ .builder()
+ .profileFile(supplier)
+ .profileName("test")
+ .build();
+
+ assertThat(provider.resolveCredentials()).satisfies(awsCredentials -> {
+ assertThat(awsCredentials.accessKeyId()).isEqualTo("access");
+ assertThat(awsCredentials.secretAccessKey()).isEqualTo("secret");
+ });
+ }
+
+ private ProfileFile credentialFile(String credentialFile) {
+ return ProfileFile.builder()
+ .content(new StringInputStream(credentialFile))
+ .type(ProfileFile.Type.CREDENTIALS)
+ .build();
+ }
+
+ private ProfileFile credentialFile(String name, String accessKeyId, String secretAccessKey) {
+ String contents = String.format("[%s]\naws_access_key_id = %s\naws_secret_access_key = %s\n",
+ name, accessKeyId, secretAccessKey);
+ return credentialFile(contents);
+ }
+
+ private static ProfileFileSupplier supply(Iterable iterable) {
+ return iterable.iterator()::next;
+ }
+
+}
diff --git a/core/auth/src/test/java/software/amazon/awssdk/auth/credentials/InstanceProfileCredentialsProviderTest.java b/core/auth/src/test/java/software/amazon/awssdk/auth/credentials/InstanceProfileCredentialsProviderTest.java
index 3a9d3ea4c505..0f32ed3c17bd 100644
--- a/core/auth/src/test/java/software/amazon/awssdk/auth/credentials/InstanceProfileCredentialsProviderTest.java
+++ b/core/auth/src/test/java/software/amazon/awssdk/auth/credentials/InstanceProfileCredentialsProviderTest.java
@@ -41,7 +41,10 @@
import java.time.Instant;
import java.time.ZoneId;
import java.time.ZoneOffset;
-import java.time.temporal.ChronoUnit;
+import java.util.Arrays;
+import java.util.List;
+import java.util.function.Supplier;
+import java.util.stream.Collectors;
import org.junit.AfterClass;
import org.junit.Before;
import org.junit.Rule;
@@ -50,7 +53,12 @@
import software.amazon.awssdk.core.SdkSystemSetting;
import software.amazon.awssdk.core.exception.SdkClientException;
import software.amazon.awssdk.core.util.SdkUserAgent;
+import software.amazon.awssdk.profiles.ProfileFile;
+import software.amazon.awssdk.profiles.ProfileFileSupplier;
+import software.amazon.awssdk.profiles.ProfileProperty;
import software.amazon.awssdk.utils.DateUtils;
+import software.amazon.awssdk.utils.Pair;
+import software.amazon.awssdk.utils.StringInputStream;
public class InstanceProfileCredentialsProviderTest {
private static final String TOKEN_RESOURCE_PATH = "/latest/api/token";
@@ -255,6 +263,90 @@ public void resolveCredentials_customProfileFileAndName_usesCorrectEndpoint() {
}
}
+ @Test
+ public void resolveCredentials_customProfileFileSupplierAndNameSettingEndpointOverride_usesCorrectEndpointFromSupplier() {
+ System.clearProperty(SdkSystemSetting.AWS_EC2_METADATA_SERVICE_ENDPOINT.property());
+ WireMockServer mockMetadataEndpoint_2 = new WireMockServer(WireMockConfiguration.options().dynamicPort());
+ mockMetadataEndpoint_2.start();
+ try {
+ String stubToken = "some-token";
+ mockMetadataEndpoint_2.stubFor(put(urlPathEqualTo(TOKEN_RESOURCE_PATH)).willReturn(aResponse().withBody(stubToken)));
+ mockMetadataEndpoint_2.stubFor(get(urlPathEqualTo(CREDENTIALS_RESOURCE_PATH)).willReturn(aResponse().withBody("some-profile")));
+ mockMetadataEndpoint_2.stubFor(get(urlPathEqualTo(CREDENTIALS_RESOURCE_PATH + "some-profile")).willReturn(aResponse().withBody(STUB_CREDENTIALS)));
+
+ String mockServer2Endpoint = "http://localhost:" + mockMetadataEndpoint_2.port();
+
+ ProfileFile config = configFile("profile test",
+ Pair.of(ProfileProperty.EC2_METADATA_SERVICE_ENDPOINT, mockServer2Endpoint));
+
+ List profileFileList = Arrays.asList(credentialFile("test", "key1", "secret1"),
+ credentialFile("test", "key2", "secret2"),
+ credentialFile("test", "key3", "secret3"));
+
+ InstanceProfileCredentialsProvider provider = InstanceProfileCredentialsProvider
+ .builder()
+ .profileFile(ProfileFileSupplier.aggregate(supply(profileFileList), () -> config))
+ .profileName("test")
+ .build();
+
+ AwsCredentials awsCredentials1 = provider.resolveCredentials();
+
+ assertThat(awsCredentials1).isNotNull();
+
+ String userAgentHeader = "User-Agent";
+ String userAgent = SdkUserAgent.create().userAgent();
+ mockMetadataEndpoint_2.verify(putRequestedFor(urlPathEqualTo(TOKEN_RESOURCE_PATH)).withHeader(userAgentHeader, equalTo(userAgent)));
+ mockMetadataEndpoint_2.verify(getRequestedFor(urlPathEqualTo(CREDENTIALS_RESOURCE_PATH)).withHeader(userAgentHeader, equalTo(userAgent)));
+ mockMetadataEndpoint_2.verify(getRequestedFor(urlPathEqualTo(CREDENTIALS_RESOURCE_PATH + "some-profile")).withHeader(userAgentHeader, equalTo(userAgent)));
+
+ // all requests should have gone to the second server, and none to the other one
+ mockMetadataEndpoint.verify(0, RequestPatternBuilder.allRequests());
+ } finally {
+ mockMetadataEndpoint_2.stop();
+ }
+ }
+
+ @Test
+ public void resolveCredentials_customSupplierProfileFileAndNameSettingEndpointOverride_usesCorrectEndpointFromSupplier() {
+ System.clearProperty(SdkSystemSetting.AWS_EC2_METADATA_SERVICE_ENDPOINT.property());
+ WireMockServer mockMetadataEndpoint_2 = new WireMockServer(WireMockConfiguration.options().dynamicPort());
+ mockMetadataEndpoint_2.start();
+ try {
+ String stubToken = "some-token";
+ mockMetadataEndpoint_2.stubFor(put(urlPathEqualTo(TOKEN_RESOURCE_PATH)).willReturn(aResponse().withBody(stubToken)));
+ mockMetadataEndpoint_2.stubFor(get(urlPathEqualTo(CREDENTIALS_RESOURCE_PATH)).willReturn(aResponse().withBody("some-profile")));
+ mockMetadataEndpoint_2.stubFor(get(urlPathEqualTo(CREDENTIALS_RESOURCE_PATH + "some-profile")).willReturn(aResponse().withBody(STUB_CREDENTIALS)));
+
+ String mockServer2Endpoint = "http://localhost:" + mockMetadataEndpoint_2.port();
+
+ ProfileFile config = configFile("profile test",
+ Pair.of(ProfileProperty.EC2_METADATA_SERVICE_ENDPOINT, mockServer2Endpoint));
+
+ Supplier supplier = () -> config;
+
+ InstanceProfileCredentialsProvider provider = InstanceProfileCredentialsProvider
+ .builder()
+ .profileFile(supplier)
+ .profileName("test")
+ .build();
+
+ AwsCredentials awsCredentials1 = provider.resolveCredentials();
+
+ assertThat(awsCredentials1).isNotNull();
+
+ String userAgentHeader = "User-Agent";
+ String userAgent = SdkUserAgent.create().userAgent();
+ mockMetadataEndpoint_2.verify(putRequestedFor(urlPathEqualTo(TOKEN_RESOURCE_PATH)).withHeader(userAgentHeader, equalTo(userAgent)));
+ mockMetadataEndpoint_2.verify(getRequestedFor(urlPathEqualTo(CREDENTIALS_RESOURCE_PATH)).withHeader(userAgentHeader, equalTo(userAgent)));
+ mockMetadataEndpoint_2.verify(getRequestedFor(urlPathEqualTo(CREDENTIALS_RESOURCE_PATH + "some-profile")).withHeader(userAgentHeader, equalTo(userAgent)));
+
+ // all requests should have gone to the second server, and none to the other one
+ mockMetadataEndpoint.verify(0, RequestPatternBuilder.allRequests());
+ } finally {
+ mockMetadataEndpoint_2.stop();
+ }
+ }
+
@Test
public void resolveCredentials_doesNotFailIfImdsReturnsExpiredCredentials() {
String credentialsResponse =
@@ -448,4 +540,38 @@ public Instant instant() {
return time;
}
}
+
+ private static ProfileFileSupplier supply(Iterable iterable) {
+ return iterable.iterator()::next;
+ }
+
+ private ProfileFile credentialFile(String credentialFile) {
+ return ProfileFile.builder()
+ .content(new StringInputStream(credentialFile))
+ .type(ProfileFile.Type.CREDENTIALS)
+ .build();
+ }
+
+ private ProfileFile configFile(String credentialFile) {
+ return ProfileFile.builder()
+ .content(new StringInputStream(credentialFile))
+ .type(ProfileFile.Type.CONFIGURATION)
+ .build();
+ }
+
+ private ProfileFile credentialFile(String name, String accessKeyId, String secretAccessKey) {
+ String contents = String.format("[%s]\naws_access_key_id = %s\naws_secret_access_key = %s\n",
+ name, accessKeyId, secretAccessKey);
+ return credentialFile(contents);
+ }
+
+ private ProfileFile configFile(String name, Pair, ?>... pairs) {
+ String values = Arrays.stream(pairs)
+ .map(pair -> String.format("%s=%s", pair.left(), pair.right()))
+ .collect(Collectors.joining(System.lineSeparator()));
+ String contents = String.format("[%s]\n%s", name, values);
+
+ return configFile(contents);
+ }
+
}
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 d6f261779206..17b9b24ae092 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
@@ -18,9 +18,19 @@
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
+import com.google.common.jimfs.Jimfs;
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.FileSystem;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.function.Supplier;
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import software.amazon.awssdk.core.exception.SdkClientException;
import software.amazon.awssdk.profiles.ProfileFile;
+import software.amazon.awssdk.profiles.ProfileFileSupplier;
import software.amazon.awssdk.profiles.ProfileProperty;
import software.amazon.awssdk.utils.StringInputStream;
@@ -28,18 +38,37 @@
* Verify functionality of {@link ProfileCredentialsProvider}.
*/
public class ProfileCredentialsProviderTest {
+
+ private static FileSystem jimfs;
+ private static Path testDirectory;
+
+ @BeforeAll
+ public static void setup() {
+ jimfs = Jimfs.newFileSystem();
+ testDirectory = jimfs.getPath("test");
+ }
+
+ @AfterAll
+ public static void tearDown() {
+ try {
+ jimfs.close();
+ } catch (IOException e) {
+ // no-op
+ }
+ }
+
@Test
- public void missingCredentialsFileThrowsExceptionInGetCredentials() {
+ void missingCredentialsFileThrowsExceptionInResolveCredentials() {
ProfileCredentialsProvider provider =
- new ProfileCredentialsProvider.BuilderImpl()
- .defaultProfileFileLoader(() -> { throw new IllegalStateException(); })
- .build();
+ new ProfileCredentialsProvider.BuilderImpl()
+ .defaultProfileFileLoader(() -> { throw new IllegalStateException(); })
+ .build();
assertThatThrownBy(provider::resolveCredentials).isInstanceOf(IllegalStateException.class);
}
@Test
- public void missingProfileFileThrowsExceptionInGetCredentials() {
+ void missingProfileFileThrowsExceptionInResolveCredentials() {
ProfileCredentialsProvider provider =
new ProfileCredentialsProvider.BuilderImpl()
.defaultProfileFileLoader(() -> ProfileFile.builder()
@@ -52,35 +81,35 @@ public void missingProfileFileThrowsExceptionInGetCredentials() {
}
@Test
- public void missingProfileThrowsExceptionInGetCredentials() {
+ void missingProfileThrowsExceptionInResolveCredentials() {
ProfileFile file = profileFile("[default]\n"
- + "aws_access_key_id = defaultAccessKey\n"
- + "aws_secret_access_key = defaultSecretAccessKey");
+ + "aws_access_key_id = defaultAccessKey\n"
+ + "aws_secret_access_key = defaultSecretAccessKey");
ProfileCredentialsProvider provider =
- ProfileCredentialsProvider.builder().profileFile(file).profileName("foo").build();
+ ProfileCredentialsProvider.builder().profileFile(file).profileName("foo").build();
assertThatThrownBy(provider::resolveCredentials).isInstanceOf(SdkClientException.class);
}
@Test
- public void profileWithoutCredentialsThrowsExceptionInGetCredentials() {
+ void profileWithoutCredentialsThrowsExceptionInResolveCredentials() {
ProfileFile file = profileFile("[default]");
ProfileCredentialsProvider provider =
- ProfileCredentialsProvider.builder().profileFile(file).profileName("default").build();
+ ProfileCredentialsProvider.builder().profileFile(file).profileName("default").build();
assertThatThrownBy(provider::resolveCredentials).isInstanceOf(SdkClientException.class);
}
@Test
- public void presentProfileReturnsCredentials() {
+ void presentProfileReturnsCredentials() {
ProfileFile file = profileFile("[default]\n"
+ "aws_access_key_id = defaultAccessKey\n"
+ "aws_secret_access_key = defaultSecretAccessKey");
ProfileCredentialsProvider provider =
- ProfileCredentialsProvider.builder().profileFile(file).profileName("default").build();
+ ProfileCredentialsProvider.builder().profileFile(file).profileName("default").build();
assertThat(provider.resolveCredentials()).satisfies(credentials -> {
assertThat(credentials.accessKeyId()).isEqualTo("defaultAccessKey");
@@ -89,7 +118,7 @@ public void presentProfileReturnsCredentials() {
}
@Test
- public void profileWithWebIdentityToken() {
+ void profileWithWebIdentityToken() {
String token = "/User/home/test";
ProfileFile file = profileFile("[default]\n"
@@ -100,7 +129,186 @@ public void profileWithWebIdentityToken() {
assertThat(file.profile("default").get().property(ProfileProperty.WEB_IDENTITY_TOKEN_FILE).get()).isEqualTo(token);
}
+ @Test
+ void resolveCredentials_missingProfileFileCausesExceptionInMethod_throwsException() {
+ ProfileCredentialsProvider.BuilderImpl builder = new ProfileCredentialsProvider.BuilderImpl();
+ builder.defaultProfileFileLoader(() -> ProfileFile.builder()
+ .content(new StringInputStream(""))
+ .type(ProfileFile.Type.CONFIGURATION)
+ .build());
+ ProfileCredentialsProvider provider = builder.build();
+
+ assertThatThrownBy(provider::resolveCredentials).isInstanceOf(SdkClientException.class);
+ }
+
+ @Test
+ void resolveCredentials_missingProfile_throwsException() {
+ ProfileFile file = profileFile("[default]\n"
+ + "aws_access_key_id = defaultAccessKey\n"
+ + "aws_secret_access_key = defaultSecretAccessKey");
+
+ try (ProfileCredentialsProvider provider =
+ ProfileCredentialsProvider.builder()
+ .profileFile(() -> file)
+ .profileName("foo")
+ .build()) {
+
+ assertThatThrownBy(provider::resolveCredentials).isInstanceOf(SdkClientException.class);
+ }
+ }
+
+ @Test
+ void resolveCredentials_profileWithoutCredentials_throwsException() {
+ ProfileFile file = profileFile("[default]");
+
+ ProfileCredentialsProvider provider =
+ ProfileCredentialsProvider.builder()
+ .profileFile(() -> file)
+ .profileName("default")
+ .build();
+
+ assertThatThrownBy(provider::resolveCredentials).isInstanceOf(SdkClientException.class);
+ }
+
+ @Test
+ void resolveCredentials_presentProfile_returnsCredentials() {
+ ProfileFile file = profileFile("[default]\n"
+ + "aws_access_key_id = defaultAccessKey\n"
+ + "aws_secret_access_key = defaultSecretAccessKey");
+
+ ProfileCredentialsProvider provider =
+ ProfileCredentialsProvider.builder()
+ .profileFile(() -> file)
+ .profileName("default")
+ .build();
+
+ assertThat(provider.resolveCredentials()).satisfies(credentials -> {
+ assertThat(credentials.accessKeyId()).isEqualTo("defaultAccessKey");
+ assertThat(credentials.secretAccessKey()).isEqualTo("defaultSecretAccessKey");
+ });
+ }
+
+ @Test
+ void resolveCredentials_presentProfileFileSupplier_returnsCredentials() {
+ Path path = generateTestCredentialsFile("defaultAccessKey", "defaultSecretAccessKey");
+
+ ProfileCredentialsProvider provider =
+ ProfileCredentialsProvider.builder()
+ .profileFile(ProfileFileSupplier.reloadWhenModified(path, ProfileFile.Type.CREDENTIALS))
+ .profileName("default")
+ .build();
+
+ assertThat(provider.resolveCredentials()).satisfies(credentials -> {
+ assertThat(credentials.accessKeyId()).isEqualTo("defaultAccessKey");
+ assertThat(credentials.secretAccessKey()).isEqualTo("defaultSecretAccessKey");
+ });
+ }
+
+ @Test
+ void resolveCredentials_presentSupplierProfileFile_returnsCredentials() {
+ Supplier supplier = () -> profileFile("[default]\naws_access_key_id = defaultAccessKey\n"
+ + "aws_secret_access_key = defaultSecretAccessKey\n");
+
+ ProfileCredentialsProvider provider =
+ ProfileCredentialsProvider.builder()
+ .profileFile(supplier)
+ .profileName("default")
+ .build();
+
+ assertThat(provider.resolveCredentials()).satisfies(credentials -> {
+ assertThat(credentials.accessKeyId()).isEqualTo("defaultAccessKey");
+ assertThat(credentials.secretAccessKey()).isEqualTo("defaultSecretAccessKey");
+ });
+ }
+
+ @Test
+ void create_noProfileName_returnsProfileCredentialsProviderToResolveWithDefaults() {
+ ProfileCredentialsProvider provider = ProfileCredentialsProvider.create();
+ String toString = provider.toString();
+
+ assertThat(toString).satisfies(s -> assertThat(s).contains("profileName=default"));
+ }
+
+ @Test
+ void create_givenProfileName_returnsProfileCredentialsProviderToResolveForGivenName() {
+ ProfileCredentialsProvider provider = ProfileCredentialsProvider.create("override");
+ String toString = provider.toString();
+
+ assertThat(toString).satisfies(s -> assertThat(s).contains("profileName=override"));
+ }
+
+ @Test
+ void toString_anyProfileCredentialsProviderAfterResolvingCredentialsFileDoesExists_returnsProfileFile() {
+ ProfileCredentialsProvider provider = new ProfileCredentialsProvider.BuilderImpl()
+ .defaultProfileFileLoader(() -> profileFile("[default]\naws_access_key_id = not-set\n"
+ + "aws_secret_access_key = not-set\n"))
+ .build();
+ provider.resolveCredentials();
+ String toString = provider.toString();
+
+ assertThat(toString).satisfies(s -> {
+ assertThat(s).contains("profileName=default");
+ assertThat(s).contains("profileFile=");
+ });
+ }
+
+ @Test
+ void toString_anyProfileCredentialsProviderAfterResolvingCredentialsFileDoesNotExist_throwsException() {
+ ProfileCredentialsProvider provider = new ProfileCredentialsProvider.BuilderImpl()
+ .defaultProfileFileLoader(() -> ProfileFile.builder()
+ .content(new StringInputStream(""))
+ .type(ProfileFile.Type.CONFIGURATION)
+ .build())
+ .build();
+
+ assertThatThrownBy(provider::resolveCredentials).isInstanceOf(SdkClientException.class);
+ }
+
+ @Test
+ void toString_anyProfileCredentialsProviderBeforeResolvingCredentials_doesNotReturnProfileFile() {
+ ProfileCredentialsProvider provider =
+ new ProfileCredentialsProvider.BuilderImpl()
+ .defaultProfileFileLoader(() -> ProfileFile.builder()
+ .content(new StringInputStream(""))
+ .type(ProfileFile.Type.CONFIGURATION)
+ .build())
+ .build();
+
+ String toString = provider.toString();
+
+ assertThat(toString).satisfies(s -> {
+ assertThat(s).contains("profileName");
+ assertThat(s).doesNotContain("profileFile");
+ });
+ }
+
+ @Test
+ void toBuilder_fromCredentialsProvider_returnsBuilderCapableOfProducingSimilarProvider() {
+ ProfileCredentialsProvider provider1 = ProfileCredentialsProvider.create("override");
+ ProfileCredentialsProvider provider2 = provider1.toBuilder().build();
+
+ String provider1ToString = provider1.toString();
+ String provider2ToString = provider2.toString();
+ assertThat(provider1ToString).isEqualTo(provider2ToString);
+ }
+
private ProfileFile profileFile(String string) {
return ProfileFile.builder().content(new StringInputStream(string)).type(ProfileFile.Type.CONFIGURATION).build();
}
+
+ private Path generateTestFile(String contents, String filename) {
+ try {
+ Files.createDirectories(testDirectory);
+ return Files.write(testDirectory.resolve(filename), contents.getBytes(StandardCharsets.UTF_8));
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ private Path generateTestCredentialsFile(String accessKeyId, String secretAccessKey) {
+ String contents = String.format("[default]\naws_access_key_id = %s\naws_secret_access_key = %s\n",
+ accessKeyId, secretAccessKey);
+ return generateTestFile(contents, "credentials.txt");
+ }
+
}
diff --git a/core/auth/src/test/java/software/amazon/awssdk/auth/token/credentials/ProfileTokenProviderTest.java b/core/auth/src/test/java/software/amazon/awssdk/auth/token/credentials/ProfileTokenProviderTest.java
index a03cd87d7cf4..f5324dd43771 100644
--- a/core/auth/src/test/java/software/amazon/awssdk/auth/token/credentials/ProfileTokenProviderTest.java
+++ b/core/auth/src/test/java/software/amazon/awssdk/auth/token/credentials/ProfileTokenProviderTest.java
@@ -17,16 +17,17 @@
import static org.assertj.core.api.Assertions.assertThatThrownBy;
+import java.util.function.Supplier;
import org.junit.jupiter.api.Test;
-import software.amazon.awssdk.auth.token.credentials.ProfileTokenProvider;
+import org.mockito.Mockito;
import software.amazon.awssdk.core.exception.SdkClientException;
import software.amazon.awssdk.profiles.ProfileFile;
import software.amazon.awssdk.utils.StringInputStream;
-public class ProfileTokenProviderTest {
+class ProfileTokenProviderTest {
@Test
- public void missingProfileFile_throwsException() {
+ void missingProfileFile_throwsException() {
ProfileTokenProvider provider =
new ProfileTokenProvider.BuilderImpl()
.defaultProfileFileLoader(() -> ProfileFile.builder()
@@ -39,7 +40,7 @@ public void missingProfileFile_throwsException() {
}
@Test
- public void emptyProfileFile_throwsException() {
+ void emptyProfileFile_throwsException() {
ProfileTokenProvider provider =
new ProfileTokenProvider.BuilderImpl()
.defaultProfileFileLoader(() -> ProfileFile.builder()
@@ -52,7 +53,7 @@ public void emptyProfileFile_throwsException() {
}
@Test
- public void missingProfile_throwsException() {
+ void missingProfile_throwsException() {
ProfileFile file = profileFile("[default]\n"
+ "aws_access_key_id = defaultAccessKey\n"
+ "aws_secret_access_key = defaultSecretAccessKey");
@@ -64,7 +65,7 @@ public void missingProfile_throwsException() {
}
@Test
- public void compatibleProfileSettings_callsLoader() {
+ void compatibleProfileSettings_callsLoader() {
ProfileFile file = profileFile("[default]");
ProfileTokenProvider provider =
@@ -73,6 +74,28 @@ public void compatibleProfileSettings_callsLoader() {
assertThatThrownBy(provider::resolveToken).hasMessageContaining("does not have sso_session property");
}
+ @Test
+ void resolveToken_profileFileSupplier_suppliesObjectPerCall() {
+ ProfileFile file1 = profileFile("[profile sso]\n"
+ + "aws_access_key_id = defaultAccessKey\n"
+ + "aws_secret_access_key = defaultSecretAccessKey\n"
+ + "sso_session = xyz");
+ ProfileFile file2 = profileFile("[profile sso]\n"
+ + "aws_access_key_id = modifiedAccessKey\n"
+ + "aws_secret_access_key = modifiedSecretAccessKey\n"
+ + "sso_session = xyz");
+ Supplier supplier = Mockito.mock(Supplier.class);
+
+ ProfileTokenProvider provider =
+ ProfileTokenProvider.builder().profileFile(supplier).profileName("sso").build();
+
+ Mockito.when(supplier.get()).thenReturn(file1, file2);
+ assertThatThrownBy(provider::resolveToken).isInstanceOf(IllegalArgumentException.class);
+ assertThatThrownBy(provider::resolveToken).isInstanceOf(IllegalArgumentException.class);
+
+ Mockito.verify(supplier, Mockito.times(2)).get();
+ }
+
private ProfileFile profileFile(String string) {
return ProfileFile.builder()
.content(new StringInputStream(string))
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 5b8a7c13287f..c41e604afccb 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
@@ -21,6 +21,7 @@
import java.util.Arrays;
import java.util.List;
import java.util.Optional;
+import java.util.function.Supplier;
import software.amazon.awssdk.annotations.SdkProtectedApi;
import software.amazon.awssdk.annotations.SdkTestInternalApi;
import software.amazon.awssdk.auth.credentials.AwsCredentialsProvider;
@@ -245,7 +246,7 @@ private URI resolveEndpoint(SdkClientConfiguration config) {
private URI endpointFromConfig(SdkClientConfiguration config) {
return new DefaultServiceEndpointBuilder(serviceEndpointPrefix(), DEFAULT_ENDPOINT_PROTOCOL)
.withRegion(config.option(AwsClientOption.AWS_REGION))
- .withProfileFile(() -> config.option(SdkClientOption.PROFILE_FILE))
+ .withProfileFile(config.option(SdkClientOption.PROFILE_FILE_SUPPLIER))
.withProfileName(config.option(SdkClientOption.PROFILE_NAME))
.putAdvancedOption(ServiceMetadataAdvancedOption.DEFAULT_S3_US_EAST_1_REGIONAL_ENDPOINT,
config.option(ServiceMetadataAdvancedOption.DEFAULT_S3_US_EAST_1_REGIONAL_ENDPOINT))
@@ -272,23 +273,22 @@ private Region regionFromDefaultProvider(SdkClientConfiguration config) {
throw new IllegalStateException("No region was configured, and use-region-provider-chain was disabled.");
}
- ProfileFile profileFile = config.option(SdkClientOption.PROFILE_FILE);
+ Supplier profileFile = config.option(SdkClientOption.PROFILE_FILE_SUPPLIER);
String profileName = config.option(SdkClientOption.PROFILE_NAME);
return DefaultAwsRegionProviderChain.builder()
- .profileFile(() -> profileFile)
+ .profileFile(profileFile)
.profileName(profileName)
.build()
.getRegion();
}
private DefaultsMode resolveDefaultsMode(SdkClientConfiguration config) {
- DefaultsMode defaultsMode =
- config.option(AwsClientOption.DEFAULTS_MODE) != null ?
- config.option(AwsClientOption.DEFAULTS_MODE) :
- DefaultsModeResolver.create()
- .profileFile(() -> config.option(SdkClientOption.PROFILE_FILE))
- .profileName(config.option(SdkClientOption.PROFILE_NAME))
- .resolve();
+ DefaultsMode defaultsMode;
+ defaultsMode = config.option(AwsClientOption.DEFAULTS_MODE) != null ? config.option(AwsClientOption.DEFAULTS_MODE) :
+ DefaultsModeResolver.create()
+ .profileFile(config.option(SdkClientOption.PROFILE_FILE_SUPPLIER))
+ .profileName(config.option(SdkClientOption.PROFILE_NAME))
+ .resolve();
if (defaultsMode == DefaultsMode.AUTO) {
defaultsMode = autoDefaultsModeDiscovery.discover(config.option(AwsClientOption.AWS_REGION));
@@ -313,10 +313,10 @@ private Boolean resolveDualstackEndpointEnabled(SdkClientConfiguration config) {
* Load the dualstack endpoint setting from the default provider logic.
*/
private Boolean resolveUseDualstackFromDefaultProvider(SdkClientConfiguration config) {
- ProfileFile profileFile = config.option(SdkClientOption.PROFILE_FILE);
+ Supplier profileFile = config.option(SdkClientOption.PROFILE_FILE_SUPPLIER);
String profileName = config.option(SdkClientOption.PROFILE_NAME);
return DualstackEnabledProvider.builder()
- .profileFile(() -> profileFile)
+ .profileFile(profileFile)
.profileName(profileName)
.build()
.isDualstackEnabled()
@@ -336,10 +336,10 @@ private Boolean resolveFipsEndpointEnabled(SdkClientConfiguration config) {
* Load the dualstack endpoint setting from the default provider logic.
*/
private Boolean resolveUseFipsFromDefaultProvider(SdkClientConfiguration config) {
- ProfileFile profileFile = config.option(SdkClientOption.PROFILE_FILE);
+ Supplier profileFile = config.option(SdkClientOption.PROFILE_FILE_SUPPLIER);
String profileName = config.option(SdkClientOption.PROFILE_NAME);
return FipsEnabledProvider.builder()
- .profileFile(() -> profileFile)
+ .profileFile(profileFile)
.profileName(profileName)
.build()
.isFipsEnabled()
@@ -353,7 +353,7 @@ private AwsCredentialsProvider resolveCredentials(SdkClientConfiguration config)
return config.option(AwsClientOption.CREDENTIALS_PROVIDER) != null
? config.option(AwsClientOption.CREDENTIALS_PROVIDER)
: DefaultCredentialsProvider.builder()
- .profileFile(config.option(SdkClientOption.PROFILE_FILE))
+ .profileFile(config.option(SdkClientOption.PROFILE_FILE_SUPPLIER))
.profileName(config.option(SdkClientOption.PROFILE_NAME))
.build();
}
@@ -370,7 +370,7 @@ private RetryPolicy resolveAwsRetryPolicy(SdkClientConfiguration config) {
}
RetryMode retryMode = RetryMode.resolver()
- .profileFile(() -> config.option(SdkClientOption.PROFILE_FILE))
+ .profileFile(config.option(SdkClientOption.PROFILE_FILE_SUPPLIER))
.profileName(config.option(SdkClientOption.PROFILE_NAME))
.defaultRetryMode(config.option(SdkClientOption.DEFAULT_RETRY_MODE))
.resolve();
@@ -466,4 +466,5 @@ private static Pair> transformFipsPseudoRegionIfNecess
return Pair.of(region, Optional.empty());
}
+
}
diff --git a/core/aws-core/src/main/java/software/amazon/awssdk/awscore/internal/AwsExecutionContextBuilder.java b/core/aws-core/src/main/java/software/amazon/awssdk/awscore/internal/AwsExecutionContextBuilder.java
index 6bcd1ec65b78..32daaba3f3fb 100644
--- a/core/aws-core/src/main/java/software/amazon/awssdk/awscore/internal/AwsExecutionContextBuilder.java
+++ b/core/aws-core/src/main/java/software/amazon/awssdk/awscore/internal/AwsExecutionContextBuilder.java
@@ -82,7 +82,10 @@ private AwsExecutionContextBuilder() {
.putAttribute(SdkInternalExecutionAttribute.HAS_INITIAL_REQUEST_EVENT, executionParams.hasInitialRequestEvent())
.putAttribute(SdkExecutionAttribute.CLIENT_TYPE, clientConfig.option(SdkClientOption.CLIENT_TYPE))
.putAttribute(SdkExecutionAttribute.SERVICE_NAME, clientConfig.option(SdkClientOption.SERVICE_NAME))
- .putAttribute(SdkExecutionAttribute.PROFILE_FILE, clientConfig.option(SdkClientOption.PROFILE_FILE))
+ .putAttribute(SdkExecutionAttribute.PROFILE_FILE, clientConfig.option(SdkClientOption.PROFILE_FILE_SUPPLIER) != null ?
+ clientConfig.option(SdkClientOption.PROFILE_FILE_SUPPLIER).get() :
+ null)
+ .putAttribute(SdkExecutionAttribute.PROFILE_FILE_SUPPLIER, clientConfig.option(SdkClientOption.PROFILE_FILE_SUPPLIER))
.putAttribute(SdkExecutionAttribute.PROFILE_NAME, clientConfig.option(SdkClientOption.PROFILE_NAME))
.putAttribute(AwsExecutionAttribute.DUALSTACK_ENDPOINT_ENABLED,
clientConfig.option(AwsClientOption.DUALSTACK_ENDPOINT_ENABLED))
diff --git a/core/aws-core/src/test/java/software/amazon/awssdk/awscore/internal/AwsExecutionContextBuilderTest.java b/core/aws-core/src/test/java/software/amazon/awssdk/awscore/internal/AwsExecutionContextBuilderTest.java
index 1234c05a90e7..67ef23a8426c 100644
--- a/core/aws-core/src/test/java/software/amazon/awssdk/awscore/internal/AwsExecutionContextBuilderTest.java
+++ b/core/aws-core/src/test/java/software/amazon/awssdk/awscore/internal/AwsExecutionContextBuilderTest.java
@@ -25,6 +25,7 @@
import java.util.Collections;
import java.util.List;
import java.util.Optional;
+import java.util.function.Supplier;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -48,6 +49,7 @@
import software.amazon.awssdk.core.interceptor.trait.HttpChecksum;
import software.amazon.awssdk.core.internal.util.HttpChecksumUtils;
import software.amazon.awssdk.core.signer.Signer;
+import software.amazon.awssdk.profiles.ProfileFile;
@RunWith(MockitoJUnitRunner.class)
public class AwsExecutionContextBuilderTest {
@@ -178,6 +180,22 @@ public void invokeInterceptorsAndCreateExecutionContext_multipleExecutionContext
assertThat(checksumSpecs1).isNotSameAs(checksumSpecs2);
assertThat(checksumSpecs2).isSameAs(checksumSpecs3);
}
+
+ @Test
+ public void invokeInterceptorsAndCreateExecutionContext_profileFileSupplier_storesValueInExecutionAttributes() {
+ ClientExecutionParams executionParams = clientExecutionParams();
+ Supplier profileFileSupplier = () -> null;
+ SdkClientConfiguration clientConfig = testClientConfiguration()
+ .option(SdkClientOption.PROFILE_FILE_SUPPLIER, profileFileSupplier)
+ .build();
+
+ ExecutionContext executionContext =
+ AwsExecutionContextBuilder.invokeInterceptorsAndCreateExecutionContext(executionParams, clientConfig);
+
+ ExecutionAttributes executionAttributes = executionContext.executionAttributes();
+
+ assertThat(profileFileSupplier).isSameAs(executionAttributes.getAttribute(SdkExecutionAttribute.PROFILE_FILE_SUPPLIER));
+ }
private ClientExecutionParams clientExecutionParams() {
return new ClientExecutionParams()
diff --git a/core/profiles/pom.xml b/core/profiles/pom.xml
index 6d904037e880..e77e31fbd594 100644
--- a/core/profiles/pom.xml
+++ b/core/profiles/pom.xml
@@ -55,6 +55,12 @@
assertj-coretest
+
+ com.google.jimfs
+ jimfs
+ ${jimfs.version}
+ test
+
diff --git a/core/profiles/src/main/java/software/amazon/awssdk/profiles/ProfileFile.java b/core/profiles/src/main/java/software/amazon/awssdk/profiles/ProfileFile.java
index 064397c90c6c..b12370b1cebd 100644
--- a/core/profiles/src/main/java/software/amazon/awssdk/profiles/ProfileFile.java
+++ b/core/profiles/src/main/java/software/amazon/awssdk/profiles/ProfileFile.java
@@ -273,7 +273,7 @@ public void setContent(InputStream contentStream) {
@Override
public Builder content(Path contentLocation) {
Validate.paramNotNull(contentLocation, "profileLocation");
- Validate.validState(contentLocation.toFile().exists(), "Profile file '%s' does not exist.", contentLocation);
+ Validate.validState(Files.exists(contentLocation), "Profile file '%s' does not exist.", contentLocation);
this.content = null;
this.contentLocation = contentLocation;
diff --git a/core/profiles/src/main/java/software/amazon/awssdk/profiles/ProfileFileSupplier.java b/core/profiles/src/main/java/software/amazon/awssdk/profiles/ProfileFileSupplier.java
new file mode 100644
index 000000000000..1521fdcfbc2d
--- /dev/null
+++ b/core/profiles/src/main/java/software/amazon/awssdk/profiles/ProfileFileSupplier.java
@@ -0,0 +1,160 @@
+/*
+ * 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.profiles;
+
+import java.nio.file.Path;
+import java.util.Objects;
+import java.util.Optional;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.atomic.AtomicReference;
+import java.util.function.Supplier;
+import software.amazon.awssdk.annotations.SdkPublicApi;
+import software.amazon.awssdk.profiles.internal.ProfileFileRefresher;
+
+/**
+ * Encapsulates the logic for supplying either a single or multiple ProfileFile instances.
+ *
+ * Each call to the {@link #get()} method will result in either a new or previously supplied profile based on the
+ * implementation's rules.
+ */
+@SdkPublicApi
+@FunctionalInterface
+public interface ProfileFileSupplier extends Supplier {
+
+ /**
+ * Creates a {@link ProfileFileSupplier} capable of producing multiple profile objects by aggregating the default
+ * credentials and configuration files as determined by {@link ProfileFileLocation#credentialsFileLocation()} abd
+ * {@link ProfileFileLocation#configurationFileLocation()}. This supplier will return a new ProfileFile instance only once
+ * either disk file has been modified. Multiple calls to the supplier while both disk files are unchanged will return the
+ * same object.
+ *
+ * @return Implementation of {@link ProfileFileSupplier} that is capable of supplying a new aggregate profile when either file
+ * has been modified.
+ */
+ static ProfileFileSupplier defaultSupplier() {
+ Optional credentialsSupplierOptional
+ = ProfileFileLocation.credentialsFileLocation()
+ .map(path -> reloadWhenModified(path, ProfileFile.Type.CREDENTIALS));
+
+ Optional configurationSupplierOptional
+ = ProfileFileLocation.configurationFileLocation()
+ .map(path -> reloadWhenModified(path, ProfileFile.Type.CONFIGURATION));
+
+ ProfileFileSupplier supplier = () -> ProfileFile.builder().build();
+ if (credentialsSupplierOptional.isPresent() && configurationSupplierOptional.isPresent()) {
+ supplier = aggregate(credentialsSupplierOptional.get(), configurationSupplierOptional.get());
+ } else if (credentialsSupplierOptional.isPresent()) {
+ supplier = credentialsSupplierOptional.get();
+ } else if (configurationSupplierOptional.isPresent()) {
+ supplier = configurationSupplierOptional.get();
+ }
+
+ return supplier;
+ }
+
+ /**
+ * Creates a {@link ProfileFileSupplier} capable of producing multiple profile objects from a file. This supplier will
+ * return a new ProfileFile instance only once the disk file has been modified. Multiple calls to the supplier while the
+ * disk file is unchanged will return the same object.
+ *
+ * @param path Path to the file to read from.
+ * @param type The type of file. See {@link ProfileFile.Type} for possible values.
+ * @return Implementation of {@link ProfileFileSupplier} that is capable of supplying a new profile when the file
+ * has been modified.
+ */
+ static ProfileFileSupplier reloadWhenModified(Path path, ProfileFile.Type type) {
+ return new ProfileFileSupplier() {
+
+ final ProfileFile.Builder builder = ProfileFile.builder()
+ .content(path)
+ .type(type);
+
+ final ProfileFileRefresher refresher = ProfileFileRefresher.builder()
+ .profileFile(builder::build)
+ .profileFilePath(path)
+ .build();
+
+ @Override
+ public ProfileFile get() {
+ return refresher.refreshIfStale();
+ }
+
+ };
+ }
+
+ /**
+ * Creates a {@link ProfileFileSupplier} that produces an existing profile.
+ *
+ * @param profileFile Profile object to supply.
+ * @return Implementation of {@link ProfileFileSupplier} that is capable of supplying a single profile.
+ */
+ static ProfileFileSupplier fixedProfileFile(ProfileFile profileFile) {
+ return () -> profileFile;
+ }
+
+ /**
+ * Creates a {@link ProfileFileSupplier} by combining the {@link ProfileFile} objects from multiple {@code
+ * ProfileFileSupplier}s. Objects are passed into {@link ProfileFile.Aggregator}.
+ *
+ * @param suppliers Array of {@code ProfileFileSupplier} objects. {@code ProfileFile} objects are passed to
+ * {@link ProfileFile.Aggregator#addFile(ProfileFile)} in the same argument order as the supplier that
+ * generated it.
+ * @return Implementation of {@link ProfileFileSupplier} aggregating results from the supplier objects.
+ */
+ static ProfileFileSupplier aggregate(ProfileFileSupplier... suppliers) {
+
+ return new ProfileFileSupplier() {
+
+ final AtomicReference currentAggregateProfileFile = new AtomicReference<>();
+ final ConcurrentHashMap, ProfileFile> currentValuesBySupplier = new ConcurrentHashMap<>();
+
+ @Override
+ public ProfileFile get() {
+ boolean refreshAggregate = false;
+ for (ProfileFileSupplier supplier : suppliers) {
+ if (didSuppliedValueChange(supplier)) {
+ refreshAggregate = true;
+ }
+ }
+
+ if (refreshAggregate) {
+ refreshCurrentAggregate();
+ }
+
+ return currentAggregateProfileFile.get();
+ }
+
+ private boolean didSuppliedValueChange(Supplier supplier) {
+ ProfileFile next = supplier.get();
+ ProfileFile current = currentValuesBySupplier.put(supplier, next);
+
+ return !Objects.equals(next, current);
+ }
+
+ private void refreshCurrentAggregate() {
+ ProfileFile.Aggregator aggregator = ProfileFile.aggregator();
+ currentValuesBySupplier.values().forEach(aggregator::addFile);
+ ProfileFile current = currentAggregateProfileFile.get();
+ ProfileFile next = aggregator.build();
+ if (!Objects.equals(current, next)) {
+ currentAggregateProfileFile.compareAndSet(current, next);
+ }
+ }
+
+ };
+ }
+
+}
diff --git a/core/profiles/src/main/java/software/amazon/awssdk/profiles/ProfileFileSupplierBuilder.java b/core/profiles/src/main/java/software/amazon/awssdk/profiles/ProfileFileSupplierBuilder.java
new file mode 100644
index 000000000000..6e3aea7cf5bf
--- /dev/null
+++ b/core/profiles/src/main/java/software/amazon/awssdk/profiles/ProfileFileSupplierBuilder.java
@@ -0,0 +1,104 @@
+/*
+ * 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.profiles;
+
+import java.nio.file.Path;
+import java.time.Clock;
+import java.util.Objects;
+import java.util.function.Consumer;
+import java.util.function.Supplier;
+import software.amazon.awssdk.annotations.SdkInternalApi;
+import software.amazon.awssdk.profiles.internal.ProfileFileRefresher;
+
+@SdkInternalApi
+final class ProfileFileSupplierBuilder {
+
+ private boolean reloadingSupplier = false;
+ private Supplier profileFile;
+ private Path profileFilePath;
+ private Clock clock;
+ private Consumer onProfileFileLoad;
+
+ public ProfileFileSupplierBuilder reloadWhenModified(Path path, ProfileFile.Type type) {
+ ProfileFile.Builder builder = ProfileFile.builder()
+ .content(path)
+ .type(type);
+ this.profileFile = builder::build;
+ this.profileFilePath = path;
+ this.reloadingSupplier = true;
+ return this;
+ }
+
+ public ProfileFileSupplierBuilder fixedProfileFile(Path path, ProfileFile.Type type) {
+ return fixedProfileFile(ProfileFile.builder()
+ .content(path)
+ .type(type)
+ .build());
+ }
+
+ public ProfileFileSupplierBuilder fixedProfileFile(ProfileFile profileFile) {
+ this.profileFile = () -> profileFile;
+ this.profileFilePath = null;
+ this.reloadingSupplier = false;
+ return this;
+ }
+
+ public ProfileFileSupplierBuilder onProfileFileLoad(Consumer action) {
+ this.onProfileFileLoad = action;
+ return this;
+ }
+
+ public ProfileFileSupplierBuilder clock(Clock clock) {
+ this.clock = clock;
+ return this;
+ }
+
+ public ProfileFileSupplier build() {
+ return fromBuilder(this);
+ }
+
+ /**
+ * Completes {@link ProfileFileSupplier} build.
+ * @param builder Object to complete build.
+ * @return Implementation of {@link ProfileFileSupplier}.
+ */
+ static ProfileFileSupplier fromBuilder(ProfileFileSupplierBuilder builder) {
+ if (builder.reloadingSupplier) {
+
+ ProfileFileRefresher.Builder refresherBuilder = ProfileFileRefresher.builder()
+ .profileFile(builder.profileFile)
+ .profileFilePath(builder.profileFilePath);
+
+ if (Objects.nonNull(builder.clock)) {
+ refresherBuilder.clock(builder.clock);
+ }
+ if (Objects.nonNull(builder.onProfileFileLoad)) {
+ refresherBuilder.onProfileFileReload(builder.onProfileFileLoad);
+ }
+
+ ProfileFileRefresher refresher = refresherBuilder.build();
+
+ return new ProfileFileSupplier() {
+ @Override
+ public ProfileFile get() {
+ return refresher.refreshIfStale();
+ }
+ };
+ }
+
+ return builder.profileFile::get;
+ }
+}
diff --git a/core/profiles/src/main/java/software/amazon/awssdk/profiles/internal/ProfileFileRefresher.java b/core/profiles/src/main/java/software/amazon/awssdk/profiles/internal/ProfileFileRefresher.java
new file mode 100644
index 000000000000..a55ea28415b2
--- /dev/null
+++ b/core/profiles/src/main/java/software/amazon/awssdk/profiles/internal/ProfileFileRefresher.java
@@ -0,0 +1,235 @@
+/*
+ * 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.profiles.internal;
+
+import java.io.IOException;
+import java.io.UncheckedIOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.time.Clock;
+import java.time.Instant;
+import java.util.Objects;
+import java.util.function.Consumer;
+import java.util.function.Supplier;
+import software.amazon.awssdk.annotations.SdkInternalApi;
+import software.amazon.awssdk.annotations.SdkTestInternalApi;
+import software.amazon.awssdk.profiles.ProfileFile;
+import software.amazon.awssdk.utils.cache.CachedSupplier;
+import software.amazon.awssdk.utils.cache.RefreshResult;
+
+/**
+ * Class used for caching and reloading ProfileFile objects from a Supplier.
+ */
+@SdkInternalApi
+public final class ProfileFileRefresher {
+
+ private static final ProfileFileRefreshRecord EMPTY_REFRESH_RECORD = ProfileFileRefreshRecord.builder()
+ .refreshTime(Instant.MIN)
+ .build();
+ private final CachedSupplier profileFileCache;
+ private volatile ProfileFileRefreshRecord currentRefreshRecord;
+ private final Supplier profileFile;
+ private final Path profileFilePath;
+ private final Consumer onProfileFileReload;
+ private final Clock clock;
+
+ private ProfileFileRefresher(Builder builder) {
+ this.clock = builder.clock;
+ this.profileFile = builder.profileFile;
+ this.profileFilePath = builder.profileFilePath;
+ this.onProfileFileReload = builder.onProfileFileReload;
+ this.profileFileCache = CachedSupplier.builder(this::refreshResult)
+ .clock(this.clock)
+ .build();
+ this.currentRefreshRecord = EMPTY_REFRESH_RECORD;
+ }
+
+ /**
+ * Builder method to construct instance of ProfileFileRefresher.
+ */
+ public static ProfileFileRefresher.Builder builder() {
+ return new ProfileFileRefresher.Builder();
+ }
+
+ /**
+ * Retrieves the cache value or refreshes it if stale.
+ */
+ public ProfileFile refreshIfStale() {
+ ProfileFileRefreshRecord cachedOrRefreshedRecord = profileFileCache.get();
+ ProfileFile cachedOrRefreshedProfileFile = cachedOrRefreshedRecord.profileFile;
+ if (isNewProfileFile(cachedOrRefreshedProfileFile)) {
+ currentRefreshRecord = cachedOrRefreshedRecord;
+ }
+
+ return cachedOrRefreshedProfileFile;
+ }
+
+ private RefreshResult refreshResult() {
+ return reloadAsRefreshResultIfStale();
+ }
+
+ private RefreshResult reloadAsRefreshResultIfStale() {
+ Instant now = clock.instant();
+ ProfileFileRefreshRecord refreshRecord;
+
+ if (canReloadProfileFile() || hasNotBeenPreviouslyLoaded()) {
+ ProfileFile reloadedProfileFile = reload(profileFile, onProfileFileReload);
+ refreshRecord = ProfileFileRefreshRecord.builder()
+ .profileFile(reloadedProfileFile)
+ .refreshTime(now)
+ .build();
+ } else {
+ refreshRecord = currentRefreshRecord;
+ }
+
+ return wrapIntoRefreshResult(refreshRecord, now);
+ }
+
+ private RefreshResult wrapIntoRefreshResult(T value, Instant staleTime) {
+ return RefreshResult.builder(value)
+ .staleTime(staleTime)
+ .build();
+ }
+
+ private static ProfileFile reload(Supplier supplier) {
+ return supplier.get();
+ }
+
+ private static ProfileFile reload(Supplier supplier, Consumer consumer) {
+ ProfileFile reloadedProfileFile = reload(supplier);
+ consumer.accept(reloadedProfileFile);
+
+ return reloadedProfileFile;
+ }
+
+ private boolean isNewProfileFile(ProfileFile profileFile) {
+ return !Objects.equals(currentRefreshRecord.profileFile, profileFile);
+ }
+
+ private boolean canReloadProfileFile() {
+ if (Objects.isNull(profileFilePath)) {
+ return false;
+ }
+
+ try {
+ Instant lastModifiedInstant = Files.getLastModifiedTime(profileFilePath).toInstant();
+ return currentRefreshRecord.refreshTime.isBefore(lastModifiedInstant);
+ } catch (IOException e) {
+ throw new UncheckedIOException(e);
+ }
+ }
+
+ private boolean hasNotBeenPreviouslyLoaded() {
+ return currentRefreshRecord == EMPTY_REFRESH_RECORD;
+ }
+
+
+ public static final class Builder {
+
+ private Supplier profileFile;
+ private Path profileFilePath;
+ private Consumer onProfileFileReload = p -> { };
+ private Clock clock = Clock.systemUTC();
+
+ private Builder() {
+ }
+
+ public Builder profileFile(Supplier profileFile) {
+ this.profileFile = profileFile;
+ return this;
+ }
+
+ public Builder profileFilePath(Path profileFilePath) {
+ this.profileFilePath = profileFilePath;
+ return this;
+ }
+
+ /**
+ * Sets a clock for managing stale and prefetch durations.
+ */
+ @SdkTestInternalApi
+ public Builder clock(Clock clock) {
+ this.clock = clock;
+ return this;
+ }
+
+ /**
+ * Sets a custom action to perform when a profile file is reloaded. This action is executed when both the cache is stale
+ * and the disk file associated with the profile file has been modified since the last load.
+ *
+ * @param consumer The action to perform.
+ */
+ public Builder onProfileFileReload(Consumer consumer) {
+ this.onProfileFileReload = consumer;
+ return this;
+ }
+
+ public ProfileFileRefresher build() {
+ return new ProfileFileRefresher(this);
+ }
+ }
+
+ /**
+ * Class used to encapsulate additional refresh information.
+ */
+ public static final class ProfileFileRefreshRecord {
+ private final Instant refreshTime;
+ private final ProfileFile profileFile;
+
+ private ProfileFileRefreshRecord(Builder builder) {
+ this.profileFile = builder.profileFile;
+ this.refreshTime = builder.refreshTime;
+ }
+
+ /**
+ * The refreshed ProfileFile instance.
+ */
+ public ProfileFile profileFile() {
+ return profileFile;
+ }
+
+ /**
+ * The time at which the RefreshResult was created.
+ */
+ public Instant refreshTime() {
+ return refreshTime;
+ }
+
+ static Builder builder() {
+ return new Builder();
+ }
+
+ private static final class Builder {
+ private Instant refreshTime;
+ private ProfileFile profileFile;
+
+ Builder refreshTime(Instant refreshTime) {
+ this.refreshTime = refreshTime;
+ return this;
+ }
+
+ Builder profileFile(ProfileFile profileFile) {
+ this.profileFile = profileFile;
+ return this;
+ }
+
+ ProfileFileRefreshRecord build() {
+ return new ProfileFileRefreshRecord(this);
+ }
+ }
+ }
+
+}
diff --git a/core/profiles/src/test/java/software/amazon/awssdk/profiles/ProfileFileSupplierTest.java b/core/profiles/src/test/java/software/amazon/awssdk/profiles/ProfileFileSupplierTest.java
new file mode 100644
index 000000000000..e51bc53101f0
--- /dev/null
+++ b/core/profiles/src/test/java/software/amazon/awssdk/profiles/ProfileFileSupplierTest.java
@@ -0,0 +1,573 @@
+/*
+ * 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.profiles;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+import com.google.common.jimfs.Jimfs;
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.FileSystem;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.attribute.FileTime;
+import java.time.Clock;
+import java.time.Duration;
+import java.time.Instant;
+import java.time.ZoneId;
+import java.time.ZoneOffset;
+import java.time.temporal.TemporalAmount;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Objects;
+import java.util.Optional;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.function.Predicate;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+import software.amazon.awssdk.utils.Pair;
+import software.amazon.awssdk.utils.StringInputStream;
+
+class ProfileFileSupplierTest {
+
+ private static FileSystem jimfs;
+ private static Path testDirectory;
+
+ @BeforeAll
+ public static void setup() {
+ jimfs = Jimfs.newFileSystem();
+ testDirectory = jimfs.getPath("test");
+ }
+
+ @AfterAll
+ public static void tearDown() {
+ try {
+ jimfs.close();
+ } catch (IOException e) {
+ // no-op
+ }
+ }
+
+ @Test
+ void get_profileFileFixed_doesNotReloadProfileFile() {
+ Path credentialsFilePath = generateTestCredentialsFile("defaultAccessKey", "defaultSecretAccessKey");
+
+ ProfileFileSupplier supplier = builder()
+ .fixedProfileFile(credentialsFilePath, ProfileFile.Type.CREDENTIALS)
+ .build();
+
+ ProfileFile file1 = supplier.get();
+
+ generateTestCredentialsFile("modifiedAccessKey", "modifiedSecretAccessKey");
+
+ ProfileFile file2 = supplier.get();
+
+ assertThat(file2).isSameAs(file1);
+ }
+
+ @Test
+ void get_profileModifiedWithinJitterPeriod_doesNotReloadCredentials() {
+ Path credentialsFilePath = generateTestCredentialsFile("defaultAccessKey", "defaultSecretAccessKey");
+
+ AdjustableClock clock = new AdjustableClock();
+ Duration durationWithinJitter = Duration.ofMillis(10);
+ ProfileFileSupplier supplier = builderWithClock(clock)
+ .reloadWhenModified(credentialsFilePath, ProfileFile.Type.CREDENTIALS)
+ .build();
+
+ ProfileFile file1 = supplier.get();
+
+ generateTestCredentialsFile("modifiedAccessKey", "modifiedSecretAccessKey");
+ updateModificationTime(credentialsFilePath, clock.instant().plus(durationWithinJitter));
+
+ clock.tickForward(durationWithinJitter);
+ ProfileFile file2 = supplier.get();
+
+ assertThat(file2).isSameAs(file1);
+ }
+
+ @Test
+ void get_profileModifiedOutsideJitterPeriod_reloadsCredentials() {
+ Path credentialsFilePath = generateTestCredentialsFile("defaultAccessKey", "defaultSecretAccessKey");
+
+ AdjustableClock clock = new AdjustableClock();
+
+ ProfileFileSupplier supplier = builderWithClock(clock)
+ .reloadWhenModified(credentialsFilePath, ProfileFile.Type.CREDENTIALS)
+ .build();
+
+ Duration durationOutsideJitter = Duration.ofSeconds(1);
+
+ supplier.get();
+
+ generateTestCredentialsFile("modifiedAccessKey", "modifiedSecretAccessKey");
+ updateModificationTime(credentialsFilePath, clock.instant().plus(durationOutsideJitter));
+
+ clock.tickForward(durationOutsideJitter);
+
+ Optional fileOptional = supplier.get().profile("default");
+ assertThat(fileOptional).isPresent();
+
+ assertThat(fileOptional.get()).satisfies(profile -> {
+ Optional awsAccessKeyIdOptional = profile.property("aws_access_key_id");
+ assertThat(awsAccessKeyIdOptional).isPresent();
+ String awsAccessKeyId = awsAccessKeyIdOptional.get();
+ assertThat(awsAccessKeyId).isEqualTo("modifiedAccessKey");
+
+ Optional awsSecretAccessKeyOptional = profile.property("aws_secret_access_key");
+ assertThat(awsSecretAccessKeyOptional).isPresent();
+ String awsSecretAccessKey = awsSecretAccessKeyOptional.get();
+ assertThat(awsSecretAccessKey).isEqualTo("modifiedSecretAccessKey");
+ });
+ }
+
+ @Test
+ void get_profileModified_reloadsProfileFile() {
+ Path credentialsFilePath = generateTestCredentialsFile("defaultAccessKey", "defaultSecretAccessKey");
+
+ AdjustableClock clock = new AdjustableClock();
+ ProfileFileSupplier supplier = builderWithClock(clock)
+ .reloadWhenModified(credentialsFilePath, ProfileFile.Type.CREDENTIALS)
+ .build();
+
+ Duration duration = Duration.ofSeconds(10);
+ ProfileFile file1 = supplier.get();
+
+ generateTestCredentialsFile("modifiedAccessKey", "modifiedSecretAccessKey");
+ updateModificationTime(credentialsFilePath, clock.instant().plusMillis(1));
+
+ clock.tickForward(duration);
+ ProfileFile file2 = supplier.get();
+
+ assertThat(file2).isNotSameAs(file1);
+ }
+
+ @Test
+ void get_profileModifiedOnceButRefreshedMultipleTimes_reloadsProfileFileOnce() {
+ Path credentialsFilePath = generateTestCredentialsFile("defaultAccessKey", "defaultSecretAccessKey");
+
+ AdjustableClock clock = new AdjustableClock();
+ ProfileFileSupplier supplier = builderWithClock(clock)
+ .reloadWhenModified(credentialsFilePath, ProfileFile.Type.CREDENTIALS)
+ .build();
+ ProfileFile file1 = supplier.get();
+
+ clock.tickForward(Duration.ofSeconds(5));
+ ProfileFile file2 = supplier.get();
+
+ generateTestCredentialsFile("modifiedAccessKey", "modifiedSecretAccessKey");
+ updateModificationTime(credentialsFilePath, clock.instant().plusMillis(1));
+
+ clock.tickForward(Duration.ofSeconds(5));
+ ProfileFile file3 = supplier.get();
+
+ assertThat(file2).isSameAs(file1);
+ assertThat(file3).isNotSameAs(file2);
+ }
+
+ @Test
+ void get_profileModifiedMultipleTimes_reloadsProfileFileOncePerChange() {
+ Path credentialsFilePath = generateTestCredentialsFile("defaultAccessKey", "defaultSecretAccessKey");
+
+ AdjustableClock clock = new AdjustableClock();
+ ProfileFileSupplier supplier = builderWithClock(clock)
+ .reloadWhenModified(credentialsFilePath, ProfileFile.Type.CREDENTIALS)
+ .build();
+ Duration duration = Duration.ofSeconds(5);
+
+ ProfileFile file1 = supplier.get();
+
+ clock.tickForward(duration);
+ ProfileFile file2 = supplier.get();
+
+ generateTestCredentialsFile("modifiedAccessKey", "modifiedSecretAccessKey");
+ updateModificationTime(credentialsFilePath, clock.instant().plusMillis(1));
+
+ clock.tickForward(duration);
+ ProfileFile file3 = supplier.get();
+
+ generateTestCredentialsFile("updatedAccessKey", "updatedSecretAccessKey");
+ updateModificationTime(credentialsFilePath, clock.instant().plusMillis(1));
+
+ clock.tickForward(duration);
+ ProfileFile file4 = supplier.get();
+
+ clock.tickForward(duration);
+ ProfileFile file5 = supplier.get();
+
+ assertThat(file2).isSameAs(file1);
+ assertThat(file3).isNotSameAs(file2);
+ assertThat(file4).isNotSameAs(file3);
+ assertThat(file5).isSameAs(file4);
+ }
+
+ @Test
+ void get_supplierBuiltByReloadWhenModified_loadsProfileFile() {
+ Path credentialsFilePath = generateTestCredentialsFile("defaultAccessKey", "defaultSecretAccessKey");
+
+ ProfileFileSupplier supplier = ProfileFileSupplier.reloadWhenModified(credentialsFilePath, ProfileFile.Type.CREDENTIALS);
+ ProfileFile file = supplier.get();
+
+ Optional profileOptional = file.profile("default");
+ assertThat(profileOptional).isPresent();
+
+ assertThat(profileOptional.get()).satisfies(profile -> {
+ Optional awsAccessKeyIdOptional = profile.property("aws_access_key_id");
+ assertThat(awsAccessKeyIdOptional).isPresent();
+ String awsAccessKeyId = awsAccessKeyIdOptional.get();
+ assertThat(awsAccessKeyId).isEqualTo("defaultAccessKey");
+
+ Optional awsSecretAccessKeyOptional = profile.property("aws_secret_access_key");
+ assertThat(awsSecretAccessKeyOptional).isPresent();
+ String awsSecretAccessKey = awsSecretAccessKeyOptional.get();
+ assertThat(awsSecretAccessKey).isEqualTo("defaultSecretAccessKey");
+ });
+ }
+
+ @Test
+ void get_supplierBuiltByFixedProfileFile_returnsProfileFile() {
+ Path credentialsFilePath = generateTestCredentialsFile("defaultAccessKey", "defaultSecretAccessKey");
+
+ ProfileFileSupplier supplier = ProfileFileSupplier.fixedProfileFile(ProfileFile.builder()
+ .content(credentialsFilePath)
+ .type(ProfileFile.Type.CREDENTIALS)
+ .build());
+ ProfileFile file = supplier.get();
+
+ Optional profileOptional = file.profile("default");
+ assertThat(profileOptional).isPresent();
+
+ assertThat(profileOptional.get()).satisfies(profile -> {
+ Optional awsAccessKeyIdOptional = profile.property("aws_access_key_id");
+ assertThat(awsAccessKeyIdOptional).isPresent();
+ String awsAccessKeyId = awsAccessKeyIdOptional.get();
+ assertThat(awsAccessKeyId).isEqualTo("defaultAccessKey");
+
+ Optional awsSecretAccessKeyOptional = profile.property("aws_secret_access_key");
+ assertThat(awsSecretAccessKeyOptional).isPresent();
+ String awsSecretAccessKey = awsSecretAccessKeyOptional.get();
+ assertThat(awsSecretAccessKey).isEqualTo("defaultSecretAccessKey");
+ });
+ }
+
+ @Test
+ void get_supplierBuiltByReloadWhenModifiedAggregate_reloadsCredentials() {
+ Path credentialsFilePath = generateTestCredentialsFile("defaultAccessKey", "defaultSecretAccessKey");
+ Path configFilePath = generateTestConfigFile(Pair.of("region", "us-west-2"));
+
+ ProfileFileSupplier credentialsProfileFileSupplier = ProfileFileSupplier.reloadWhenModified(credentialsFilePath,
+ ProfileFile.Type.CREDENTIALS);
+ ProfileFileSupplier configProfileFileSupplier = ProfileFileSupplier.reloadWhenModified(configFilePath,
+ ProfileFile.Type.CONFIGURATION);
+ ProfileFileSupplier supplier = ProfileFileSupplier.aggregate(credentialsProfileFileSupplier, configProfileFileSupplier);
+
+ Optional fileOptional = supplier.get().profile("default");
+ assertThat(fileOptional).isPresent();
+
+ assertThat(fileOptional.get()).satisfies(profile -> {
+ Optional awsAccessKeyIdOptional = profile.property("aws_access_key_id");
+ assertThat(awsAccessKeyIdOptional).isPresent();
+ String awsAccessKeyId = awsAccessKeyIdOptional.get();
+ assertThat(awsAccessKeyId).isEqualTo("defaultAccessKey");
+
+ Optional awsSecretAccessKeyOptional = profile.property("aws_secret_access_key");
+ assertThat(awsSecretAccessKeyOptional).isPresent();
+ String awsSecretAccessKey = awsSecretAccessKeyOptional.get();
+ assertThat(awsSecretAccessKey).isEqualTo("defaultSecretAccessKey");
+
+ Optional regionOptional = profile.property("region");
+ assertThat(regionOptional).isPresent();
+ String region = regionOptional.get();
+ assertThat(region).isEqualTo("us-west-2");
+ });
+ }
+
+ @Test
+ void get_supplierBuiltByFixedProfileFileAggregate_returnsAggregateProfileFileInstance() {
+ Path credentialsFilePath = generateTestCredentialsFile("defaultAccessKey", "defaultSecretAccessKey");
+ Path configFilePath = generateTestConfigFile(Pair.of("region", "us-west-2"));
+
+ ProfileFileSupplier credentialsProfileFileSupplier
+ = ProfileFileSupplier.fixedProfileFile(ProfileFile.builder()
+ .content(credentialsFilePath)
+ .type(ProfileFile.Type.CREDENTIALS)
+ .build());
+ ProfileFileSupplier configProfileFileSupplier
+ = ProfileFileSupplier.fixedProfileFile(ProfileFile.builder()
+ .content(configFilePath)
+ .type(ProfileFile.Type.CONFIGURATION)
+ .build());
+ ProfileFileSupplier supplier = ProfileFileSupplier.aggregate(credentialsProfileFileSupplier, configProfileFileSupplier);
+ ProfileFile file = supplier.get();
+
+ Optional profileOptional = file.profile("default");
+ assertThat(profileOptional).isPresent();
+
+ assertThat(profileOptional.get()).satisfies(profile -> {
+ Optional awsAccessKeyIdOptional = profile.property("aws_access_key_id");
+ assertThat(awsAccessKeyIdOptional).isPresent();
+ String awsAccessKeyId = awsAccessKeyIdOptional.get();
+ assertThat(awsAccessKeyId).isEqualTo("defaultAccessKey");
+
+ Optional awsSecretAccessKeyOptional = profile.property("aws_secret_access_key");
+ assertThat(awsSecretAccessKeyOptional).isPresent();
+ String awsSecretAccessKey = awsSecretAccessKeyOptional.get();
+ assertThat(awsSecretAccessKey).isEqualTo("defaultSecretAccessKey");
+
+ Optional regionOptional = profile.property("region");
+ assertThat(regionOptional).isPresent();
+ String region = regionOptional.get();
+ assertThat(region).isEqualTo("us-west-2");
+ });
+ }
+
+ @Test
+ void aggregate_supplierReturnsSameInstanceMultipleTimesAggregatingProfileFile_aggregatesOnlyDistinctInstances() {
+ ProfileFile credentialFile1 = credentialFile("test1", "key1", "secret1");
+ ProfileFile credentialFile2 = credentialFile("test2", "key2", "secret2");
+ ProfileFile credentialFile3 = credentialFile("test3", "key3", "secret3");
+ ProfileFile credentialFile4 = credentialFile("test4", "key4", "secret4");
+ ProfileFile configFile = configFile("profile test", Pair.of("region", "us-west-2"));
+
+ List orderedCredentialsFiles
+ = Arrays.asList(credentialFile1, credentialFile1, credentialFile2, credentialFile3, credentialFile3, credentialFile4,
+ credentialFile4, credentialFile4);
+
+ ProfileFile aggregateFile1 = ProfileFile.aggregator().addFile(credentialFile1).addFile(configFile).build();
+ ProfileFile aggregateFile2 = ProfileFile.aggregator().addFile(credentialFile2).addFile(configFile).build();
+ ProfileFile aggregateFile3 = ProfileFile.aggregator().addFile(credentialFile3).addFile(configFile).build();
+ ProfileFile aggregateFile4 = ProfileFile.aggregator().addFile(credentialFile4).addFile(configFile).build();
+
+ List distinctAggregateFiles = Arrays.asList(aggregateFile1, aggregateFile2, aggregateFile3, aggregateFile4);
+
+ ProfileFileSupplier supplier = ProfileFileSupplier.aggregate(supply(orderedCredentialsFiles), () -> configFile);
+
+ List suppliedProfileFiles = Stream.generate(supplier)
+ .limit(orderedCredentialsFiles.size())
+ .filter(uniqueInstances())
+ .collect(Collectors.toList());
+
+ assertThat(suppliedProfileFiles).isEqualTo(distinctAggregateFiles);
+ }
+
+ @Test
+ void aggregate_supplierReturnsSameInstanceMultipleTimesAggregatingProfileFileSupplier_aggregatesOnlyDistinctInstances() {
+ ProfileFile credentialFile1 = credentialFile("test1", "key1", "secret1");
+ ProfileFile credentialFile2 = credentialFile("test2", "key2", "secret2");
+ ProfileFile credentialFile3 = credentialFile("test3", "key3", "secret3");
+ ProfileFile credentialFile4 = credentialFile("test4", "key4", "secret4");
+ ProfileFile configFile1 = configFile("profile test", Pair.of("region", "us-west-1"));
+ ProfileFile configFile2 = configFile("profile test", Pair.of("region", "us-west-2"));
+ ProfileFile configFile3 = configFile("profile test", Pair.of("region", "us-west-3"));
+
+ List orderedCredentialsFiles
+ = Arrays.asList(credentialFile1, credentialFile1, credentialFile2, credentialFile2, credentialFile3,
+ credentialFile4, credentialFile4, credentialFile4);
+
+ List orderedConfigFiles
+ = Arrays.asList(configFile1, configFile1, configFile1, configFile2, configFile3, configFile3, configFile3,
+ configFile3);
+
+ ProfileFile aggregateFile11 = ProfileFile.aggregator().addFile(credentialFile1).addFile(configFile1).build();
+ ProfileFile aggregateFile21 = ProfileFile.aggregator().addFile(credentialFile2).addFile(configFile1).build();
+ ProfileFile aggregateFile22 = ProfileFile.aggregator().addFile(credentialFile2).addFile(configFile2).build();
+ ProfileFile aggregateFile33 = ProfileFile.aggregator().addFile(credentialFile3).addFile(configFile3).build();
+ ProfileFile aggregateFile43 = ProfileFile.aggregator().addFile(credentialFile4).addFile(configFile3).build();
+
+ List aggregateProfileFiles
+ = Arrays.asList(aggregateFile11, aggregateFile11, aggregateFile21, aggregateFile22, aggregateFile33,
+ aggregateFile43, aggregateFile43, aggregateFile43);
+
+ List distinctAggregateProfileFiles
+ = Arrays.asList(aggregateFile11, aggregateFile21, aggregateFile22, aggregateFile33, aggregateFile43);
+
+ ProfileFileSupplier supplier = ProfileFileSupplier.aggregate(supply(orderedCredentialsFiles), supply(orderedConfigFiles));
+
+ List suppliedProfileFiles = Stream.generate(supplier)
+ .filter(Objects::nonNull)
+ .limit(aggregateProfileFiles.size())
+ .filter(uniqueInstances())
+ .collect(Collectors.toList());
+
+ assertThat(suppliedProfileFiles).isEqualTo(distinctAggregateProfileFiles);
+ }
+
+ @Test
+ void fixedProfileFile_nullProfileFile_returnsNonNullSupplier() {
+ ProfileFile file = null;
+ ProfileFileSupplier supplier = ProfileFileSupplier.fixedProfileFile(file);
+
+ assertThat(supplier).isNotNull();
+ }
+
+ @Test
+ void get_givenOnLoadAction_callsActionOncePerNewProfileFile() {
+ int actualProfilesCount = 3;
+ AtomicInteger blockCount = new AtomicInteger();
+
+ Path credentialsFilePath = generateTestCredentialsFile("defaultAccessKey", "defaultSecretAccessKey");
+
+ AdjustableClock clock = new AdjustableClock();
+ ProfileFileSupplier supplier = builderWithClock(clock)
+ .reloadWhenModified(credentialsFilePath, ProfileFile.Type.CREDENTIALS)
+ .onProfileFileLoad(f -> blockCount.incrementAndGet())
+ .build();
+ Duration duration = Duration.ofSeconds(5);
+
+ supplier.get();
+
+ clock.tickForward(duration);
+ supplier.get();
+
+ generateTestCredentialsFile("modifiedAccessKey", "modifiedSecretAccessKey");
+ updateModificationTime(credentialsFilePath, clock.instant().plusMillis(1));
+
+ clock.tickForward(duration);
+ supplier.get();
+
+ generateTestCredentialsFile("updatedAccessKey", "updatedSecretAccessKey");
+ updateModificationTime(credentialsFilePath, clock.instant().plusMillis(1));
+
+ clock.tickForward(duration);
+ supplier.get();
+
+ clock.tickForward(duration);
+ supplier.get();
+
+ assertThat(blockCount.get()).isEqualTo(actualProfilesCount);
+ }
+
+ private Path generateTestFile(String contents, String filename) {
+ try {
+ Files.createDirectories(testDirectory);
+ return Files.write(testDirectory.resolve(filename), contents.getBytes(StandardCharsets.UTF_8));
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ private Path generateTestCredentialsFile(String accessKeyId, String secretAccessKey) {
+ String contents = String.format("[default]\naws_access_key_id = %s\naws_secret_access_key = %s\n",
+ accessKeyId, secretAccessKey);
+ return generateTestFile(contents, "credentials.txt");
+ }
+
+ private Path generateTestConfigFile(Pair
*
*
* 2) Using the low-level client {@link S3Client#utilities()} method. This is recommended as SDK will use the same
@@ -87,7 +87,6 @@
* GetUrlRequest request = GetUrlRequest.builder().bucket("foo-bucket").key("key-without-spaces").build();
* URL url = utilities.getUrl(request);
*
- *
*
* Note: This class does not make network calls.
*/
@@ -112,8 +111,8 @@ public final class S3Utilities {
private S3Utilities(Builder builder) {
this.region = Validate.paramNotNull(builder.region, "Region");
this.endpoint = builder.endpoint;
- this.profileFile = builder.profileFile != null ? () -> builder.profileFile
- : new Lazy<>(ProfileFile::defaultProfileFile)::getValue;
+ this.profileFile = Optional.ofNullable(builder.profileFile)
+ .orElse(ProfileFile::defaultProfileFile);
this.profileName = builder.profileName;
if (builder.s3Configuration == null) {
@@ -172,7 +171,7 @@ static S3Utilities create(SdkClientConfiguration clientConfiguration) {
S3Utilities.Builder builder = builder()
.region(clientConfiguration.option(AwsClientOption.AWS_REGION))
.s3Configuration((S3Configuration) clientConfiguration.option(SdkClientOption.SERVICE_CONFIGURATION))
- .profileFile(clientConfiguration.option(SdkClientOption.PROFILE_FILE))
+ .profileFile(clientConfiguration.option(SdkClientOption.PROFILE_FILE_SUPPLIER))
.profileName(clientConfiguration.option(SdkClientOption.PROFILE_NAME));
if (Boolean.TRUE.equals(clientConfiguration.option(SdkClientOption.ENDPOINT_OVERRIDDEN))) {
@@ -362,7 +361,7 @@ private UseGlobalEndpointResolver createUseGlobalEndpointResolver() {
SdkClientConfiguration config =
SdkClientConfiguration.builder()
.option(ServiceMetadataAdvancedOption.DEFAULT_S3_US_EAST_1_REGIONAL_ENDPOINT, standardOption)
- .option(SdkClientOption.PROFILE_FILE, profileFile.get())
+ .option(SdkClientOption.PROFILE_FILE_SUPPLIER, profileFile)
.option(SdkClientOption.PROFILE_NAME, profileName)
.build();
@@ -377,7 +376,7 @@ public static final class Builder {
private URI endpoint;
private S3Configuration s3Configuration;
- private ProfileFile profileFile;
+ private Supplier profileFile;
private String profileName;
private Boolean dualstackEnabled;
private Boolean fipsEnabled;
@@ -468,7 +467,13 @@ public Builder s3Configuration(S3Configuration s3Configuration) {
* confusing to support the full {@link ClientOverrideConfiguration} object in the future.
*/
private Builder profileFile(ProfileFile profileFile) {
- this.profileFile = profileFile;
+ return profileFile(Optional.ofNullable(profileFile)
+ .map(ProfileFileSupplier::fixedProfileFile)
+ .orElse(null));
+ }
+
+ private Builder profileFile(Supplier profileFileSupplier) {
+ this.profileFile = profileFileSupplier;
return this;
}
diff --git a/services/s3/src/main/java/software/amazon/awssdk/services/s3/internal/endpoints/UseGlobalEndpointResolver.java b/services/s3/src/main/java/software/amazon/awssdk/services/s3/internal/endpoints/UseGlobalEndpointResolver.java
index 568b47c085cd..412fd56d0c91 100644
--- a/services/s3/src/main/java/software/amazon/awssdk/services/s3/internal/endpoints/UseGlobalEndpointResolver.java
+++ b/services/s3/src/main/java/software/amazon/awssdk/services/s3/internal/endpoints/UseGlobalEndpointResolver.java
@@ -44,7 +44,7 @@ public UseGlobalEndpointResolver(SdkClientConfiguration config) {
String defaultS3UsEast1RegionalEndpointFromSmartDefaults =
config.option(ServiceMetadataAdvancedOption.DEFAULT_S3_US_EAST_1_REGIONAL_ENDPOINT);
this.useUsEast1RegionalEndpoint =
- new Lazy<>(() -> useUsEast1RegionalEndpoint(() -> config.option(SdkClientOption.PROFILE_FILE),
+ new Lazy<>(() -> useUsEast1RegionalEndpoint(config.option(SdkClientOption.PROFILE_FILE_SUPPLIER),
() -> config.option(SdkClientOption.PROFILE_NAME),
defaultS3UsEast1RegionalEndpointFromSmartDefaults));
}
diff --git a/services/s3/src/main/java/software/amazon/awssdk/services/s3/internal/settingproviders/DisableMultiRegionProviderChain.java b/services/s3/src/main/java/software/amazon/awssdk/services/s3/internal/settingproviders/DisableMultiRegionProviderChain.java
index 9b5fb603054e..cda136d92edd 100644
--- a/services/s3/src/main/java/software/amazon/awssdk/services/s3/internal/settingproviders/DisableMultiRegionProviderChain.java
+++ b/services/s3/src/main/java/software/amazon/awssdk/services/s3/internal/settingproviders/DisableMultiRegionProviderChain.java
@@ -18,6 +18,7 @@
import java.util.Arrays;
import java.util.List;
import java.util.Optional;
+import java.util.function.Supplier;
import software.amazon.awssdk.annotations.SdkInternalApi;
import software.amazon.awssdk.profiles.ProfileFile;
import software.amazon.awssdk.profiles.ProfileFileSystemSetting;
@@ -50,7 +51,7 @@ private DisableMultiRegionProviderChain(List provide
*
*/
public static DisableMultiRegionProviderChain create() {
- return create(ProfileFile.defaultProfileFile(),
+ return create(ProfileFile::defaultProfileFile,
ProfileFileSystemSetting.AWS_PROFILE.getStringValueOrThrow());
}
@@ -60,6 +61,12 @@ public static DisableMultiRegionProviderChain create(ProfileFile profileFile, St
ProfileDisableMultiRegionProvider.create(profileFile, profileName)));
}
+ public static DisableMultiRegionProviderChain create(Supplier profileFile, String profileName) {
+ return new DisableMultiRegionProviderChain(Arrays.asList(
+ SystemsSettingsDisableMultiRegionProvider.create(),
+ ProfileDisableMultiRegionProvider.create(profileFile, profileName)));
+ }
+
@Override
public Optional resolve() {
for (DisableMultiRegionProvider provider : providers) {
diff --git a/services/s3/src/main/java/software/amazon/awssdk/services/s3/internal/settingproviders/ProfileDisableMultiRegionProvider.java b/services/s3/src/main/java/software/amazon/awssdk/services/s3/internal/settingproviders/ProfileDisableMultiRegionProvider.java
index 4316baebda93..77889e7330cf 100644
--- a/services/s3/src/main/java/software/amazon/awssdk/services/s3/internal/settingproviders/ProfileDisableMultiRegionProvider.java
+++ b/services/s3/src/main/java/software/amazon/awssdk/services/s3/internal/settingproviders/ProfileDisableMultiRegionProvider.java
@@ -49,6 +49,10 @@ public static ProfileDisableMultiRegionProvider create(ProfileFile profileFile,
return new ProfileDisableMultiRegionProvider(() -> profileFile, profileName);
}
+ public static ProfileDisableMultiRegionProvider create(Supplier profileFile, String profileName) {
+ return new ProfileDisableMultiRegionProvider(profileFile, profileName);
+ }
+
@Override
public Optional resolve() {
return profileFile.get()
diff --git a/services/s3/src/main/java/software/amazon/awssdk/services/s3/internal/settingproviders/ProfileUseArnRegionProvider.java b/services/s3/src/main/java/software/amazon/awssdk/services/s3/internal/settingproviders/ProfileUseArnRegionProvider.java
index 23f54d98b018..8f35eebacf74 100644
--- a/services/s3/src/main/java/software/amazon/awssdk/services/s3/internal/settingproviders/ProfileUseArnRegionProvider.java
+++ b/services/s3/src/main/java/software/amazon/awssdk/services/s3/internal/settingproviders/ProfileUseArnRegionProvider.java
@@ -28,7 +28,7 @@
@SdkInternalApi
public final class ProfileUseArnRegionProvider implements UseArnRegionProvider {
/**
- * Property name for specifying whether or not use arn region should be enabled.
+ * Property name for specifying whether or not to use arn region should be enabled.
*/
private static final String AWS_USE_ARN_REGION = "s3_use_arn_region";
@@ -49,6 +49,10 @@ public static ProfileUseArnRegionProvider create(ProfileFile profileFile, String
return new ProfileUseArnRegionProvider(() -> profileFile, profileName);
}
+ public static ProfileUseArnRegionProvider create(Supplier profileFile, String profileName) {
+ return new ProfileUseArnRegionProvider(profileFile, profileName);
+ }
+
@Override
public Optional resolveUseArnRegion() {
return profileFile.get()
diff --git a/services/s3/src/main/java/software/amazon/awssdk/services/s3/internal/settingproviders/UseArnRegionProviderChain.java b/services/s3/src/main/java/software/amazon/awssdk/services/s3/internal/settingproviders/UseArnRegionProviderChain.java
index a64fc440c354..d418d496d17b 100644
--- a/services/s3/src/main/java/software/amazon/awssdk/services/s3/internal/settingproviders/UseArnRegionProviderChain.java
+++ b/services/s3/src/main/java/software/amazon/awssdk/services/s3/internal/settingproviders/UseArnRegionProviderChain.java
@@ -18,6 +18,7 @@
import java.util.Arrays;
import java.util.List;
import java.util.Optional;
+import java.util.function.Supplier;
import software.amazon.awssdk.annotations.SdkInternalApi;
import software.amazon.awssdk.profiles.ProfileFile;
import software.amazon.awssdk.profiles.ProfileFileSystemSetting;
@@ -49,7 +50,7 @@ private UseArnRegionProviderChain(List providers) {
*
*/
public static UseArnRegionProviderChain create() {
- return create(ProfileFile.defaultProfileFile(),
+ return create(ProfileFile::defaultProfileFile,
ProfileFileSystemSetting.AWS_PROFILE.getStringValueOrThrow());
}
@@ -58,6 +59,11 @@ public static UseArnRegionProviderChain create(ProfileFile profileFile, String p
ProfileUseArnRegionProvider.create(profileFile, profileName)));
}
+ public static UseArnRegionProviderChain create(Supplier profileFile, String profileName) {
+ return new UseArnRegionProviderChain(Arrays.asList(SystemsSettingsUseArnRegionProvider.create(),
+ ProfileUseArnRegionProvider.create(profileFile, profileName)));
+ }
+
@Override
public Optional resolveUseArnRegion() {
for (UseArnRegionProvider provider : providers) {
diff --git a/services/s3/src/main/java/software/amazon/awssdk/services/s3/internal/signing/DefaultS3Presigner.java b/services/s3/src/main/java/software/amazon/awssdk/services/s3/internal/signing/DefaultS3Presigner.java
index 73356356bdef..ec79bb132ffe 100644
--- a/services/s3/src/main/java/software/amazon/awssdk/services/s3/internal/signing/DefaultS3Presigner.java
+++ b/services/s3/src/main/java/software/amazon/awssdk/services/s3/internal/signing/DefaultS3Presigner.java
@@ -129,7 +129,7 @@ private DefaultS3Presigner(Builder b) {
S3Configuration serviceConfiguration = b.serviceConfiguration != null ? b.serviceConfiguration :
S3Configuration.builder()
- .profileFile(profileFile())
+ .profileFile(profileFileSupplier())
.profileName(profileName())
.checksumValidationEnabled(false)
.build();
@@ -214,7 +214,7 @@ private SdkClientConfiguration createClientConfiguration() {
} else {
URI defaultEndpoint = new DefaultServiceEndpointBuilder(SERVICE_NAME, "https")
.withRegion(region())
- .withProfileFile(this::profileFile)
+ .withProfileFile(profileFileSupplier())
.withProfileName(profileName())
.withDualstackEnabled(serviceConfiguration.dualstackEnabled())
.withFipsEnabled(fipsEnabled())
@@ -533,7 +533,7 @@ private UseGlobalEndpointResolver createUseGlobalEndpointResolver() {
SdkClientConfiguration config = clientConfiguration.toBuilder()
.option(ServiceMetadataAdvancedOption.DEFAULT_S3_US_EAST_1_REGIONAL_ENDPOINT, legacyOption)
- .option(SdkClientOption.PROFILE_FILE, profileFile())
+ .option(SdkClientOption.PROFILE_FILE_SUPPLIER, profileFileSupplier())
.option(SdkClientOption.PROFILE_NAME, profileName())
.build();
diff --git a/services/s3/src/main/java/software/amazon/awssdk/services/s3/internal/signing/DefaultSdkPresigner.java b/services/s3/src/main/java/software/amazon/awssdk/services/s3/internal/signing/DefaultSdkPresigner.java
index 4b8a78ed44ed..b6df6e29fe7b 100644
--- a/services/s3/src/main/java/software/amazon/awssdk/services/s3/internal/signing/DefaultSdkPresigner.java
+++ b/services/s3/src/main/java/software/amazon/awssdk/services/s3/internal/signing/DefaultSdkPresigner.java
@@ -16,6 +16,7 @@
package software.amazon.awssdk.services.s3.internal.signing;
import java.net.URI;
+import java.util.function.Supplier;
import software.amazon.awssdk.annotations.SdkInternalApi;
import software.amazon.awssdk.auth.credentials.AwsCredentialsProvider;
import software.amazon.awssdk.auth.credentials.DefaultCredentialsProvider;
@@ -30,14 +31,14 @@
/**
* The base class implementing the {@link SdkPresigner} interface.
- *
+ *