-
Notifications
You must be signed in to change notification settings - Fork 919
Add ec2InstanceProfileName configuration #6208
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
{ | ||
"type": "feature", | ||
"category": "AWS SDK for Java v2", | ||
"contributor": "", | ||
"description": "Adding ec2InstanceProfileName configuration to specify IMDS instance profile for retrieving credentials." | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -18,6 +18,7 @@ | |
import static java.time.temporal.ChronoUnit.MINUTES; | ||
import static software.amazon.awssdk.utils.ComparableUtils.maximum; | ||
import static software.amazon.awssdk.utils.FunctionalUtils.invokeSafely; | ||
import static software.amazon.awssdk.utils.StringUtils.isBlank; | ||
import static software.amazon.awssdk.utils.cache.CachedSupplier.StaleValueBehavior.ALLOW; | ||
|
||
import java.net.URI; | ||
|
@@ -103,6 +104,8 @@ private enum ApiVersion { | |
private final Supplier<ProfileFile> profileFile; | ||
|
||
private final String profileName; | ||
|
||
private final String ec2InstanceProfileName; | ||
|
||
private final Duration staleTime; | ||
|
||
|
@@ -118,6 +121,13 @@ private InstanceProfileCredentialsProvider(BuilderImpl builder) { | |
.orElseGet(() -> ProfileFileSupplier.fixedProfileFile(ProfileFile.defaultProfileFile())); | ||
this.profileName = Optional.ofNullable(builder.profileName) | ||
.orElseGet(ProfileFileSystemSetting.AWS_PROFILE::getStringValueOrThrow); | ||
this.ec2InstanceProfileName = builder.ec2InstanceProfileName; | ||
|
||
if (isBlank(ec2InstanceProfileName) && ec2InstanceProfileName != null) { | ||
throw SdkClientException.builder() | ||
.message("ec2InstanceProfileName cannot be blank") | ||
.build(); | ||
} | ||
|
||
this.httpCredentialsLoader = HttpCredentialsLoader.create(PROVIDER_NAME); | ||
this.configProvider = | ||
|
@@ -173,6 +183,11 @@ private RefreshResult<AwsCredentials> refreshCredentials() { | |
|
||
try { | ||
LoadedCredentials credentials = httpCredentialsLoader.loadCredentials(createEndpointProvider()); | ||
|
||
if (apiVersion == ApiVersion.UNKNOWN) { | ||
apiVersion = ApiVersion.EXTENDED; | ||
} | ||
Comment on lines
+187
to
+189
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What is the purpose of this change? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Initially, apiVersion is set to UNKNOWN. When ec2InstanceProfileName is set by the user, we skip the first GET request to IMDS in the getSecurityCredentials() method. In refreshCredentials(), the code attempts to load credentials using the extended API endpoint first. If successful, we update apiVersion to EXTENDED. This serves two purposes - It records that the extended API is supported for future reference so it prevents unnecessary attempts to use the legacy API in subsequent calls, as we now know the extended API works.(This is from IMDS SEP 2.1) |
||
|
||
Instant expiration = credentials.getExpiration().orElse(null); | ||
log.debug(() -> "Loaded credentials from IMDS with expiration time of " + expiration); | ||
|
||
|
@@ -186,19 +201,29 @@ private RefreshResult<AwsCredentials> refreshCredentials() { | |
} catch (Ec2MetadataClientException e) { | ||
if (e.statusCode() == 404) { | ||
log.debug(() -> "Resolved profile is no longer available. Resetting it and trying again."); | ||
resolvedProfile = null; | ||
|
||
if (apiVersion == ApiVersion.UNKNOWN) { | ||
apiVersion = ApiVersion.LEGACY; | ||
return refreshCredentials(); | ||
} else if (resolveProfileName() == null) { | ||
// Resolved profile name is invalid, reset it and try again | ||
resolvedProfile = null; | ||
|
||
profileRetryCount++; | ||
if (profileRetryCount <= MAX_PROFILE_RETRIES) { | ||
log.debug(() -> "Profile name not found, retrying fetching the profile name again."); | ||
return refreshCredentials(); | ||
} | ||
} else { | ||
String profileName = resolveProfileName(); | ||
throw SdkClientException.builder() | ||
.message(String.format("Invalid EC2 instance profile name: '%s'. " + | ||
"Verify that the profile exists and that your instance " + | ||
"has permission to access it. ", | ||
profileName)) | ||
.cause(e) | ||
.build(); | ||
} | ||
|
||
profileRetryCount++; | ||
if (profileRetryCount <= MAX_PROFILE_RETRIES) { | ||
log.debug(() -> "Profile name not found, retrying fetching the profile name again."); | ||
return refreshCredentials(); | ||
} | ||
throw SdkClientException.create(FAILED_TO_LOAD_CREDENTIALS_ERROR, e); | ||
} | ||
throw SdkClientException.create(FAILED_TO_LOAD_CREDENTIALS_ERROR, e); | ||
} catch (RuntimeException e) { | ||
|
@@ -327,7 +352,18 @@ private boolean isInsecureFallbackDisabled() { | |
return configProvider.isMetadataV1Disabled(); | ||
} | ||
|
||
private String resolveProfileName() { | ||
return ec2InstanceProfileName != null ? | ||
ec2InstanceProfileName : | ||
configProvider.ec2InstanceProfileName(); | ||
} | ||
|
||
private String[] getSecurityCredentials(String imdsHostname, String metadataToken) { | ||
String profileName = resolveProfileName(); | ||
if (profileName != null) { | ||
return new String[]{profileName}; | ||
} | ||
|
||
if (resolvedProfile != null) { | ||
return new String[]{resolvedProfile}; | ||
} | ||
|
@@ -383,6 +419,16 @@ public Builder toBuilder() { | |
*/ | ||
public interface Builder extends HttpCredentialsProvider.Builder<InstanceProfileCredentialsProvider, Builder>, | ||
CopyableBuilder<Builder, InstanceProfileCredentialsProvider> { | ||
/** | ||
* Configure the EC2 instance profile name to use for retrieving credentials. | ||
* | ||
* <p>When this is set, the provider will skip fetching the list of available instance profiles | ||
* and use this name directly. | ||
* | ||
* @param ec2InstanceProfileName The EC2 instance profile name to use | ||
*/ | ||
Builder ec2InstanceProfileName(String ec2InstanceProfileName); | ||
|
||
/** | ||
* Configure the profile file used for loading IMDS-related configuration, like the endpoint mode (IPv4 vs IPv6). | ||
* | ||
|
@@ -434,6 +480,7 @@ static final class BuilderImpl implements Builder { | |
private String asyncThreadName; | ||
private Supplier<ProfileFile> profileFile; | ||
private String profileName; | ||
private String ec2InstanceProfileName; | ||
private Duration staleTime; | ||
|
||
private BuilderImpl() { | ||
|
@@ -447,6 +494,7 @@ private BuilderImpl(InstanceProfileCredentialsProvider provider) { | |
this.asyncThreadName = provider.asyncThreadName; | ||
this.profileFile = provider.profileFile; | ||
this.profileName = provider.profileName; | ||
this.ec2InstanceProfileName = provider.ec2InstanceProfileName; | ||
this.staleTime = provider.staleTime; | ||
} | ||
|
||
|
@@ -515,6 +563,17 @@ public Builder profileName(String profileName) { | |
public void setProfileName(String profileName) { | ||
profileName(profileName); | ||
} | ||
|
||
@Override | ||
public Builder ec2InstanceProfileName(String ec2InstanceProfileName) { | ||
this.ec2InstanceProfileName = ec2InstanceProfileName; | ||
return this; | ||
} | ||
|
||
public void setEc2InstanceProfileName(String ec2InstanceProfileName) { | ||
ec2InstanceProfileName(ec2InstanceProfileName); | ||
} | ||
|
||
|
||
@Override | ||
public Builder staleTime(Duration duration) { | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -46,12 +46,14 @@ public final class Ec2MetadataConfigProvider { | |
|
||
private final Lazy<Boolean> metadataV1Disabled; | ||
private final Lazy<Long> serviceTimeout; | ||
private final Lazy<String> ec2InstanceProfileName; | ||
|
||
private Ec2MetadataConfigProvider(Builder builder) { | ||
this.profileFile = builder.profileFile; | ||
this.profileName = builder.profileName; | ||
this.metadataV1Disabled = new Lazy<>(this::resolveMetadataV1Disabled); | ||
this.serviceTimeout = new Lazy<>(this::resolveServiceTimeout); | ||
this.ec2InstanceProfileName = new Lazy<>(this::resolveEc2InstanceProfileName); | ||
} | ||
|
||
public enum EndpointMode { | ||
|
@@ -127,6 +129,14 @@ public boolean isMetadataV1Disabled() { | |
public long serviceTimeout() { | ||
return serviceTimeout.getValue(); | ||
} | ||
|
||
/** | ||
* Resolves the EC2 Instance Profile Name to use. | ||
* @return the EC2 Instance Profile Name or null if not specified. | ||
*/ | ||
public String ec2InstanceProfileName() { | ||
return ec2InstanceProfileName.getValue(); | ||
} | ||
|
||
// Internal resolution logic for Metadata V1 disabled | ||
private boolean resolveMetadataV1Disabled() { | ||
|
@@ -146,6 +156,14 @@ private long resolveServiceTimeout() { | |
.orElseGet(() -> parseTimeoutValue(SdkSystemSetting.AWS_METADATA_SERVICE_TIMEOUT.defaultValue())); | ||
} | ||
|
||
private String resolveEc2InstanceProfileName() { | ||
return OptionalUtils.firstPresent( | ||
fromSystemSettingsEc2InstanceProfileName(), | ||
() -> fromProfileFileEc2InstanceProfileName(profileFile, profileName) | ||
) | ||
.orElse(null); | ||
} | ||
|
||
// System settings resolution for Metadata V1 disabled | ||
private static Optional<Boolean> fromSystemSettingsMetadataV1Disabled() { | ||
return SdkSystemSetting.AWS_EC2_METADATA_V1_DISABLED.getBooleanValue(); | ||
|
@@ -171,6 +189,22 @@ private static Optional<Long> fromProfileFileServiceTimeout(Supplier<ProfileFile | |
.flatMap(p -> p.property(ProfileProperty.METADATA_SERVICE_TIMEOUT)) | ||
.map(Ec2MetadataConfigProvider::parseTimeoutValue); | ||
} | ||
|
||
// System settings resolution for EC2 Instance Profile Name | ||
private static Optional<String> fromSystemSettingsEc2InstanceProfileName() { | ||
return SdkSystemSetting.AWS_EC2_INSTANCE_PROFILE_NAME.getNonDefaultStringValue(); | ||
} | ||
|
||
// Profile file resolution for EC2 Instance Profile Name | ||
private static Optional<String> fromProfileFileEc2InstanceProfileName(Supplier<ProfileFile> profileFile, String profileName) { | ||
try { | ||
return profileFile.get() | ||
.profile(profileName) | ||
.flatMap(p -> p.property(ProfileProperty.EC2_INSTANCE_PROFILE_NAME)); | ||
} catch (Exception e) { | ||
return Optional.empty(); | ||
} | ||
Comment on lines
+204
to
+206
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What case is this guarding against? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is guarding against exceptions from aggregated profile suppliers, particularly NoSuchElementException like when there are invalid profiles or when suppliers throw exceptions during normal operation. |
||
} | ||
|
||
// Parses a timeout value from a string to milliseconds | ||
private static long parseTimeoutValue(String timeoutValue) { | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This can be simplified to
StringUtils.isWhitespace(ec2InstanceProfileName)
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmm, the StringUtils(software.amazon.awssdk.utils.StringUtils) doesn't have "isWhitespace" method, it only has "isBlank" method.
Apache Commons Lang StringUtils (from org.apache.commons.lang3.StringUtils) does have an isWhitespace method, but it's not available for this module.