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-lang3 test + + 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: *

    - *
  1. Java System Properties - aws.accessKeyId and aws.secretAccessKey
  2. - *
  3. Environment Variables - AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY
  4. + *
  5. Java System Properties - {@code aws.accessKeyId} and {@code aws.secretAccessKey}
  6. + *
  7. Environment Variables - {@code AWS_ACCESS_KEY_ID} and {@code AWS_SECRET_ACCESS_KEY}
  8. *
  9. Web Identity Token credentials from system properties or environment variables
  10. *
  11. Credential profiles file at the default location (~/.aws/credentials) shared by all AWS SDKs and the AWS CLI
  12. *
  13. 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-core test + + 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... 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("[default]\n%s", values); + + return generateTestFile(contents, "config.txt"); + } + + private void updateModificationTime(Path path, Instant instant) { + try { + Files.setLastModifiedTime(path, FileTime.from(instant)); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + 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 ProfileFile configFile(String credentialFile) { + return ProfileFile.builder() + .content(new StringInputStream(credentialFile)) + .type(ProfileFile.Type.CONFIGURATION) + .build(); + } + + 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); + } + + private static Predicate uniqueInstances() { + Set seen = ConcurrentHashMap.newKeySet(); + return e -> { + boolean unique = seen.stream().noneMatch(o -> o == e); + if (unique) { + seen.add(e); + } + + return unique; + }; + } + + private static ProfileFileSupplier supply(Iterable iterable) { + return iterable.iterator()::next; + } + + private ProfileFileSupplierBuilder builder() { + return new ProfileFileSupplierBuilder(); + } + + private ProfileFileSupplierBuilder builderWithClock(Clock clock) { + return new ProfileFileSupplierBuilder().clock(clock); + } + + private static final class AdjustableClock extends Clock { + private Instant time; + + private AdjustableClock() { + this.time = Instant.now(); + } + + @Override + public ZoneId getZone() { + return ZoneOffset.UTC; + } + + @Override + public Clock withZone(ZoneId zone) { + throw new UnsupportedOperationException(); + } + + @Override + public Instant instant() { + return time; + } + + public void tickForward(TemporalAmount amount) { + time = time.plus(amount); + } + + public void tickBackward(TemporalAmount amount) { + time = time.minus(amount); + } + } +} \ No newline at end of file diff --git a/core/profiles/src/test/java/software/amazon/awssdk/profiles/internal/ProfileFileRefresherTest.java b/core/profiles/src/test/java/software/amazon/awssdk/profiles/internal/ProfileFileRefresherTest.java new file mode 100644 index 000000000000..24755e1cc9a7 --- /dev/null +++ b/core/profiles/src/test/java/software/amazon/awssdk/profiles/internal/ProfileFileRefresherTest.java @@ -0,0 +1,319 @@ +/* + * 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 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.concurrent.atomic.AtomicInteger; +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import software.amazon.awssdk.profiles.ProfileFile; +import software.amazon.awssdk.utils.StringInputStream; + +public class ProfileFileRefresherTest { + + 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 refreshIfStale_profileModifiedNoPathSpecified_doesNotReloadProfileFile() { + Path credentialsFilePath = generateTestCredentialsFile("defaultAccessKey", "defaultSecretAccessKey"); + + AdjustableClock clock = new AdjustableClock(); + ProfileFileRefresher refresher = refresherWithClock(clock) + .profileFile(() -> profileFile(credentialsFilePath)) + .build(); + Duration intervalWithinJitter = Duration.ofMillis(100); + + ProfileFile file1 = refresher.refreshIfStale(); + + generateTestCredentialsFile("modifiedAccessKey", "modifiedSecretAccessKey"); + updateModificationTime(credentialsFilePath, clock.instant().plusMillis(1)); + + clock.tickForward(intervalWithinJitter); + ProfileFile file2 = refresher.refreshIfStale(); + + Assertions.assertThat(file2).isSameAs(file1); + } + + @Test + void refreshIfStale_profileModifiedWithinJitterPeriod_doesNotReloadProfileFile() { + Path credentialsFilePath = generateTestCredentialsFile("defaultAccessKey", "defaultSecretAccessKey"); + + AdjustableClock clock = new AdjustableClock(); + ProfileFileRefresher refresher = refresherWithClock(clock) + .profileFile(() -> profileFile(credentialsFilePath)) + .profileFilePath(credentialsFilePath) + .build(); + Duration intervalWithinJitter = Duration.ofMillis(100); + + ProfileFile file1 = refresher.refreshIfStale(); + + clock.tickForward(intervalWithinJitter); + generateTestCredentialsFile("modifiedAccessKey", "modifiedSecretAccessKey"); + updateModificationTime(credentialsFilePath, clock.instant()); + + ProfileFile file2 = refresher.refreshIfStale(); + + Assertions.assertThat(file2).isSameAs(file1); + } + + @Test + void refreshIfStale_profileModifiedOutsideJitterPeriod_reloadsProfileFile() { + Path credentialsFilePath = generateTestCredentialsFile("defaultAccessKey", "defaultSecretAccessKey"); + + AdjustableClock clock = new AdjustableClock(); + ProfileFileRefresher refresher = refresherWithClock(clock) + .profileFile(() -> profileFile(credentialsFilePath)) + .profileFilePath(credentialsFilePath) + .build(); + Duration intervalOutsideJitter = Duration.ofMillis(1_000); + + ProfileFile file1 = refresher.refreshIfStale(); + + clock.tickForward(intervalOutsideJitter); + generateTestCredentialsFile("modifiedAccessKey", "modifiedSecretAccessKey"); + updateModificationTime(credentialsFilePath, clock.instant()); + + ProfileFile file2 = refresher.refreshIfStale(); + + Assertions.assertThat(file2).isNotSameAs(file1); + } + + @Test + void refreshIfStale_profileModified_reloadsProfileFile() { + Path credentialsFilePath = generateTestCredentialsFile("defaultAccessKey", "defaultSecretAccessKey"); + + AdjustableClock clock = new AdjustableClock(); + ProfileFileRefresher refresher = refresherWithClock(clock) + .profileFile(() -> profileFile(credentialsFilePath)) + .profileFilePath(credentialsFilePath) + .build(); + + Duration refreshInterval = Duration.ofSeconds(15); + + ProfileFile file1 = refresher.refreshIfStale(); + + generateTestCredentialsFile("modifiedAccessKey", "modifiedSecretAccessKey"); + updateModificationTime(credentialsFilePath, clock.instant().plusMillis(1)); + + clock.tickForward(refreshInterval.plusSeconds(10)); + ProfileFile file2 = refresher.refreshIfStale(); + + Assertions.assertThat(file2).isNotSameAs(file1); + } + + @Test + void refreshIfStale_profileModifiedOnceButRefreshedMultipleTimes_reloadsProfileFileOnce() { + Path credentialsFilePath = generateTestCredentialsFile("defaultAccessKey", "defaultSecretAccessKey"); + + AdjustableClock clock = new AdjustableClock(); + ProfileFileRefresher refresher = refresherWithClock(clock) + .profileFile(() -> profileFile(credentialsFilePath)) + .profileFilePath(credentialsFilePath) + .build(); + ProfileFile file1 = refresher.refreshIfStale(); + + clock.tickForward(Duration.ofSeconds(5)); + ProfileFile file2 = refresher.refreshIfStale(); + + generateTestCredentialsFile("modifiedAccessKey", "modifiedSecretAccessKey"); + updateModificationTime(credentialsFilePath, clock.instant().plusMillis(1)); + + clock.tickForward(Duration.ofSeconds(5)); + ProfileFile file3 = refresher.refreshIfStale(); + + Assertions.assertThat(file2).isSameAs(file1); + Assertions.assertThat(file3).isNotSameAs(file2); + } + + @Test + void refreshIfStale_profileModifiedMultipleTimes_reloadsProfileFileOncePerChange() { + Path credentialsFilePath = generateTestCredentialsFile("defaultAccessKey", "defaultSecretAccessKey"); + + AdjustableClock clock = new AdjustableClock(); + ProfileFileRefresher refresher = refresherWithClock(clock) + .profileFile(() -> profileFile(credentialsFilePath)) + .profileFilePath(credentialsFilePath) + .build(); + Duration duration = Duration.ofSeconds(5); + + ProfileFile file1 = refresher.refreshIfStale(); + + clock.tickForward(duration); + ProfileFile file2 = refresher.refreshIfStale(); + + generateTestCredentialsFile("modifiedAccessKey", "modifiedSecretAccessKey"); + updateModificationTime(credentialsFilePath, clock.instant().plusMillis(1)); + + clock.tickForward(duration); + ProfileFile file3 = refresher.refreshIfStale(); + + generateTestCredentialsFile("updatedAccessKey", "updatedSecretAccessKey"); + updateModificationTime(credentialsFilePath, clock.instant().plusMillis(1)); + + clock.tickForward(duration); + ProfileFile file4 = refresher.refreshIfStale(); + + clock.tickForward(duration); + ProfileFile file5 = refresher.refreshIfStale(); + + Assertions.assertThat(file2).isSameAs(file1); + Assertions.assertThat(file3).isNotSameAs(file2); + Assertions.assertThat(file4).isNotSameAs(file3); + } + + @Test + void refreshIfStale_givenOnReloadConsumer_callsConsumerOncePerChange() { + int actualRefreshOperations = 3; + AtomicInteger refreshOperationsCounter = new AtomicInteger(); + + Path credentialsFilePath = generateTestCredentialsFile("defaultAccessKey", "defaultSecretAccessKey"); + + AdjustableClock clock = new AdjustableClock(); + ProfileFileRefresher refresher = refresherWithClock(clock) + .profileFile(() -> profileFile(credentialsFilePath)) + .profileFilePath(credentialsFilePath) + .onProfileFileReload(f -> refreshOperationsCounter.incrementAndGet()) + .build(); + Duration duration = Duration.ofSeconds(5); + + ProfileFile file1 = refresher.refreshIfStale(); + + clock.tickForward(duration); + ProfileFile file2 = refresher.refreshIfStale(); + + generateTestCredentialsFile("modifiedAccessKey", "modifiedSecretAccessKey"); + updateModificationTime(credentialsFilePath, clock.instant().plusMillis(1)); + + clock.tickForward(duration); + ProfileFile file3 = refresher.refreshIfStale(); + + generateTestCredentialsFile("updatedAccessKey", "updatedSecretAccessKey"); + updateModificationTime(credentialsFilePath, clock.instant().plusMillis(1)); + + clock.tickForward(duration); + ProfileFile file4 = refresher.refreshIfStale(); + + clock.tickForward(duration); + ProfileFile file5 = refresher.refreshIfStale(); + + Assertions.assertThat(file2).isSameAs(file1); + Assertions.assertThat(file3).isNotSameAs(file2); + Assertions.assertThat(file4).isNotSameAs(file3); + Assertions.assertThat(file5).isSameAs(file4); + + Assertions.assertThat(refreshOperationsCounter.get()).isEqualTo(actualRefreshOperations); + } + + private ProfileFile credentialFile(String credentialFile) { + return ProfileFile.builder() + .content(new StringInputStream(credentialFile)) + .type(ProfileFile.Type.CREDENTIALS) + .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"); + } + + private void updateModificationTime(Path path, Instant instant) { + try { + Files.setLastModifiedTime(path, FileTime.from(instant)); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + private ProfileFile profileFile(Path path) { + return ProfileFile.builder().content(path).type(ProfileFile.Type.CREDENTIALS).build(); + } + + private ProfileFileRefresher.Builder refresherWithClock(Clock clock) { + return ProfileFileRefresher.builder() + .clock(clock); + } + + private static final class AdjustableClock extends Clock { + private Instant time; + + private AdjustableClock() { + this.time = Instant.now(); + } + + @Override + public ZoneId getZone() { + return ZoneOffset.UTC; + } + + @Override + public Clock withZone(ZoneId zone) { + throw new UnsupportedOperationException(); + } + + @Override + public Instant instant() { + return time; + } + + public void tickForward(TemporalAmount amount) { + time = time.plus(amount); + } + + public void tickBackward(TemporalAmount amount) { + time = time.minus(amount); + } + } +} diff --git a/core/sdk-core/src/main/java/software/amazon/awssdk/core/client/builder/SdkDefaultClientBuilder.java b/core/sdk-core/src/main/java/software/amazon/awssdk/core/client/builder/SdkDefaultClientBuilder.java index 9b68e9f9f5dd..62fa6a368f5c 100644 --- a/core/sdk-core/src/main/java/software/amazon/awssdk/core/client/builder/SdkDefaultClientBuilder.java +++ b/core/sdk-core/src/main/java/software/amazon/awssdk/core/client/builder/SdkDefaultClientBuilder.java @@ -36,6 +36,7 @@ import static software.amazon.awssdk.core.client.config.SdkClientOption.INTERNAL_USER_AGENT; import static software.amazon.awssdk.core.client.config.SdkClientOption.METRIC_PUBLISHERS; import static software.amazon.awssdk.core.client.config.SdkClientOption.PROFILE_FILE; +import static software.amazon.awssdk.core.client.config.SdkClientOption.PROFILE_FILE_SUPPLIER; import static software.amazon.awssdk.core.client.config.SdkClientOption.PROFILE_NAME; import static software.amazon.awssdk.core.client.config.SdkClientOption.RETRY_POLICY; import static software.amazon.awssdk.core.client.config.SdkClientOption.SCHEDULED_EXECUTOR_SERVICE; @@ -86,6 +87,7 @@ import software.amazon.awssdk.http.async.SdkAsyncHttpClient; import software.amazon.awssdk.metrics.MetricPublisher; import software.amazon.awssdk.profiles.ProfileFile; +import software.amazon.awssdk.profiles.ProfileFileSupplier; import software.amazon.awssdk.profiles.ProfileFileSystemSetting; import software.amazon.awssdk.utils.AttributeMap; import software.amazon.awssdk.utils.Either; @@ -230,7 +232,9 @@ private SdkClientConfiguration setOverrides(SdkClientConfiguration configuration builder.option(API_CALL_ATTEMPT_TIMEOUT, clientOverrideConfiguration.apiCallAttemptTimeout().orElse(null)); builder.option(DISABLE_HOST_PREFIX_INJECTION, clientOverrideConfiguration.advancedOption(DISABLE_HOST_PREFIX_INJECTION).orElse(null)); - builder.option(PROFILE_FILE, clientOverrideConfiguration.defaultProfileFile().orElse(null)); + builder.option(PROFILE_FILE_SUPPLIER, clientOverrideConfiguration.defaultProfileFile() + .map(ProfileFileSupplier::fixedProfileFile) + .orElse(null)); builder.option(PROFILE_NAME, clientOverrideConfiguration.defaultProfileName().orElse(null)); builder.option(METRIC_PUBLISHERS, clientOverrideConfiguration.metricPublishers()); builder.option(EXECUTION_ATTRIBUTES, clientOverrideConfiguration.executionAttributes()); @@ -260,12 +264,14 @@ protected SdkClientConfiguration mergeChildDefaults(SdkClientConfiguration confi */ private SdkClientConfiguration mergeGlobalDefaults(SdkClientConfiguration configuration) { // Don't load the default profile file if the customer already gave us one. - ProfileFile configuredProfileFile = configuration.option(PROFILE_FILE); - ProfileFile profileFile = configuredProfileFile != null ? configuredProfileFile : ProfileFile.defaultProfileFile(); + Supplier configuredProfileFileSupplier = configuration.option(PROFILE_FILE_SUPPLIER); + Supplier profileFileSupplier = Optional.ofNullable(configuredProfileFileSupplier) + .orElseGet(() -> ProfileFile::defaultProfileFile); return configuration.merge(c -> c.option(EXECUTION_INTERCEPTORS, new ArrayList<>()) .option(ADDITIONAL_HTTP_HEADERS, new LinkedHashMap<>()) - .option(PROFILE_FILE, profileFile) + .option(PROFILE_FILE, profileFileSupplier.get()) + .option(PROFILE_FILE_SUPPLIER, profileFileSupplier) .option(PROFILE_NAME, ProfileFileSystemSetting.AWS_PROFILE.getStringValueOrThrow()) .option(USER_AGENT_PREFIX, SdkUserAgent.create().userAgent()) .option(USER_AGENT_SUFFIX, "") @@ -330,7 +336,7 @@ private RetryPolicy resolveRetryPolicy(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(); diff --git a/core/sdk-core/src/main/java/software/amazon/awssdk/core/client/config/SdkClientOption.java b/core/sdk-core/src/main/java/software/amazon/awssdk/core/client/config/SdkClientOption.java index 8e056a7dabd5..07361d75f23d 100644 --- a/core/sdk-core/src/main/java/software/amazon/awssdk/core/client/config/SdkClientOption.java +++ b/core/sdk-core/src/main/java/software/amazon/awssdk/core/client/config/SdkClientOption.java @@ -20,6 +20,7 @@ import java.util.List; import java.util.Map; import java.util.concurrent.ScheduledExecutorService; +import java.util.function.Supplier; import software.amazon.awssdk.annotations.SdkProtectedApi; import software.amazon.awssdk.core.ClientType; import software.amazon.awssdk.core.ServiceConfiguration; @@ -126,9 +127,23 @@ public final class SdkClientOption extends ClientOption { /** * The profile file to use for this client. - */ + * + * @deprecated This option was used to: + * - Read configuration options in profile files in aws-core, sdk-core + * - Build service configuration objects from profile files in codegen, s3control + * - Build service configuration objects from profile files, set endpoint options in s3 + * - Set retry mode in dynamodb, kinesis + * This has been replaced with {@code PROFILE_FILE_SUPPLIER.get()}. + */ + @Deprecated public static final SdkClientOption PROFILE_FILE = new SdkClientOption<>(ProfileFile.class); + /** + * The profile file supplier to use for this client. + */ + public static final SdkClientOption> PROFILE_FILE_SUPPLIER = + new SdkClientOption<>(new UnsafeValueType(Supplier.class)); + /** * The profile name to use for this client. */ diff --git a/core/sdk-core/src/main/java/software/amazon/awssdk/core/endpointdiscovery/providers/DefaultEndpointDiscoveryProviderChain.java b/core/sdk-core/src/main/java/software/amazon/awssdk/core/endpointdiscovery/providers/DefaultEndpointDiscoveryProviderChain.java index 8c814799998d..fd8e086e2f3d 100644 --- a/core/sdk-core/src/main/java/software/amazon/awssdk/core/endpointdiscovery/providers/DefaultEndpointDiscoveryProviderChain.java +++ b/core/sdk-core/src/main/java/software/amazon/awssdk/core/endpointdiscovery/providers/DefaultEndpointDiscoveryProviderChain.java @@ -30,7 +30,7 @@ public DefaultEndpointDiscoveryProviderChain() { } public DefaultEndpointDiscoveryProviderChain(SdkClientConfiguration clientConfiguration) { - this(() -> clientConfiguration.option(SdkClientOption.PROFILE_FILE), + this(clientConfiguration.option(SdkClientOption.PROFILE_FILE_SUPPLIER), clientConfiguration.option(SdkClientOption.PROFILE_NAME)); } diff --git a/core/sdk-core/src/main/java/software/amazon/awssdk/core/interceptor/SdkExecutionAttribute.java b/core/sdk-core/src/main/java/software/amazon/awssdk/core/interceptor/SdkExecutionAttribute.java index 9d42d033f677..94f78e64a0df 100644 --- a/core/sdk-core/src/main/java/software/amazon/awssdk/core/interceptor/SdkExecutionAttribute.java +++ b/core/sdk-core/src/main/java/software/amazon/awssdk/core/interceptor/SdkExecutionAttribute.java @@ -16,6 +16,7 @@ package software.amazon.awssdk.core.interceptor; import java.net.URI; +import java.util.function.Supplier; import software.amazon.awssdk.annotations.SdkPublicApi; import software.amazon.awssdk.core.ClientType; import software.amazon.awssdk.core.ServiceConfiguration; @@ -75,8 +76,17 @@ public class SdkExecutionAttribute { */ public static final ExecutionAttribute SIGNER_OVERRIDDEN = new ExecutionAttribute<>("SignerOverridden"); + /** + * @deprecated This attribute is used for: + * - Set profile file of service endpoint builder docdb, nepture, rds + * This has been replaced with {@code PROFILE_FILE_SUPPLIER.get()}. + */ + @Deprecated public static final ExecutionAttribute PROFILE_FILE = new ExecutionAttribute<>("ProfileFile"); + public static final ExecutionAttribute> PROFILE_FILE_SUPPLIER = + new ExecutionAttribute<>("ProfileFileSupplier"); + public static final ExecutionAttribute PROFILE_NAME = new ExecutionAttribute<>("ProfileName"); /** diff --git a/core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/handler/BaseClientHandler.java b/core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/handler/BaseClientHandler.java index e6a493d69ae8..21421eeb911a 100644 --- a/core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/handler/BaseClientHandler.java +++ b/core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/handler/BaseClientHandler.java @@ -200,7 +200,11 @@ private static InterceptorContext runModifyHttpRequestAndHttpContentInterceptors .putAttribute(SdkExecutionAttribute.SERVICE_CONFIG, clientConfiguration.option(SdkClientOption.SERVICE_CONFIGURATION)) .putAttribute(SdkExecutionAttribute.SERVICE_NAME, clientConfiguration.option(SdkClientOption.SERVICE_NAME)) - .putAttribute(SdkExecutionAttribute.PROFILE_FILE, clientConfiguration.option(SdkClientOption.PROFILE_FILE)) + .putAttribute(SdkExecutionAttribute.PROFILE_FILE, + clientConfiguration.option(SdkClientOption.PROFILE_FILE_SUPPLIER) != null ? + clientConfiguration.option(SdkClientOption.PROFILE_FILE_SUPPLIER).get() : null) + .putAttribute(SdkExecutionAttribute.PROFILE_FILE_SUPPLIER, + clientConfiguration.option(SdkClientOption.PROFILE_FILE_SUPPLIER)) .putAttribute(SdkExecutionAttribute.PROFILE_NAME, clientConfiguration.option(SdkClientOption.PROFILE_NAME)); ExecutionInterceptorChain interceptorChain = diff --git a/core/sdk-core/src/test/java/software/amazon/awssdk/core/client/builder/DefaultClientBuilderTest.java b/core/sdk-core/src/test/java/software/amazon/awssdk/core/client/builder/DefaultClientBuilderTest.java index d8f8dcb0f921..8bb2455099ba 100644 --- a/core/sdk-core/src/test/java/software/amazon/awssdk/core/client/builder/DefaultClientBuilderTest.java +++ b/core/sdk-core/src/test/java/software/amazon/awssdk/core/client/builder/DefaultClientBuilderTest.java @@ -35,6 +35,7 @@ import static software.amazon.awssdk.core.client.config.SdkClientOption.EXECUTION_INTERCEPTORS; import static software.amazon.awssdk.core.client.config.SdkClientOption.METRIC_PUBLISHERS; import static software.amazon.awssdk.core.client.config.SdkClientOption.PROFILE_FILE; +import static software.amazon.awssdk.core.client.config.SdkClientOption.PROFILE_FILE_SUPPLIER; import static software.amazon.awssdk.core.client.config.SdkClientOption.PROFILE_NAME; import static software.amazon.awssdk.core.client.config.SdkClientOption.RETRY_POLICY; import static software.amazon.awssdk.core.internal.SdkInternalTestAdvancedClientOption.ENDPOINT_OVERRIDDEN_OVERRIDE; @@ -59,12 +60,11 @@ import org.mockito.junit.MockitoJUnitRunner; import software.amazon.awssdk.core.client.config.ClientOverrideConfiguration; import software.amazon.awssdk.core.client.config.SdkAdvancedClientOption; -import software.amazon.awssdk.core.client.config.SdkClientOption; import software.amazon.awssdk.core.client.config.SdkClientConfiguration; +import software.amazon.awssdk.core.client.config.SdkClientOption; import software.amazon.awssdk.core.interceptor.ExecutionAttribute; import software.amazon.awssdk.core.interceptor.ExecutionAttributes; import software.amazon.awssdk.core.interceptor.ExecutionInterceptor; -import software.amazon.awssdk.core.retry.RetryMode; import software.amazon.awssdk.core.retry.RetryPolicy; import software.amazon.awssdk.core.signer.NoOpSigner; import software.amazon.awssdk.core.signer.Signer; @@ -261,6 +261,7 @@ public void close() { assertThat(config.option(API_CALL_ATTEMPT_TIMEOUT)).isEqualTo(apiCallAttemptTimeout); assertThat(config.option(DISABLE_HOST_PREFIX_INJECTION)).isEqualTo(Boolean.TRUE); assertThat(config.option(PROFILE_FILE)).isEqualTo(profileFile); + assertThat(config.option(PROFILE_FILE_SUPPLIER).get()).isEqualTo(profileFile); assertThat(config.option(PROFILE_NAME)).isEqualTo(profileName); assertThat(config.option(METRIC_PUBLISHERS)).contains(metricPublisher); assertThat(config.option(EXECUTION_ATTRIBUTES).getAttribute(execAttribute)).isEqualTo("value"); diff --git a/services/docdb/src/main/java/software/amazon/awssdk/services/docdb/internal/RdsPresignInterceptor.java b/services/docdb/src/main/java/software/amazon/awssdk/services/docdb/internal/RdsPresignInterceptor.java index 24bb975020db..5dd3248fe31f 100644 --- a/services/docdb/src/main/java/software/amazon/awssdk/services/docdb/internal/RdsPresignInterceptor.java +++ b/services/docdb/src/main/java/software/amazon/awssdk/services/docdb/internal/RdsPresignInterceptor.java @@ -162,7 +162,7 @@ private URI createEndpoint(String regionName, String serviceName, ExecutionAttri return new DefaultServiceEndpointBuilder(SERVICE_NAME, Protocol.HTTPS.toString()) .withRegion(region) - .withProfileFile(() -> attributes.getAttribute(SdkExecutionAttribute.PROFILE_FILE)) + .withProfileFile(attributes.getAttribute(SdkExecutionAttribute.PROFILE_FILE_SUPPLIER)) .withProfileName(attributes.getAttribute(SdkExecutionAttribute.PROFILE_NAME)) .withDualstackEnabled(attributes.getAttribute(AwsExecutionAttribute.DUALSTACK_ENDPOINT_ENABLED)) .withFipsEnabled(attributes.getAttribute(AwsExecutionAttribute.FIPS_ENDPOINT_ENABLED)) diff --git a/services/docdb/src/test/java/software/amazon/awssdk/services/docdb/internal/PresignRequestHandlerTest.java b/services/docdb/src/test/java/software/amazon/awssdk/services/docdb/internal/PresignRequestHandlerTest.java index 4ffb5f70142f..1a261fc2ebdd 100644 --- a/services/docdb/src/test/java/software/amazon/awssdk/services/docdb/internal/PresignRequestHandlerTest.java +++ b/services/docdb/src/test/java/software/amazon/awssdk/services/docdb/internal/PresignRequestHandlerTest.java @@ -164,7 +164,8 @@ private SdkHttpFullRequest marshallRequest(CopyDbClusterSnapshotRequest request) private ExecutionAttributes executionAttributes() { return new ExecutionAttributes().putAttribute(AwsSignerExecutionAttribute.AWS_CREDENTIALS, CREDENTIALS) .putAttribute(AwsSignerExecutionAttribute.SIGNING_REGION, DESTINATION_REGION) - .putAttribute(SdkExecutionAttribute.PROFILE_FILE, ProfileFile.defaultProfileFile()) + .putAttribute(SdkExecutionAttribute.PROFILE_FILE_SUPPLIER, + ProfileFile::defaultProfileFile) .putAttribute(SdkExecutionAttribute.PROFILE_NAME, "default"); } diff --git a/services/dynamodb/src/main/java/software/amazon/awssdk/services/dynamodb/DynamoDbRetryPolicy.java b/services/dynamodb/src/main/java/software/amazon/awssdk/services/dynamodb/DynamoDbRetryPolicy.java index b8ddfb4632d0..3830c23cef5f 100644 --- a/services/dynamodb/src/main/java/software/amazon/awssdk/services/dynamodb/DynamoDbRetryPolicy.java +++ b/services/dynamodb/src/main/java/software/amazon/awssdk/services/dynamodb/DynamoDbRetryPolicy.java @@ -64,7 +64,7 @@ public static RetryPolicy resolveRetryPolicy(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(); diff --git a/services/dynamodb/src/test/java/software/amazon/awssdk/services/dynamodb/DynamoDbRetryPolicyTest.java b/services/dynamodb/src/test/java/software/amazon/awssdk/services/dynamodb/DynamoDbRetryPolicyTest.java index c99de392ed52..6b4863c996d6 100644 --- a/services/dynamodb/src/test/java/software/amazon/awssdk/services/dynamodb/DynamoDbRetryPolicyTest.java +++ b/services/dynamodb/src/test/java/software/amazon/awssdk/services/dynamodb/DynamoDbRetryPolicyTest.java @@ -8,12 +8,16 @@ import org.junit.jupiter.api.Test; import software.amazon.awssdk.core.SdkSystemSetting; import software.amazon.awssdk.core.client.config.SdkClientConfiguration; +import software.amazon.awssdk.core.client.config.SdkClientOption; +import software.amazon.awssdk.core.retry.RetryMode; import software.amazon.awssdk.core.retry.RetryPolicy; import software.amazon.awssdk.core.retry.backoff.BackoffStrategy; import software.amazon.awssdk.core.retry.backoff.FullJitterBackoffStrategy; +import software.amazon.awssdk.profiles.ProfileFile; import software.amazon.awssdk.testutils.EnvironmentVariableHelper; +import software.amazon.awssdk.utils.StringInputStream; -public class DynamoDbRetryPolicyTest { +class DynamoDbRetryPolicyTest { private EnvironmentVariableHelper environmentVariableHelper; @@ -28,7 +32,7 @@ public void reset() { } @Test - public void test_numRetries_with_standardRetryPolicy() { + void test_numRetries_with_standardRetryPolicy() { environmentVariableHelper.set(SdkSystemSetting.AWS_RETRY_MODE.environmentVariable(), "standard"); final SdkClientConfiguration sdkClientConfiguration = SdkClientConfiguration.builder().build(); final RetryPolicy retryPolicy = DynamoDbRetryPolicy.resolveRetryPolicy(sdkClientConfiguration); @@ -36,7 +40,7 @@ public void test_numRetries_with_standardRetryPolicy() { } @Test - public void test_numRetries_with_legacyRetryPolicy() { + void test_numRetries_with_legacyRetryPolicy() { environmentVariableHelper.set(SdkSystemSetting.AWS_RETRY_MODE.environmentVariable(), "legacy"); final SdkClientConfiguration sdkClientConfiguration = SdkClientConfiguration.builder().build(); final RetryPolicy retryPolicy = DynamoDbRetryPolicy.resolveRetryPolicy(sdkClientConfiguration); @@ -44,7 +48,7 @@ public void test_numRetries_with_legacyRetryPolicy() { } @Test - public void test_backoffBaseDelay_with_standardRetryPolicy() { + void test_backoffBaseDelay_with_standardRetryPolicy() { environmentVariableHelper.set(SdkSystemSetting.AWS_RETRY_MODE.environmentVariable(), "standard"); SdkClientConfiguration sdkClientConfiguration = SdkClientConfiguration.builder().build(); RetryPolicy retryPolicy = DynamoDbRetryPolicy.resolveRetryPolicy(sdkClientConfiguration); @@ -55,4 +59,86 @@ public void test_backoffBaseDelay_with_standardRetryPolicy() { }); } + @Test + void resolve_retryModeSetInEnv_doesNotCallSupplier() { + environmentVariableHelper.set(SdkSystemSetting.AWS_RETRY_MODE.environmentVariable(), "standard"); + SdkClientConfiguration sdkClientConfiguration = SdkClientConfiguration.builder().build(); + RetryPolicy retryPolicy = DynamoDbRetryPolicy.resolveRetryPolicy(sdkClientConfiguration); + RetryMode retryMode = retryPolicy.retryMode(); + + assertThat(retryMode).isEqualTo(RetryMode.STANDARD); + } + + @Test + void resolve_retryModeSetWithEnvAndSupplier_resolvesFromEnv() { + environmentVariableHelper.set(SdkSystemSetting.AWS_RETRY_MODE.environmentVariable(), "standard"); + ProfileFile profileFile = ProfileFile.builder() + .content(new StringInputStream("[profile default]\n" + + "retry_mode = adaptive")) + .type(ProfileFile.Type.CONFIGURATION) + .build(); + SdkClientConfiguration sdkClientConfiguration = SdkClientConfiguration + .builder() + .option(SdkClientOption.PROFILE_FILE_SUPPLIER, () -> profileFile) + .option(SdkClientOption.PROFILE_NAME, "default") + .build(); + RetryPolicy retryPolicy = DynamoDbRetryPolicy.resolveRetryPolicy(sdkClientConfiguration); + RetryMode retryMode = retryPolicy.retryMode(); + + assertThat(retryMode).isEqualTo(RetryMode.STANDARD); + } + + @Test + void resolve_retryModeSetWithSupplier_resolvesFromSupplier() { + ProfileFile profileFile = ProfileFile.builder() + .content(new StringInputStream("[profile default]\n" + + "retry_mode = adaptive")) + .type(ProfileFile.Type.CONFIGURATION) + .build(); + SdkClientConfiguration sdkClientConfiguration = SdkClientConfiguration + .builder() + .option(SdkClientOption.PROFILE_FILE_SUPPLIER, () -> profileFile) + .option(SdkClientOption.PROFILE_NAME, "default") + .build(); + RetryPolicy retryPolicy = DynamoDbRetryPolicy.resolveRetryPolicy(sdkClientConfiguration); + RetryMode retryMode = retryPolicy.retryMode(); + + assertThat(retryMode).isEqualTo(RetryMode.ADAPTIVE); + } + + @Test + void resolve_retryModeSetWithSdkClientOption_resolvesFromSdkClientOption() { + ProfileFile profileFile = ProfileFile.builder() + .content(new StringInputStream("[profile default]\n")) + .type(ProfileFile.Type.CONFIGURATION) + .build(); + SdkClientConfiguration sdkClientConfiguration = SdkClientConfiguration + .builder() + .option(SdkClientOption.PROFILE_FILE_SUPPLIER, () -> profileFile) + .option(SdkClientOption.PROFILE_NAME, "default") + .option(SdkClientOption.DEFAULT_RETRY_MODE, RetryMode.STANDARD) + .build(); + RetryPolicy retryPolicy = DynamoDbRetryPolicy.resolveRetryPolicy(sdkClientConfiguration); + RetryMode retryMode = retryPolicy.retryMode(); + + assertThat(retryMode).isEqualTo(RetryMode.STANDARD); + } + + @Test + void resolve_retryModeNotSetWithEnvNorSupplier_resolvesFromSdkDefault() { + ProfileFile profileFile = ProfileFile.builder() + .content(new StringInputStream("[profile default]\n")) + .type(ProfileFile.Type.CONFIGURATION) + .build(); + SdkClientConfiguration sdkClientConfiguration = SdkClientConfiguration + .builder() + .option(SdkClientOption.PROFILE_FILE_SUPPLIER, () -> profileFile) + .option(SdkClientOption.PROFILE_NAME, "default") + .build(); + RetryPolicy retryPolicy = DynamoDbRetryPolicy.resolveRetryPolicy(sdkClientConfiguration); + RetryMode retryMode = retryPolicy.retryMode(); + + assertThat(retryMode).isEqualTo(RetryMode.LEGACY); + } + } diff --git a/services/kinesis/pom.xml b/services/kinesis/pom.xml index f3d712284e13..7158fe921be4 100644 --- a/services/kinesis/pom.xml +++ b/services/kinesis/pom.xml @@ -61,11 +61,6 @@ protocol-core ${awsjavasdk.version} - - software.amazon.awssdk - profiles - ${awsjavasdk.version} - software.amazon.eventstream eventstream diff --git a/services/kinesis/src/main/java/software/amazon/awssdk/services/kinesis/KinesisRetryPolicy.java b/services/kinesis/src/main/java/software/amazon/awssdk/services/kinesis/KinesisRetryPolicy.java index 621993a91f4f..1538ec0dc67e 100644 --- a/services/kinesis/src/main/java/software/amazon/awssdk/services/kinesis/KinesisRetryPolicy.java +++ b/services/kinesis/src/main/java/software/amazon/awssdk/services/kinesis/KinesisRetryPolicy.java @@ -40,7 +40,7 @@ public static RetryPolicy resolveRetryPolicy(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(); diff --git a/services/neptune/src/main/java/software/amazon/awssdk/services/neptune/internal/RdsPresignInterceptor.java b/services/neptune/src/main/java/software/amazon/awssdk/services/neptune/internal/RdsPresignInterceptor.java index d0b0e9faf558..ba04154891a3 100644 --- a/services/neptune/src/main/java/software/amazon/awssdk/services/neptune/internal/RdsPresignInterceptor.java +++ b/services/neptune/src/main/java/software/amazon/awssdk/services/neptune/internal/RdsPresignInterceptor.java @@ -164,7 +164,7 @@ private URI createEndpoint(String regionName, String serviceName, ExecutionAttri return new DefaultServiceEndpointBuilder(SERVICE_NAME, Protocol.HTTPS.toString()) .withRegion(region) - .withProfileFile(() -> attributes.getAttribute(SdkExecutionAttribute.PROFILE_FILE)) + .withProfileFile(attributes.getAttribute(SdkExecutionAttribute.PROFILE_FILE_SUPPLIER)) .withProfileName(attributes.getAttribute(SdkExecutionAttribute.PROFILE_NAME)) .withDualstackEnabled(attributes.getAttribute(AwsExecutionAttribute.DUALSTACK_ENDPOINT_ENABLED)) .withFipsEnabled(attributes.getAttribute(AwsExecutionAttribute.FIPS_ENDPOINT_ENABLED)) diff --git a/services/neptune/src/test/java/software/amazon/awssdk/services/neptune/internal/PresignRequestHandlerTest.java b/services/neptune/src/test/java/software/amazon/awssdk/services/neptune/internal/PresignRequestHandlerTest.java index 9ed640a617f4..e0f083a4da88 100644 --- a/services/neptune/src/test/java/software/amazon/awssdk/services/neptune/internal/PresignRequestHandlerTest.java +++ b/services/neptune/src/test/java/software/amazon/awssdk/services/neptune/internal/PresignRequestHandlerTest.java @@ -164,7 +164,8 @@ private SdkHttpFullRequest marshallRequest(CopyDbClusterSnapshotRequest request) private ExecutionAttributes executionAttributes() { return new ExecutionAttributes().putAttribute(AwsSignerExecutionAttribute.AWS_CREDENTIALS, CREDENTIALS) .putAttribute(AwsSignerExecutionAttribute.SIGNING_REGION, DESTINATION_REGION) - .putAttribute(SdkExecutionAttribute.PROFILE_FILE, ProfileFile.defaultProfileFile()) + .putAttribute(SdkExecutionAttribute.PROFILE_FILE_SUPPLIER, + ProfileFile::defaultProfileFile) .putAttribute(SdkExecutionAttribute.PROFILE_NAME, "default"); } diff --git a/services/polly/src/main/java/software/amazon/awssdk/services/polly/internal/presigner/DefaultPollyPresigner.java b/services/polly/src/main/java/software/amazon/awssdk/services/polly/internal/presigner/DefaultPollyPresigner.java index 1dd6218801b1..d939bdbc389d 100644 --- a/services/polly/src/main/java/software/amazon/awssdk/services/polly/internal/presigner/DefaultPollyPresigner.java +++ b/services/polly/src/main/java/software/amazon/awssdk/services/polly/internal/presigner/DefaultPollyPresigner.java @@ -25,6 +25,7 @@ import java.util.List; import java.util.Map; import java.util.function.Function; +import java.util.function.Supplier; import java.util.stream.Stream; import software.amazon.awssdk.annotations.SdkInternalApi; import software.amazon.awssdk.auth.credentials.AwsCredentials; @@ -68,7 +69,7 @@ public final class DefaultPollyPresigner implements PollyPresigner { private static final String SERVICE_NAME = "polly"; private static final Aws4Signer DEFAULT_SIGNER = Aws4Signer.create(); - private final ProfileFile profileFile; + private final Supplier profileFile; private final String profileName; private final Region region; private final AwsCredentialsProvider credentialsProvider; @@ -77,11 +78,11 @@ public final class DefaultPollyPresigner implements PollyPresigner { private final Boolean fipsEnabled; private DefaultPollyPresigner(BuilderImpl builder) { - this.profileFile = ProfileFile.defaultProfileFile(); + this.profileFile = ProfileFile::defaultProfileFile; this.profileName = ProfileFileSystemSetting.AWS_PROFILE.getStringValueOrThrow(); this.region = builder.region != null ? builder.region : DefaultAwsRegionProviderChain.builder() - .profileFile(() -> profileFile) + .profileFile(profileFile) .profileName(profileName) .build() .getRegion(); @@ -93,14 +94,14 @@ private DefaultPollyPresigner(BuilderImpl builder) { this.endpointOverride = builder.endpointOverride; this.dualstackEnabled = builder.dualstackEnabled != null ? builder.dualstackEnabled : DualstackEnabledProvider.builder() - .profileFile(() -> profileFile) + .profileFile(profileFile) .profileName(profileName) .build() .isDualstackEnabled() .orElse(false); this.fipsEnabled = builder.fipsEnabled != null ? builder.fipsEnabled : FipsEnabledProvider.builder() - .profileFile(() -> profileFile) + .profileFile(profileFile) .profileName(profileName) .build() .isFipsEnabled() @@ -245,7 +246,7 @@ private URI resolveEndpoint() { return new DefaultServiceEndpointBuilder(SERVICE_NAME, "https") .withRegion(region()) - .withProfileFile(() -> profileFile) + .withProfileFile(profileFile) .withProfileName(profileName) .withDualstackEnabled(dualstackEnabled) .withFipsEnabled(fipsEnabled) diff --git a/services/polly/src/test/java/software/amazon/awssdk/services/polly/internal/presigner/DefaultPollyPresignerTest.java b/services/polly/src/test/java/software/amazon/awssdk/services/polly/internal/presigner/DefaultPollyPresignerTest.java index f5a17a07b9bd..4d5cc4973ff3 100644 --- a/services/polly/src/test/java/software/amazon/awssdk/services/polly/internal/presigner/DefaultPollyPresignerTest.java +++ b/services/polly/src/test/java/software/amazon/awssdk/services/polly/internal/presigner/DefaultPollyPresignerTest.java @@ -41,7 +41,7 @@ /** * Tests for {@link DefaultPollyPresigner}. */ -public class DefaultPollyPresignerTest { +class DefaultPollyPresignerTest { private static final SynthesizeSpeechRequest BASIC_SYNTHESIZE_SPEECH_REQUEST = SynthesizeSpeechRequest.builder() .voiceId("Salli") .outputFormat(OutputFormat.PCM) @@ -56,7 +56,7 @@ public void methodSetup() { } @Test - public void presign_requestLevelCredentials_honored() { + void presign_requestLevelCredentials_honored() { AwsCredentials requestCredentials = AwsBasicCredentials.create("akid2", "skid2"); PollyPresigner presigner = DefaultPollyPresigner.builder() @@ -80,7 +80,7 @@ public void presign_requestLevelCredentials_honored() { } @Test - public void presign_requestLevelHeaders_included() { + void presign_requestLevelHeaders_included() { PollyPresigner presigner = DefaultPollyPresigner.builder() .region(Region.US_EAST_1) .credentialsProvider(credentialsProvider) @@ -104,7 +104,7 @@ public void presign_requestLevelHeaders_included() { } @Test - public void presign_includesRequestLevelHeaders_notBrowserCompatible() { + void presign_includesRequestLevelHeaders_notBrowserCompatible() { PollyPresigner presigner = DefaultPollyPresigner.builder() .region(Region.US_EAST_1) .credentialsProvider(credentialsProvider) @@ -128,7 +128,7 @@ public void presign_includesRequestLevelHeaders_notBrowserCompatible() { } @Test - public void presign_includesRequestLevelQueryParams_included() { + void presign_includesRequestLevelQueryParams_included() { PollyPresigner presigner = DefaultPollyPresigner.builder() .region(Region.US_EAST_1) .credentialsProvider(credentialsProvider) @@ -151,7 +151,7 @@ public void presign_includesRequestLevelQueryParams_included() { } @Test - public void presign_endpointOverriden() { + void presign_endpointOverriden() { PollyPresigner presigner = DefaultPollyPresigner.builder() .region(Region.US_EAST_1) .credentialsProvider(credentialsProvider) @@ -173,7 +173,7 @@ public void presign_endpointOverriden() { } @Test - public void close_closesCustomCloseableCredentialsProvider() throws IOException { + void close_closesCustomCloseableCredentialsProvider() throws IOException { TestCredentialsProvider mockCredentialsProvider = mock(TestCredentialsProvider.class); PollyPresigner presigner = DefaultPollyPresigner.builder() @@ -186,6 +186,19 @@ public void close_closesCustomCloseableCredentialsProvider() throws IOException verify(mockCredentialsProvider).close(); } + @Test + void presigner_credentialsProviderSetToNullByBuilder_createsDefaultCredentialsProvider() { + DefaultPollyPresigner presigner = (DefaultPollyPresigner) + DefaultPollyPresigner.builder() + .region(Region.US_EAST_1) + .credentialsProvider(null) + .build(); + + + AwsCredentialsProvider awsCredentialsProvider = presigner.credentialsProvider(); + assertThat(awsCredentialsProvider).isNotNull(); + } + private interface TestCredentialsProvider extends AwsCredentialsProvider, Closeable { } } diff --git a/services/rds/src/main/java/software/amazon/awssdk/services/rds/internal/RdsPresignInterceptor.java b/services/rds/src/main/java/software/amazon/awssdk/services/rds/internal/RdsPresignInterceptor.java index 6f4dc67954de..cd2ad833f8db 100644 --- a/services/rds/src/main/java/software/amazon/awssdk/services/rds/internal/RdsPresignInterceptor.java +++ b/services/rds/src/main/java/software/amazon/awssdk/services/rds/internal/RdsPresignInterceptor.java @@ -162,7 +162,7 @@ private URI createEndpoint(String regionName, String serviceName, ExecutionAttri return new DefaultServiceEndpointBuilder(SERVICE_NAME, Protocol.HTTPS.toString()) .withRegion(region) - .withProfileFile(() -> attributes.getAttribute(SdkExecutionAttribute.PROFILE_FILE)) + .withProfileFile(attributes.getAttribute(SdkExecutionAttribute.PROFILE_FILE_SUPPLIER)) .withProfileName(attributes.getAttribute(SdkExecutionAttribute.PROFILE_NAME)) .withDualstackEnabled(attributes.getAttribute(AwsExecutionAttribute.DUALSTACK_ENDPOINT_ENABLED)) .withFipsEnabled(attributes.getAttribute(AwsExecutionAttribute.FIPS_ENDPOINT_ENABLED)) diff --git a/services/rds/src/test/java/software/amazon/awssdk/services/rds/internal/PresignRequestHandlerTest.java b/services/rds/src/test/java/software/amazon/awssdk/services/rds/internal/PresignRequestHandlerTest.java index e79a81d0b003..65a04d3a2f2a 100644 --- a/services/rds/src/test/java/software/amazon/awssdk/services/rds/internal/PresignRequestHandlerTest.java +++ b/services/rds/src/test/java/software/amazon/awssdk/services/rds/internal/PresignRequestHandlerTest.java @@ -165,7 +165,8 @@ private SdkHttpFullRequest marshallRequest(CopyDbSnapshotRequest request) { private ExecutionAttributes executionAttributes() { return new ExecutionAttributes().putAttribute(AwsSignerExecutionAttribute.AWS_CREDENTIALS, CREDENTIALS) .putAttribute(AwsSignerExecutionAttribute.SIGNING_REGION, DESTINATION_REGION) - .putAttribute(SdkExecutionAttribute.PROFILE_FILE, ProfileFile.defaultProfileFile()) + .putAttribute(SdkExecutionAttribute.PROFILE_FILE_SUPPLIER, + ProfileFile::defaultProfileFile) .putAttribute(SdkExecutionAttribute.PROFILE_NAME, "default"); } diff --git a/services/s3/src/main/java/software/amazon/awssdk/services/s3/S3Configuration.java b/services/s3/src/main/java/software/amazon/awssdk/services/s3/S3Configuration.java index 74a2fd11523b..5dec8e93602f 100644 --- a/services/s3/src/main/java/software/amazon/awssdk/services/s3/S3Configuration.java +++ b/services/s3/src/main/java/software/amazon/awssdk/services/s3/S3Configuration.java @@ -15,12 +15,15 @@ package software.amazon.awssdk.services.s3; +import java.util.Optional; +import java.util.function.Supplier; import software.amazon.awssdk.annotations.Immutable; import software.amazon.awssdk.annotations.NotThreadSafe; import software.amazon.awssdk.annotations.SdkPublicApi; import software.amazon.awssdk.annotations.ThreadSafe; import software.amazon.awssdk.core.ServiceConfiguration; import software.amazon.awssdk.profiles.ProfileFile; +import software.amazon.awssdk.profiles.ProfileFileSupplier; import software.amazon.awssdk.profiles.ProfileFileSystemSetting; import software.amazon.awssdk.services.s3.internal.FieldWithDefault; import software.amazon.awssdk.services.s3.internal.settingproviders.DisableMultiRegionProviderChain; @@ -66,23 +69,23 @@ public final class S3Configuration implements ServiceConfiguration, ToCopyableBu private final FieldWithDefault dualstackEnabled; private final FieldWithDefault checksumValidationEnabled; private final FieldWithDefault chunkedEncodingEnabled; - private final FieldWithDefault useArnRegionEnabled; - private final FieldWithDefault multiRegionEnabled; - private final FieldWithDefault profileFile; + private final Boolean useArnRegionEnabled; + private final Boolean multiRegionEnabled; + private final FieldWithDefault> profileFile; private final FieldWithDefault profileName; private S3Configuration(DefaultS3ServiceConfigurationBuilder builder) { this.dualstackEnabled = FieldWithDefault.create(builder.dualstackEnabled, DEFAULT_DUALSTACK_ENABLED); this.accelerateModeEnabled = FieldWithDefault.create(builder.accelerateModeEnabled, DEFAULT_ACCELERATE_MODE_ENABLED); this.pathStyleAccessEnabled = FieldWithDefault.create(builder.pathStyleAccessEnabled, DEFAULT_PATH_STYLE_ACCESS_ENABLED); - this.checksumValidationEnabled = FieldWithDefault.create(builder.checksumValidationEnabled, + this.checksumValidationEnabled = FieldWithDefault.create(builder.checksumValidationEnabled, DEFAULT_CHECKSUM_VALIDATION_ENABLED); this.chunkedEncodingEnabled = FieldWithDefault.create(builder.chunkedEncodingEnabled, DEFAULT_CHUNKED_ENCODING_ENABLED); - this.profileFile = FieldWithDefault.createLazy(builder.profileFile, ProfileFile::defaultProfileFile); + this.profileFile = FieldWithDefault.create(builder.profileFile, ProfileFile::defaultProfileFile); this.profileName = FieldWithDefault.create(builder.profileName, ProfileFileSystemSetting.AWS_PROFILE.getStringValueOrThrow()); - this.useArnRegionEnabled = FieldWithDefault.createLazy(builder.useArnRegionEnabled, this::resolveUseArnRegionEnabled); - this.multiRegionEnabled = FieldWithDefault.createLazy(builder.multiRegionEnabled, this::resolveMultiRegionEnabled); + this.useArnRegionEnabled = builder.useArnRegionEnabled; + this.multiRegionEnabled = builder.multiRegionEnabled; if (accelerateModeEnabled() && pathStyleAccessEnabled()) { throw new IllegalArgumentException("Accelerate mode cannot be used with path style addressing"); @@ -189,7 +192,8 @@ public boolean chunkedEncodingEnabled() { * @return True if a different region in the ARN can be used. */ public boolean useArnRegionEnabled() { - return useArnRegionEnabled.value(); + return Optional.ofNullable(useArnRegionEnabled) + .orElseGet(this::resolveUseArnRegionEnabled); } /** @@ -198,19 +202,20 @@ public boolean useArnRegionEnabled() { * @return True if multi-region ARNs is enabled. */ public boolean multiRegionEnabled() { - return multiRegionEnabled.value(); + return Optional.ofNullable(multiRegionEnabled) + .orElseGet(this::resolveMultiRegionEnabled); } @Override public Builder toBuilder() { return builder() .dualstackEnabled(dualstackEnabled.valueOrNullIfDefault()) - .multiRegionEnabled(multiRegionEnabled.valueOrNullIfDefault()) + .multiRegionEnabled(multiRegionEnabled) .accelerateModeEnabled(accelerateModeEnabled.valueOrNullIfDefault()) .pathStyleAccessEnabled(pathStyleAccessEnabled.valueOrNullIfDefault()) .checksumValidationEnabled(checksumValidationEnabled.valueOrNullIfDefault()) .chunkedEncodingEnabled(chunkedEncodingEnabled.valueOrNullIfDefault()) - .useArnRegionEnabled(useArnRegionEnabled.valueOrNullIfDefault()) + .useArnRegionEnabled(useArnRegionEnabled) .profileFile(profileFile.valueOrNullIfDefault()) .profileName(profileName.valueOrNullIfDefault()); } @@ -237,7 +242,7 @@ public interface Builder extends CopyableBuilder { Boolean accelerateModeEnabled(); /** - * Option to enable using the accelerate enedpoint when accessing S3. Accelerate + * Option to enable using the accelerate endpoint when accessing S3. Accelerate * endpoints allow faster transfer of objects by using Amazon CloudFront's * globally distributed edge locations. * @@ -325,6 +330,19 @@ public interface Builder extends CopyableBuilder { */ Builder profileFile(ProfileFile profileFile); + Supplier profileFileSupplier(); + + /** + * The supplier of profile file instances that should be consulted to determine the default value of + * {@link #useArnRegionEnabled(Boolean)} or {@link #multiRegionEnabled(Boolean)}. + * This is not used, if those parameters are configured on the builder. + * + *

    + * By default, the {@link ProfileFile#defaultProfileFile()} is used. + *

    + */ + Builder profileFile(Supplier profileFile); + String profileName(); /** @@ -347,7 +365,7 @@ static final class DefaultS3ServiceConfigurationBuilder implements Builder { private Boolean chunkedEncodingEnabled; private Boolean useArnRegionEnabled; private Boolean multiRegionEnabled; - private ProfileFile profileFile; + private Supplier profileFile; private String profileName; @Override @@ -449,11 +467,25 @@ public Builder multiRegionEnabled(Boolean multiRegionEnabled) { @Override public ProfileFile profileFile() { - return profileFile; + return Optional.ofNullable(profileFile) + .map(Supplier::get) + .orElse(null); } @Override public Builder profileFile(ProfileFile profileFile) { + return profileFile(Optional.ofNullable(profileFile) + .map(ProfileFileSupplier::fixedProfileFile) + .orElse(null)); + } + + @Override + public Supplier profileFileSupplier() { + return profileFile; + } + + @Override + public Builder profileFile(Supplier profileFile) { this.profileFile = profileFile; return this; } diff --git a/services/s3/src/main/java/software/amazon/awssdk/services/s3/S3Utilities.java b/services/s3/src/main/java/software/amazon/awssdk/services/s3/S3Utilities.java index fd5aba1897ab..0a0bbc9cfc33 100644 --- a/services/s3/src/main/java/software/amazon/awssdk/services/s3/S3Utilities.java +++ b/services/s3/src/main/java/software/amazon/awssdk/services/s3/S3Utilities.java @@ -21,6 +21,7 @@ import java.net.URL; import java.util.ArrayList; import java.util.List; +import java.util.Optional; import java.util.function.Consumer; import java.util.function.Supplier; import software.amazon.awssdk.annotations.Immutable; @@ -48,6 +49,7 @@ import software.amazon.awssdk.http.SdkHttpMethod; import software.amazon.awssdk.http.SdkHttpRequest; import software.amazon.awssdk.profiles.ProfileFile; +import software.amazon.awssdk.profiles.ProfileFileSupplier; import software.amazon.awssdk.protocols.core.OperationInfo; import software.amazon.awssdk.protocols.core.PathMarshaller; import software.amazon.awssdk.protocols.core.ProtocolUtils; @@ -61,7 +63,6 @@ import software.amazon.awssdk.services.s3.model.GetObjectRequest; import software.amazon.awssdk.services.s3.model.GetUrlRequest; import software.amazon.awssdk.utils.AttributeMap; -import software.amazon.awssdk.utils.Lazy; import software.amazon.awssdk.utils.Validate; /** @@ -75,7 +76,6 @@ * GetUrlRequest request = GetUrlRequest.builder().bucket("foo-bucket").key("key-without-spaces").build(); * URL url = utilities.getUrl(request); * - *

    * *

    * 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. - *

    + *

    * TODO: This should get moved to aws-core (or split and moved to sdk-core and aws-core) when we support presigning from * multiple services. * TODO: After moving, this should get marked as an @SdkProtectedApi. */ @SdkInternalApi public abstract class DefaultSdkPresigner implements SdkPresigner { - private final ProfileFile profileFile; + private final Supplier profileFile; private final String profileName; private final Region region; private final URI endpointOverride; @@ -46,10 +47,10 @@ public abstract class DefaultSdkPresigner implements SdkPresigner { private final boolean fipsEnabled; protected DefaultSdkPresigner(Builder b) { - this.profileFile = ProfileFile.defaultProfileFile(); + this.profileFile = ProfileFile::defaultProfileFile; this.profileName = ProfileFileSystemSetting.AWS_PROFILE.getStringValueOrThrow(); this.region = b.region != null ? b.region : DefaultAwsRegionProviderChain.builder() - .profileFile(() -> profileFile) + .profileFile(profileFile) .profileName(profileName) .build() .getRegion(); @@ -61,21 +62,21 @@ protected DefaultSdkPresigner(Builder b) { this.endpointOverride = b.endpointOverride; this.dualstackEnabled = b.dualstackEnabled != null ? b.dualstackEnabled : DualstackEnabledProvider.builder() - .profileFile(() -> profileFile) + .profileFile(profileFile) .profileName(profileName) .build() .isDualstackEnabled() .orElse(null); this.fipsEnabled = b.fipsEnabled != null ? b.fipsEnabled : FipsEnabledProvider.builder() - .profileFile(() -> profileFile) + .profileFile(profileFile) .profileName(profileName) .build() .isFipsEnabled() .orElse(false); } - protected ProfileFile profileFile() { + protected Supplier profileFileSupplier() { return profileFile; } diff --git a/services/s3/src/test/java/software/amazon/awssdk/services/s3/S3ConfigurationTest.java b/services/s3/src/test/java/software/amazon/awssdk/services/s3/S3ConfigurationTest.java index 42f7e567f1c2..d9a7ed6da7e0 100644 --- a/services/s3/src/test/java/software/amazon/awssdk/services/s3/S3ConfigurationTest.java +++ b/services/s3/src/test/java/software/amazon/awssdk/services/s3/S3ConfigurationTest.java @@ -87,4 +87,32 @@ public void multiRegionEnabled_notDisabledInProviders_shouldResolveToTrue() { assertThat(config.multiRegionEnabled()).isEqualTo(true); } + @Test + public void multiRegionEnabled_enabledInCProfile_shouldResolveToConfigCorrectlyOncePerCall() { + S3Configuration config = S3Configuration.builder().build(); + + String trueProfileConfig = getClass().getResource("internal/settingproviders/ProfileFile_true").getFile(); + System.setProperty(AWS_CONFIG_FILE.property(), trueProfileConfig); + assertThat(config.multiRegionEnabled()).isEqualTo(false); + + System.clearProperty(AWS_CONFIG_FILE.property()); + String falseProfileConfig = getClass().getResource("internal/settingproviders/ProfileFile_false").getFile(); + System.setProperty(AWS_CONFIG_FILE.property(), falseProfileConfig); + assertThat(config.multiRegionEnabled()).isEqualTo(true); + } + + @Test + public void useArnRegionEnabled_enabledInCProfile_shouldResolveToConfigCorrectlyOncePerCall() { + S3Configuration config = S3Configuration.builder().build(); + + String trueProfileConfig = getClass().getResource("internal/settingproviders/ProfileFile_true").getFile(); + System.setProperty(AWS_CONFIG_FILE.property(), trueProfileConfig); + assertThat(config.useArnRegionEnabled()).isEqualTo(true); + + System.clearProperty(AWS_CONFIG_FILE.property()); + String falseProfileConfig = getClass().getResource("internal/settingproviders/ProfileFile_false").getFile(); + System.setProperty(AWS_CONFIG_FILE.property(), falseProfileConfig); + assertThat(config.useArnRegionEnabled()).isEqualTo(false); + } + } diff --git a/services/s3/src/test/java/software/amazon/awssdk/services/s3/internal/endpoints/UseGlobalEndpointResolverProfileFileTest.java b/services/s3/src/test/java/software/amazon/awssdk/services/s3/internal/endpoints/UseGlobalEndpointResolverProfileFileTest.java new file mode 100644 index 000000000000..4ad07d71f005 --- /dev/null +++ b/services/s3/src/test/java/software/amazon/awssdk/services/s3/internal/endpoints/UseGlobalEndpointResolverProfileFileTest.java @@ -0,0 +1,130 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package software.amazon.awssdk.services.s3.internal.endpoints; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.function.Supplier; +import org.junit.jupiter.api.Test; +import software.amazon.awssdk.core.client.config.SdkClientConfiguration; +import software.amazon.awssdk.core.client.config.SdkClientOption; +import software.amazon.awssdk.profiles.ProfileFile; +import software.amazon.awssdk.regions.Region; +import software.amazon.awssdk.regions.ServiceMetadataAdvancedOption; +import software.amazon.awssdk.utils.StringInputStream; + +class UseGlobalEndpointResolverProfileFileTest { + + @Test + void resolve_nonUsEast1_resolvesToFalse() { + SdkClientConfiguration config = SdkClientConfiguration.builder().build(); + UseGlobalEndpointResolver resolver = new UseGlobalEndpointResolver(config); + + assertThat(resolver.resolve(Region.AF_SOUTH_1)).isFalse(); + } + + @Test + void resolve_nullProfileFileSupplierAndNullDefaultRegionalEndpoint_resolvesToTrue() { + SdkClientConfiguration config = SdkClientConfiguration + .builder() + .option(SdkClientOption.PROFILE_FILE_SUPPLIER, null) + .option(SdkClientOption.PROFILE_NAME, "regional_s3_endpoint") + .build(); + UseGlobalEndpointResolver resolver = new UseGlobalEndpointResolver(config); + + assertThat(resolver.resolve(Region.US_EAST_1)).isTrue(); + } + + @Test + void resolve_nullProfileFileSupplierAndDefaultRegionalEndPointLegacy_resolvesToTrue() { + SdkClientConfiguration config = SdkClientConfiguration + .builder() + .option(ServiceMetadataAdvancedOption.DEFAULT_S3_US_EAST_1_REGIONAL_ENDPOINT, "legacy") + .option(SdkClientOption.PROFILE_FILE_SUPPLIER, null) + .option(SdkClientOption.PROFILE_NAME, "regional_s3_endpoint") + .build(); + UseGlobalEndpointResolver resolver = new UseGlobalEndpointResolver(config); + + assertThat(resolver.resolve(Region.US_EAST_1)).isTrue(); + } + + @Test + void resolve_nullProfileFileSupplierAndDefaultRegionalEndPointRegional_resolvesToFalse() { + SdkClientConfiguration config = SdkClientConfiguration + .builder() + .option(ServiceMetadataAdvancedOption.DEFAULT_S3_US_EAST_1_REGIONAL_ENDPOINT, "regional") + .option(SdkClientOption.PROFILE_FILE_SUPPLIER, null) + .option(SdkClientOption.PROFILE_NAME, "regional_s3_endpoint") + .build(); + UseGlobalEndpointResolver resolver = new UseGlobalEndpointResolver(config); + + assertThat(resolver.resolve(Region.US_EAST_1)).isFalse(); + } + + @Test + void resolve_nullProfileFileSupplier_resolvesToTrue() { + Supplier supplier = null; + + SdkClientConfiguration config = SdkClientConfiguration + .builder() + .option(SdkClientOption.PROFILE_FILE_SUPPLIER, supplier) + .option(SdkClientOption.PROFILE_NAME, "regional_s3_endpoint") + .build(); + UseGlobalEndpointResolver resolver = new UseGlobalEndpointResolver(config); + + assertThat(resolver.resolve(Region.US_EAST_1)).isTrue(); + } + + @Test + void resolve_profileFileSupplierRegionalEndpointLegacy_resolvesToTrue() { + ProfileFile file = configuration("[profile regional_s3_endpoint]\n" + + "s3_us_east_1_regional_endpoint = legacy"); + Supplier supplier = () -> file; + + SdkClientConfiguration config = SdkClientConfiguration + .builder() + .option(SdkClientOption.PROFILE_FILE_SUPPLIER, supplier) + .option(SdkClientOption.PROFILE_NAME, "regional_s3_endpoint") + .build(); + UseGlobalEndpointResolver resolver = new UseGlobalEndpointResolver(config); + + assertThat(resolver.resolve(Region.US_EAST_1)).isTrue(); + } + + @Test + void resolve_profileFileSupplierRegionalEndpointRegional_resolvesToFalse() { + ProfileFile file = configuration("[profile regional_s3_endpoint]\n" + + "s3_us_east_1_regional_endpoint = regional"); + Supplier supplier = () -> file; + + SdkClientConfiguration config = SdkClientConfiguration + .builder() + .option(SdkClientOption.PROFILE_FILE_SUPPLIER, supplier) + .option(SdkClientOption.PROFILE_NAME, "regional_s3_endpoint") + .build(); + UseGlobalEndpointResolver resolver = new UseGlobalEndpointResolver(config); + + assertThat(resolver.resolve(Region.US_EAST_1)).isFalse(); + } + + private ProfileFile configuration(String string) { + return ProfileFile.builder() + .content(new StringInputStream(string)) + .type(ProfileFile.Type.CONFIGURATION) + .build(); + } + +} diff --git a/services/s3/src/test/java/software/amazon/awssdk/services/s3/internal/endpoints/UseGlobalEndpointResolverTest.java b/services/s3/src/test/java/software/amazon/awssdk/services/s3/internal/endpoints/UseGlobalEndpointResolverTest.java index 3588c07f0dd2..e1121fb3aa97 100644 --- a/services/s3/src/test/java/software/amazon/awssdk/services/s3/internal/endpoints/UseGlobalEndpointResolverTest.java +++ b/services/s3/src/test/java/software/amazon/awssdk/services/s3/internal/endpoints/UseGlobalEndpointResolverTest.java @@ -17,11 +17,14 @@ import static org.assertj.core.api.Assertions.assertThat; -import java.net.URI; +import java.io.ByteArrayInputStream; +import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Paths; import java.util.Arrays; import java.util.Collection; +import java.util.function.Supplier; +import org.apache.hc.core5.http.Chars; import org.junit.After; import org.junit.Test; import org.junit.runner.RunWith; @@ -31,10 +34,9 @@ import software.amazon.awssdk.core.client.config.SdkClientOption; import software.amazon.awssdk.profiles.ProfileFile; import software.amazon.awssdk.regions.Region; -import software.amazon.awssdk.regions.ServiceMetadata; import software.amazon.awssdk.regions.ServiceMetadataAdvancedOption; -import software.amazon.awssdk.regions.servicemetadata.EnhancedS3ServiceMetadata; import software.amazon.awssdk.testutils.EnvironmentVariableHelper; +import software.amazon.awssdk.utils.StringInputStream; import software.amazon.awssdk.utils.Validate; @RunWith(Parameterized.class) @@ -110,7 +112,7 @@ public void differentCombinationOfConfigs_shouldResolveCorrectly() { .type(ProfileFile.Type.CONFIGURATION) .build(); - configBuilder.option(SdkClientOption.PROFILE_FILE, file) + configBuilder.option(SdkClientOption.PROFILE_FILE_SUPPLIER, () -> file) .option(SdkClientOption.PROFILE_NAME, "regional_s3_endpoint"); } diff --git a/services/s3/src/test/java/software/amazon/awssdk/services/s3/internal/settingproviders/DisableMultiRegionProviderChainTest.java b/services/s3/src/test/java/software/amazon/awssdk/services/s3/internal/settingproviders/DisableMultiRegionProviderChainTest.java index f2cd9efb87ad..fb018228e5a1 100644 --- a/services/s3/src/test/java/software/amazon/awssdk/services/s3/internal/settingproviders/DisableMultiRegionProviderChainTest.java +++ b/services/s3/src/test/java/software/amazon/awssdk/services/s3/internal/settingproviders/DisableMultiRegionProviderChainTest.java @@ -24,23 +24,23 @@ import org.junit.jupiter.api.Test; import software.amazon.awssdk.testutils.EnvironmentVariableHelper; -public class DisableMultiRegionProviderChainTest { +class DisableMultiRegionProviderChainTest { private final EnvironmentVariableHelper helper = new EnvironmentVariableHelper(); @AfterEach - public void clearSystemProperty() { + void clearSystemProperty() { System.clearProperty(AWS_S3_DISABLE_MULTIREGION_ACCESS_POINTS.property()); System.clearProperty(AWS_CONFIG_FILE.property()); helper.reset(); } @Test - public void notSpecified_shouldReturnEmptyOptional() { + void notSpecified_shouldReturnEmptyOptional() { assertThat(DisableMultiRegionProviderChain.create().resolve()).isEqualTo(Optional.empty()); } @Test - public void specifiedInBothProviders_systemPropertiesShouldTakePrecedence() { + void specifiedInBothProviders_systemPropertiesShouldTakePrecedence() { System.setProperty(AWS_S3_DISABLE_MULTIREGION_ACCESS_POINTS.property(), "false"); String configFile = getClass().getResource("ProfileFile_true").getFile(); System.setProperty(AWS_CONFIG_FILE.property(), configFile); @@ -49,7 +49,7 @@ public void specifiedInBothProviders_systemPropertiesShouldTakePrecedence() { } @Test - public void systemPropertiesThrowException_shouldUseConfigFile() { + void systemPropertiesThrowException_shouldUseConfigFile() { System.setProperty(AWS_S3_DISABLE_MULTIREGION_ACCESS_POINTS.property(), "foobar"); String configFile = getClass().getResource("ProfileFile_true").getFile(); System.setProperty(AWS_CONFIG_FILE.property(), configFile); @@ -58,7 +58,7 @@ public void systemPropertiesThrowException_shouldUseConfigFile() { } @Test - public void systemPropertiesNotSpecified_shouldUseConfigFile() { + void systemPropertiesNotSpecified_shouldUseConfigFile() { String configFile = getClass().getResource("ProfileFile_true").getFile(); System.setProperty(AWS_CONFIG_FILE.property(), configFile); @@ -66,7 +66,21 @@ public void systemPropertiesNotSpecified_shouldUseConfigFile() { } @Test - public void bothProvidersThrowException_shouldReturnEmpty() { + void resolve_systemPropertiesNotSpecified_shouldResolveOncePerCall() { + String trueConfigFile = getClass().getResource("ProfileFile_true").getFile(); + System.setProperty(AWS_CONFIG_FILE.property(), trueConfigFile); + + assertThat(DisableMultiRegionProviderChain.create().resolve()).isEqualTo(Optional.of(Boolean.TRUE)); + + System.clearProperty(AWS_CONFIG_FILE.property()); + String falseConfigFile = getClass().getResource("ProfileFile_false").getFile(); + System.setProperty(AWS_CONFIG_FILE.property(), falseConfigFile); + + assertThat(DisableMultiRegionProviderChain.create().resolve()).isEqualTo(Optional.of(Boolean.FALSE)); + } + + @Test + void bothProvidersThrowException_shouldReturnEmpty() { System.setProperty(AWS_S3_DISABLE_MULTIREGION_ACCESS_POINTS.property(), "foobar"); String configFile = getClass().getResource("ProfileFile_unsupportedValue").getFile(); System.setProperty(AWS_CONFIG_FILE.property(), configFile); diff --git a/services/s3/src/test/java/software/amazon/awssdk/services/s3/internal/settingproviders/ProfileDisableMultiRegionProviderTest.java b/services/s3/src/test/java/software/amazon/awssdk/services/s3/internal/settingproviders/ProfileDisableMultiRegionProviderTest.java index a7ca5fd7363c..b4948527e601 100644 --- a/services/s3/src/test/java/software/amazon/awssdk/services/s3/internal/settingproviders/ProfileDisableMultiRegionProviderTest.java +++ b/services/s3/src/test/java/software/amazon/awssdk/services/s3/internal/settingproviders/ProfileDisableMultiRegionProviderTest.java @@ -15,6 +15,7 @@ package software.amazon.awssdk.services.s3.internal.settingproviders; +import static java.lang.Boolean.FALSE; import static java.lang.Boolean.TRUE; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; @@ -72,6 +73,20 @@ public void unsupportedValue_shouldThrowException() { assertThatThrownBy(() -> provider.resolve()).isInstanceOf(IllegalArgumentException.class); } + @Test + public void resolve_specifiedMultipleValuesInConfigFile_shouldResolveOncePerCall() { + String trueConfigFile = getClass().getResource("ProfileFile_true").getFile(); + System.setProperty(AWS_CONFIG_FILE.property(), trueConfigFile); + + assertThat(provider.resolve()).isEqualTo(Optional.of(TRUE)); + + System.clearProperty(AWS_CONFIG_FILE.property()); + String falseConfigFile = getClass().getResource("ProfileFile_false").getFile(); + System.setProperty(AWS_CONFIG_FILE.property(), falseConfigFile); + + assertThat(provider.resolve()).isEqualTo(Optional.of(FALSE)); + } + @Test public void specifiedInOverrideConfig_shouldUse() { ExecutionInterceptor interceptor = Mockito.spy(AbstractExecutionInterceptor.class); diff --git a/services/s3/src/test/java/software/amazon/awssdk/services/s3/internal/settingproviders/ProfileUseArnRegionProviderTest.java b/services/s3/src/test/java/software/amazon/awssdk/services/s3/internal/settingproviders/ProfileUseArnRegionProviderTest.java index 044142d23103..362953f90b4c 100644 --- a/services/s3/src/test/java/software/amazon/awssdk/services/s3/internal/settingproviders/ProfileUseArnRegionProviderTest.java +++ b/services/s3/src/test/java/software/amazon/awssdk/services/s3/internal/settingproviders/ProfileUseArnRegionProviderTest.java @@ -81,6 +81,20 @@ public void commaNoSpace_shouldResolveCorrectly() { assertThat(provider.resolveUseArnRegion()).isEqualTo(Optional.of(FALSE)); } + @Test + public void resolveUseArnRegion_specifiedMultipleValuesInConfigFile_shouldResolveOncePerCall() { + String trueConfigFile = getClass().getResource("ProfileFile_true").getFile(); + System.setProperty(AWS_CONFIG_FILE.property(), trueConfigFile); + + assertThat(provider.resolveUseArnRegion()).isEqualTo(Optional.of(TRUE)); + + System.clearProperty(AWS_CONFIG_FILE.property()); + String falseConfigFile = getClass().getResource("ProfileFile_false").getFile(); + System.setProperty(AWS_CONFIG_FILE.property(), falseConfigFile); + + assertThat(provider.resolveUseArnRegion()).isEqualTo(Optional.of(FALSE)); + } + @Test public void specifiedInOverrideConfig_shouldUse() { ExecutionInterceptor interceptor = Mockito.spy(AbstractExecutionInterceptor.class); diff --git a/services/s3/src/test/java/software/amazon/awssdk/services/s3/internal/settingproviders/UseArnRegionProviderChainTest.java b/services/s3/src/test/java/software/amazon/awssdk/services/s3/internal/settingproviders/UseArnRegionProviderChainTest.java index 5f1e72b77492..43bede52b75a 100644 --- a/services/s3/src/test/java/software/amazon/awssdk/services/s3/internal/settingproviders/UseArnRegionProviderChainTest.java +++ b/services/s3/src/test/java/software/amazon/awssdk/services/s3/internal/settingproviders/UseArnRegionProviderChainTest.java @@ -24,23 +24,23 @@ import org.junit.jupiter.api.Test; import software.amazon.awssdk.testutils.EnvironmentVariableHelper; -public class UseArnRegionProviderChainTest { +class UseArnRegionProviderChainTest { private final EnvironmentVariableHelper helper = new EnvironmentVariableHelper(); @AfterEach - public void clearSystemProperty() { + void clearSystemProperty() { System.clearProperty(AWS_S3_USE_ARN_REGION.property()); System.clearProperty(AWS_CONFIG_FILE.property()); helper.reset(); } @Test - public void notSpecified_shouldReturnEmptyOptional() { + void notSpecified_shouldReturnEmptyOptional() { assertThat(UseArnRegionProviderChain.create().resolveUseArnRegion()).isEqualTo(Optional.empty()); } @Test - public void specifiedInBothProviders_systemPropertiesShouldTakePrecedence() { + void specifiedInBothProviders_systemPropertiesShouldTakePrecedence() { System.setProperty(AWS_S3_USE_ARN_REGION.property(), "false"); String configFile = getClass().getResource("ProfileFile_true").getFile(); System.setProperty(AWS_CONFIG_FILE.property(), configFile); @@ -49,7 +49,7 @@ public void specifiedInBothProviders_systemPropertiesShouldTakePrecedence() { } @Test - public void systemPropertiesThrowException_shouldUseConfigFile() { + void systemPropertiesThrowException_shouldUseConfigFile() { System.setProperty(AWS_S3_USE_ARN_REGION.property(), "foobar"); String configFile = getClass().getResource("ProfileFile_true").getFile(); System.setProperty(AWS_CONFIG_FILE.property(), configFile); @@ -58,7 +58,25 @@ public void systemPropertiesThrowException_shouldUseConfigFile() { } @Test - public void bothProvidersThrowException_shouldReturnEmpty() { + void resolveUseArnRegion_systemPropertiesNotSpecifiedConfigFileValueTrue_resolvesOncePerCall() { + String trueConfigFile = getClass().getResource("ProfileFile_true").getFile(); + System.setProperty(AWS_CONFIG_FILE.property(), trueConfigFile); + + UseArnRegionProviderChain providerChain = UseArnRegionProviderChain.create(); + assertThat(providerChain.resolveUseArnRegion()).isEqualTo(Optional.of(Boolean.TRUE)); + } + + @Test + void resolveUseArnRegion_systemPropertiesNotSpecifiedConfigFileValueFalse_resolvesOncePerCall() { + String falseConfigFile = getClass().getResource("ProfileFile_false").getFile(); + System.setProperty(AWS_CONFIG_FILE.property(), falseConfigFile); + + UseArnRegionProviderChain providerChain = UseArnRegionProviderChain.create(); + assertThat(providerChain.resolveUseArnRegion()).isEqualTo(Optional.of(Boolean.FALSE)); + } + + @Test + void bothProvidersThrowException_shouldReturnEmpty() { System.setProperty(AWS_S3_USE_ARN_REGION.property(), "foobar"); String configFile = getClass().getResource("ProfileFile_unsupportedValue").getFile(); System.setProperty(AWS_CONFIG_FILE.property(), configFile); diff --git a/services/s3control/src/main/java/software/amazon/awssdk/services/s3control/S3ControlConfiguration.java b/services/s3control/src/main/java/software/amazon/awssdk/services/s3control/S3ControlConfiguration.java index c6d89635811e..00e0d3199ee9 100644 --- a/services/s3control/src/main/java/software/amazon/awssdk/services/s3control/S3ControlConfiguration.java +++ b/services/s3control/src/main/java/software/amazon/awssdk/services/s3control/S3ControlConfiguration.java @@ -15,17 +15,20 @@ package software.amazon.awssdk.services.s3control; +import java.util.Optional; +import java.util.function.Supplier; import software.amazon.awssdk.annotations.Immutable; import software.amazon.awssdk.annotations.NotThreadSafe; import software.amazon.awssdk.annotations.SdkPublicApi; import software.amazon.awssdk.annotations.ThreadSafe; import software.amazon.awssdk.core.ServiceConfiguration; import software.amazon.awssdk.profiles.ProfileFile; +import software.amazon.awssdk.profiles.ProfileFileSupplier; import software.amazon.awssdk.utils.builder.CopyableBuilder; import software.amazon.awssdk.utils.builder.ToCopyableBuilder; /** - * S3 Control specific configuration allowing customers to enabled FIPS or + * S3 Control specific configuration allowing customers to enable FIPS or * dualstack. */ @SdkPublicApi @@ -48,7 +51,7 @@ public final class S3ControlConfiguration implements ServiceConfiguration, private final Boolean fipsModeEnabled; private final Boolean dualstackEnabled; private final Boolean useArnRegionEnabled; - private final ProfileFile profileFile; + private final Supplier profileFile; private final String profileName; private S3ControlConfiguration(DefaultS3ServiceConfigurationBuilder builder) { @@ -175,6 +178,13 @@ public interface Builder extends CopyableBuilder profileFileSupplier(); + + /** + * The profile file supplier that should be consulted to determine the service-specific default configuration. + */ + Builder profileFile(Supplier profileFile); + String profileName(); /** @@ -188,7 +198,7 @@ private static final class DefaultS3ServiceConfigurationBuilder implements Build private Boolean dualstackEnabled; private Boolean fipsModeEnabled; - private ProfileFile profileFile; + private Supplier profileFile; private String profileName; private Boolean useArnRegionEnabled; @@ -239,11 +249,25 @@ public void setUseArnRegionEnabled(Boolean useArnRegionEnabled) { @Override public ProfileFile profileFile() { - return profileFile; + return Optional.ofNullable(profileFile) + .map(Supplier::get) + .orElse(null); } @Override public Builder profileFile(ProfileFile profileFile) { + return profileFile(Optional.ofNullable(profileFile) + .map(ProfileFileSupplier::fixedProfileFile) + .orElse(null)); + } + + @Override + public Supplier profileFileSupplier() { + return profileFile; + } + + @Override + public Builder profileFile(Supplier profileFile) { this.profileFile = profileFile; return this; } diff --git a/services/ssooidc/src/main/java/software/amazon/awssdk/services/ssooidc/SsoOidcProfileTokenProviderFactory.java b/services/ssooidc/src/main/java/software/amazon/awssdk/services/ssooidc/SsoOidcProfileTokenProviderFactory.java index 2c39a58f5939..e5f1bd224d3e 100644 --- a/services/ssooidc/src/main/java/software/amazon/awssdk/services/ssooidc/SsoOidcProfileTokenProviderFactory.java +++ b/services/ssooidc/src/main/java/software/amazon/awssdk/services/ssooidc/SsoOidcProfileTokenProviderFactory.java @@ -15,11 +15,14 @@ package software.amazon.awssdk.services.ssooidc; +import java.util.Objects; +import java.util.function.Supplier; import software.amazon.awssdk.annotations.SdkProtectedApi; import software.amazon.awssdk.auth.credentials.AnonymousCredentialsProvider; import software.amazon.awssdk.auth.token.credentials.ChildProfileTokenProviderFactory; import software.amazon.awssdk.auth.token.credentials.SdkToken; import software.amazon.awssdk.auth.token.credentials.SdkTokenProvider; +import software.amazon.awssdk.core.exception.SdkClientException; import software.amazon.awssdk.profiles.Profile; import software.amazon.awssdk.profiles.ProfileFile; import software.amazon.awssdk.profiles.ProfileProperty; @@ -39,7 +42,59 @@ public final class SsoOidcProfileTokenProviderFactory implements ChildProfileTok @Override public SdkTokenProvider create(ProfileFile profileFile, Profile profile) { - return new SsooidcProfileTokenProvider(profileFile, profile); + return new SsoOidcProfileTokenProvider(profileFile, profile); + } + + public SdkTokenProvider create(Supplier profileFile, String profileName) { + return new SsoOidcSuppliedProfileTokenProvider(profileFile, profileName); + } + + private static final class SsoOidcSuppliedProfileTokenProvider implements SdkTokenProvider, SdkAutoCloseable { + + private final Supplier profileFile; + private final String profileName; + private volatile ProfileFile currentProfileFile; + private volatile SsoOidcProfileTokenProvider currentTokenProvider; + + private SsoOidcSuppliedProfileTokenProvider(Supplier profileFile, String profileName) { + this.profileFile = profileFile; + this.profileName = profileName; + } + + @Override + public SdkToken resolveToken() { + return sdkTokenProvider().resolveToken(); + } + + private SdkTokenProvider sdkTokenProvider() { + ProfileFile profileFileInstance = profileFile.get(); + if (!Objects.equals(profileFileInstance, currentProfileFile)) { + synchronized (this) { + if (!Objects.equals(profileFileInstance, currentProfileFile)) { + Profile profileInstance = resolveProfile(profileFileInstance, profileName); + currentProfileFile = profileFileInstance; + currentTokenProvider = new SsoOidcProfileTokenProvider(profileFileInstance, profileInstance); + } + } + } + + return currentTokenProvider; + } + + 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(); + }); + } + + @Override + public void close() { + IoUtils.closeQuietly(currentTokenProvider, null); + } } @@ -47,10 +102,10 @@ public SdkTokenProvider create(ProfileFile profileFile, Profile profile) { * A wrapper for a {@link SdkTokenProvider} that is returned by this factory when {@link #create(ProfileFile,Profile)} is * invoked. This wrapper is important because it ensures the token provider is closed when it is no longer needed. */ - private static final class SsooidcProfileTokenProvider implements SdkTokenProvider, SdkAutoCloseable { + private static final class SsoOidcProfileTokenProvider implements SdkTokenProvider, SdkAutoCloseable { private final SsoOidcTokenProvider sdkTokenProvider; - private SsooidcProfileTokenProvider(ProfileFile profileFile, Profile profile) { + private SsoOidcProfileTokenProvider(ProfileFile profileFile, Profile profile) { String profileSsoSectionName = profile.property( ProfileSection.SSO_SESSION .getPropertyKeyName()).orElseThrow(() -> new IllegalStateException( diff --git a/services/ssooidc/src/test/java/software/amazon/awssdk/services/ssooidc/internal/SsoOidcProfileTokenProviderFactoryTest.java b/services/ssooidc/src/test/java/software/amazon/awssdk/services/ssooidc/internal/SsoOidcProfileTokenProviderFactoryTest.java index c7051474c6e9..0abb69f3bf4c 100644 --- a/services/ssooidc/src/test/java/software/amazon/awssdk/services/ssooidc/internal/SsoOidcProfileTokenProviderFactoryTest.java +++ b/services/ssooidc/src/test/java/software/amazon/awssdk/services/ssooidc/internal/SsoOidcProfileTokenProviderFactoryTest.java @@ -60,8 +60,8 @@ private static Stream ssoSessionMissingProperties() { void create_throwsExceptionIfRegionNotPassed() { String startUrl = "https://my-start-url.com"; Assertions.assertThatExceptionOfType(NullPointerException.class).isThrownBy( - () -> SdkTokenProviderFactoryProperties.builder(). - startUrl(startUrl) + () -> SdkTokenProviderFactoryProperties.builder() + .startUrl(startUrl) .build() ).withMessage("region must not be null."); } @@ -71,8 +71,8 @@ void create_throwsExceptionIfStartUrlNotPassed() { String region = "test-region"; Assertions.assertThatExceptionOfType(NullPointerException.class).isThrownBy( - () -> SdkTokenProviderFactoryProperties.builder(). - region(region) + () -> SdkTokenProviderFactoryProperties.builder() + .region(region) .build() ).withMessage("startUrl must not be null."); } @@ -95,7 +95,23 @@ void create_SsooidcTokenProvider_from_SsooidcSpecificProfile() { } @Test - void create_SsooidcTokenProvider_with_ssoAccountIdInProfile() { + void create_SsoOidcTokenProvider_from_SsooidcSpecificProfileSupplier() { + String profileContent = "[profile ssotoken]\n" + + "sso_session=admin\n" + + "[sso-session admin]\n" + + "sso_region=us-east-1\n" + + "sso_start_url= https://start-url\n"; + ProfileFile profiles = ProfileFile.builder() + .content(new StringInputStream(profileContent)) + .type(ProfileFile.Type.CONFIGURATION) + .build(); + SdkTokenProvider sdkTokenProvider = new SsoOidcProfileTokenProviderFactory().create(() -> profiles, "ssotoken"); + Assertions.assertThat(sdkTokenProvider).isNotNull(); + + } + + @Test + void create_SsoOidcTokenProvider_with_ssoAccountIdInProfile() { String profileContent = "[profile sso]\n" + "sso_region=us-east-1\n" + "sso_account_id=1234567\n" + @@ -111,7 +127,7 @@ void create_SsooidcTokenProvider_with_ssoAccountIdInProfile() { } @Test - void create_SsooidcTokenProvider_with_ssoRoleNameInProfile() { + void create_SsoOidcTokenProvider_with_ssoRoleNameInProfile() { String profileContent = "[profile sso]\n" + "sso_region=us-east-1\n" + "sso_role_name=ssoSpecificRole\n" + @@ -126,6 +142,22 @@ void create_SsooidcTokenProvider_with_ssoRoleNameInProfile() { } + @Test + void create_SsoOidcTokenProvider_with_ssoRoleNameInProfileSupplier() { + String profileContent = "[profile sso]\n" + + "sso_region=us-east-1\n" + + "sso_role_name=ssoSpecificRole\n" + + "sso_start_url= https://start-url\n"; + ProfileFile profiles = ProfileFile.builder() + .content(new StringInputStream(profileContent)) + .type(ProfileFile.Type.CONFIGURATION) + .build(); + + Assertions.assertThatNoException() + .isThrownBy(() -> new SsoOidcProfileTokenProviderFactory().create(() -> profiles, "sso")); + + } + @ParameterizedTest @MethodSource("ssoSessionMissingProperties") void incorrectSsoProperties_throwsException(String ssoProfileContent, String msg) { diff --git a/services/ssooidc/src/test/java/software/amazon/awssdk/services/ssooidc/internal/common/SsoOidcTokenRefreshTestBase.java b/services/ssooidc/src/test/java/software/amazon/awssdk/services/ssooidc/internal/common/SsoOidcTokenRefreshTestBase.java index d925d448b4f1..4d43bd154426 100644 --- a/services/ssooidc/src/test/java/software/amazon/awssdk/services/ssooidc/internal/common/SsoOidcTokenRefreshTestBase.java +++ b/services/ssooidc/src/test/java/software/amazon/awssdk/services/ssooidc/internal/common/SsoOidcTokenRefreshTestBase.java @@ -20,7 +20,6 @@ import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; -import static software.amazon.awssdk.services.ssooidc.internal.SsoOidcTokenProviderTest.START_URL; import static software.amazon.awssdk.services.ssooidc.internal.SsoOidcTokenProviderTest.deriveCacheKey; import static software.amazon.awssdk.utils.UserHomeDirectoryUtils.userHomeDirectory; @@ -41,7 +40,6 @@ import software.amazon.awssdk.auth.token.credentials.SdkTokenProvider; import software.amazon.awssdk.auth.token.internal.ProfileTokenProviderLoader; import software.amazon.awssdk.core.exception.SdkClientException; -import software.amazon.awssdk.profiles.Profile; import software.amazon.awssdk.profiles.ProfileFile; import software.amazon.awssdk.protocols.jsoncore.JsonNode; import software.amazon.awssdk.services.ssooidc.SsoOidcClient; @@ -59,7 +57,7 @@ public abstract class SsoOidcTokenRefreshTestBase { protected boolean shouldMockServiceClient; protected SsoOidcClient ssoOidcClient; protected String testStartUrl; - protected String testSessionName = "sso-prod";; + protected String testSessionName = "sso-prod"; protected String baseTokenResourceFile; protected ProfileTokenProviderLoader profileTokenProviderLoader; @@ -98,12 +96,11 @@ protected void initializeProfileProperties() { "sso_region=us-east-1\n" + "sso_start_url=" + testStartUrl + "\n"; - ProfileFile profiles = ProfileFile.builder() + ProfileFile profile = ProfileFile.builder() .content(new StringInputStream(profileContent)) .type(ProfileFile.Type.CONFIGURATION) .build(); - Optional profile = profiles.profile("sso-refresh"); - profileTokenProviderLoader = new ProfileTokenProviderLoader(profiles, profile.get()); + profileTokenProviderLoader = new ProfileTokenProviderLoader(() -> profile, "sso-refresh"); } @Test diff --git a/test/auth-tests/src/it/java/software/amazon/awssdk/auth/ssooidc/ProfileTokenProviderLoaderTest.java b/test/auth-tests/src/it/java/software/amazon/awssdk/auth/ssooidc/ProfileTokenProviderLoaderTest.java index 7a7540c8081e..be3e93eef82c 100644 --- a/test/auth-tests/src/it/java/software/amazon/awssdk/auth/ssooidc/ProfileTokenProviderLoaderTest.java +++ b/test/auth-tests/src/it/java/software/amazon/awssdk/auth/ssooidc/ProfileTokenProviderLoaderTest.java @@ -17,10 +17,11 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.assertj.core.api.Assertions.assertThatNoException; import static org.assertj.core.api.Assertions.assertThatThrownBy; -import java.util.HashMap; import java.util.Optional; +import java.util.function.Supplier; import java.util.stream.Stream; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; @@ -29,45 +30,50 @@ import software.amazon.awssdk.auth.token.credentials.SdkTokenProvider; import software.amazon.awssdk.auth.token.internal.ProfileTokenProviderLoader; import software.amazon.awssdk.core.exception.SdkClientException; -import software.amazon.awssdk.profiles.Profile; import software.amazon.awssdk.profiles.ProfileFile; import software.amazon.awssdk.utils.StringInputStream; -public class ProfileTokenProviderLoaderTest { +class ProfileTokenProviderLoaderTest { @Test - public void noProfile_throwsException() { - assertThatThrownBy(() -> new ProfileTokenProviderLoader(ProfileFile.defaultProfileFile(), null)) - .hasMessageContaining("profile must not be null"); + void profileTokenProviderLoader_noProfileFileSupplier_throwsException() { + assertThatThrownBy(() -> new ProfileTokenProviderLoader(null, "sso'")) + .hasMessageContaining("profileFile must not be null"); } @Test - public void noProfileFile_throwsException() { - assertThatThrownBy(() -> new ProfileTokenProviderLoader(null, Profile.builder().name("sso").properties(new HashMap<>()).build())) - .hasMessageContaining("profileFile must not be null"); + void profileTokenProviderLoader_noProfileName_throwsException() { + assertThatThrownBy(() -> new ProfileTokenProviderLoader(ProfileFile::defaultProfileFile, null)) + .hasMessageContaining("profileName must not be null"); } @ParameterizedTest @MethodSource("ssoErrorValues") - public void incorrectSsoProperties_throwsException(String profileContent, String msg) { + void incorrectSsoProperties_supplier_delaysThrowingExceptionUntilResolvingToken(String profileContent, String msg) { ProfileFile profileFile = configFile(profileContent); + Supplier supplier = () -> profileFile; + + ProfileTokenProviderLoader providerLoader = new ProfileTokenProviderLoader(supplier, "sso"); + + assertThatNoException().isThrownBy(providerLoader::tokenProvider); - assertThat(profileFile.profile("sso")).hasValueSatisfying(profile -> { - ProfileTokenProviderLoader providerLoader = new ProfileTokenProviderLoader(profileFile, profile); - assertThatExceptionOfType(IllegalArgumentException.class).isThrownBy(providerLoader::tokenProvider).withMessageContaining(msg); + assertThat(providerLoader.tokenProvider()).satisfies(tokenProviderOptional -> { + assertThat(tokenProviderOptional).isPresent().get().satisfies(tokenProvider-> { + assertThatExceptionOfType(IllegalArgumentException.class).isThrownBy(tokenProvider::resolveToken) + .withMessageContaining(msg); + }); }); } @Test - public void correctSsoProperties_createsTokenProvider() { + void correctSsoProperties_createsTokenProvider() { String profileContent = "[profile sso]\n" + "sso_session=admin\n" + "[sso-session admin]\n" + "sso_region=us-east-1\n" + "sso_start_url=https://d-abc123.awsapps.com/start\n"; - Optional ssoProfile = configFile(profileContent).profile("sso"); - ProfileTokenProviderLoader providerLoader = new ProfileTokenProviderLoader(configFile(profileContent), ssoProfile.get()); + ProfileTokenProviderLoader providerLoader = new ProfileTokenProviderLoader(() -> configFile(profileContent), "sso"); Optional tokenProvider = providerLoader.tokenProvider(); assertThat(tokenProvider).isPresent(); assertThatThrownBy(() -> tokenProvider.get().resolveToken())