-
Notifications
You must be signed in to change notification settings - Fork 863
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
ProfileCredentialsProvider can now reload credentials when profile files change #3487
ProfileCredentialsProvider can now reload credentials when profile files change #3487
Conversation
Kudos, SonarCloud Quality Gate passed!  |
private final AtomicReference<AwsCredentials> credentials; | ||
private final RuntimeException loadException; | ||
|
||
private final ProfileFile profileFile; | ||
private final AtomicReference<ProfileFile> profileFile; |
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.
We don't seem to perform any CAS operations on these so jusing volatile
is sufficient
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.
The credentials
reference was changed to volatile AwsCredentials
.
The reference to profileFile
was switched for a refresher object as suggested with another comment.
* @return Either the same instance in case disk has no recent updates, or a newly constructed instance. | ||
* @see #isStale() | ||
*/ | ||
public ProfileFile reload() { |
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.
I think we should extract this functionality out this class. ProfileFile
is meant to be immutable so it seems strange that it's able to do this reloading. Moving this functionality out to somewhere else will help simplify things as well since the scope of responsibility will be small
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.
Agreed. I've created a ProfileFileRefresher
class and refactored some of the code into it.
if (needToReloadCredentials()) { | ||
synchronized (this) { | ||
ProfileFile previousProfileFile = profileFile.get(); | ||
ProfileFile newProfileFile = previousProfileFile.reload(); | ||
profileFile.set(newProfileFile); | ||
|
||
AwsCredentialsProvider credentialsProvider = createCredentialsProvider(newProfileFile, this.profileName); | ||
nextCredentials = credentialsProvider.resolveCredentials(); | ||
credentials.set(nextCredentials); | ||
|
||
// The delegate credentials provider may be closeable (eg. if it's an STS credentials provider). In this case, | ||
// we should clean it up when this credentials provider is closed. | ||
IoUtils.closeIfCloseable(credentialsProvider, null); | ||
} | ||
staleTime = staleTime(now); | ||
} else { | ||
nextCredentials = credentials.get(); | ||
staleTime = pollTime(now); | ||
} | ||
|
||
return RefreshResult.builder(nextCredentials) | ||
.staleTime(staleTime) | ||
.build(); | ||
} |
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.
There's a lot going on here that should be simplified once the reloading functionality is moved to a separate class. That way, you don't need to continuously update the profileFile
. Instead, you can maintain a final reference to the file loader, and load the file whenever you need.
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 is similar to something we did recently with Bearer token support:
Line 70 in 5845142
this.tokenRefresher = CachedTokenRefresher.builder() |
which required refreshing credentials from a file
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.
Thanks for the example. This helped a lot.
public boolean isStale() { | ||
return isStaleIfAny(diskFileWasModifiedSinceLoad()); | ||
} | ||
|
||
/** | ||
* Checks if the profile files have been changed on disk as of a particular instant. | ||
* | ||
* @param instant The time to check against for any file modifications. | ||
* @return True if any files used to build this object have been modified. | ||
* @see #isStale() | ||
*/ | ||
public boolean isStaleAsOf(Instant instant) { | ||
return isStaleIfAny(details -> details.wasBuiltBefore(instant)); | ||
} |
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.
Can these just be properties or internal details on the refresher class? IMO ProfileFile
shouldn't have any concept of staleness. It should remain a relatively simple class that just contains Profile
s and that's it.
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.
Moved this logic to ProfileFileRefresher
and new class ReloadingProfileCredentialsProvider
private ProfileFile registerBuildDetails(Builder.BuildDetails details) { | ||
this.buildDetails.add(details); | ||
return this; | ||
} | ||
|
||
private ProfileFile registerBuildDetails(Collection<Builder.BuildDetails> details) { | ||
this.buildDetails.addAll(details); | ||
return this; | ||
} |
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.
"register" is not really a verb we use in the codebase. You can take a look at our coding guidelines here https://github.com/aws/aws-sdk-java-v2/blob/master/docs/design/ClientConfiguration.md
probably something like addBuildDetails
and buildDetails
respectively.
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.
Updated method names.
* @see ProfileFile | ||
*/ | ||
@SdkPublicApi | ||
public final class ReloadingProfileCredentialsProvider |
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.
I'm a bit confused. Why do we create a new credentials provider? I think we should keep it in ProfileCredentialsProvider
so that customers using the default chain get the benefit right?
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.
Pushed new commit.
/** | ||
* Define the condition for reloading a profile file. | ||
* | ||
* @param predicate Predicate for determining whether to execute the profileFileSupplier. | ||
*/ | ||
Builder profileFileReloadPredicate(Predicate<ProfileFileRefresher.ProfileFileRefreshRecord> predicate); |
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.
I don't think this is the right API for this. The reloading logic should be fully encapsulated in the Supplier<ProfileFile>
, similar to ContentStreamProvider
. This allows us to simplify ProfileFileCredentialsProvider
because it doesn't need to contain all the reloading logic; it can be externalized. Then the credentials provider can just call profileFileSupplier.get()
whenever it needs a profile file and any reloading will be taken care of internally (or not) by the supplier.
We could think about creating a separate functional API and hang convenience methods off it that helps customers create pre-canned suppliers; for example
public interface ProfileFileSupplier {
ProfileFile profileFile();
public static ProfileFileSupplier reloadWhenModified(Path profileFile) {
...
}
}
This is similar to AsyncRequestBody
.
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.
A good example actually is AsyncRequestBody#fromFile
where customer cans just create an AsyncRequestBody
by providing a file path, and an instance of AsyncRequestBody
will be created for the customer. Internally, it delegates to FileAsyncRequestBody
which does the heavy lifting of turning the file into a Publisher
.
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.
Created interface ProfileFileSupplier
public static Predicate<ProfileFileRefresher.ProfileFileRefreshRecord> wasRefreshedBeforeFileModificationTime(Path path) { | ||
return refreshRecord -> refreshRecord.wasCreatedBeforeFileModified(path); | ||
} |
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.
We probably don't need this (see comment below), but in general, we can't make this a public API since ProfileFileRefresher
itself is an internal API.
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.
Agreed, removed.
} | ||
|
||
private boolean isNewProfileFile(ProfileFile profileFile) { | ||
return currentProfileFile != profileFile; |
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.
should use .equals()
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.
Agreed. Fixed.
handleProfileFileReload(cachedOrRefreshedProfileFile); | ||
} | ||
|
||
return credentials; |
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.
We should cache the provider created in updateCredentials
, rather than the credentials. This is because depending on the credential provider that's created, the credentials can still change without the profile file changing; e.g. for assume rule.
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.
Sounds good. Change made.
*/ | ||
@SdkPublicApi | ||
@FunctionalInterface | ||
public interface ProfileFileSupplier extends SdkAutoCloseable { |
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.
Let's do a mini surface API review for this.
/** | ||
* @return A ProfileFile instance. | ||
*/ | ||
ProfileFile getProfileFile(); |
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.
nit just profileFile
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.
Updated.
* Creates a {@link ProfileFileSupplier} capable of producing multiple profile objects from a file. See | ||
* {@link ProfileFileSupplier#builder()} to create a customized implementation. |
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.
Can we add more details about what it does? Something like "Returns a new ProfileFile
when it is modified on disk"
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.
Done.
this.profileFileCache = CachedSupplier.builder(this::refreshResult) | ||
.clock(this.clock) | ||
.build(); | ||
this.emptyRefreshRecord = ProfileFileRefreshRecord.builder() |
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.
nit: looks like this can be a static constant
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.
Good catch.
} | ||
|
||
try { | ||
Instant lastModifiedInstant = Files.getLastModifiedTime(profileFilePath).toInstant(); |
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.
nice
} | ||
|
||
@Test | ||
void refreshIfStale_profileModifiedMultipleTimes_reloadsProfileFileOncePerChange() { |
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.
Nice test!
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.
Thanks!
/** | ||
* A builder for {@link ProfileFileSupplier}. | ||
*/ | ||
interface Builder { |
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.
Can we move this interface out?
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.
Since this is an internal API, removed interface completely. Only left builder implementation.
…leFile to ProfileFileRefresher
…lier or Predicate, and dealing with defaults
…rofileCredentialsProvider
…er now extends Supplier; Fixed Javadoc
074cf21
to
c088f09
Compare
SonarCloud Quality Gate failed.  |
Please make sure to run integ tests as well before merging. |
@mina-asham, the AWS Java SDK team would like to thank you for your contribution on PR #1754. Although we didn't end up merging your PR, it did serve as a starting point for functionality that we'll soon be adding. |
* ProfileCredentialsProvider can now reload credentials when profile files change (#3487) * ProfileFile can update if disk changed, reload as new instance * ProfileCredentialsProvider reloads credentials if profile file has changes * Created class ProfileFileRefresher * Created ReloadingProfileCredentialsProvider; moved new logic in ProfileFile to ProfileFileRefresher * Fix ReloadingProfileCredentialsBehavior when missing ProfileFile Supplier or Predicate, and dealing with defaults * Consolidated ReloadingProfileCredentialsProvider functionality into ProfileCredentialsProvider * Fix behavior when dealing with defaults * Created ProfileFileSupplier interface; refactored * Misc fixes * Created package-private ProfileFileSupplierBuilder; ProfileFileSupplier now extends Supplier; Fixed Javadoc * Fixed unit tests for default credentials file * Removed ProfileFileSupplier.Builder interface * Code cleanup * ProfileFileSupplier API changes, aggregating (#3558) * Added methods for aggregating ProfileFile objects * Removed redundant logic, changelog entry * Removed redundant methods * Use compare and set to make thread safe * DefaultCredentialsProvider reload profile (#3580) * Misc fixes * Reload credentials by DefaultCredentialsProvider; pass supplier to InstanceProfileCredentialsProvider * Fix code alignment * Update public APIs to Supplier<ProfileFile> instead of ProfileFileSupplier (#3607) * ProfileTokenProvider Reload ProfileFile (#3608) * Updated ProfileTokenProvider * Updated tests, do not explicitly swallow exceptions * ProfileTokenProviderLoader can use Supplier<Profilefile> internally * Simplified ProfileTokenProviderLoader API; implemented synchronized block * Use synchronized block (#3646) * S3 support classes take Supplier<ProfileFile> (#3653) * S3 support classes take Supplier<ProfileFile> * Review comments * Presigners, other Support classes take Supplier<ProfileFile> (#3677) * Presigners, other Support classes take Supplier<ProfileFile> * Split new ProfileFile tests from existing parameterized tests * Improved tests readability * ProfileFile updates to BaseClientBuilderClass, other S3 classes (#3685) * Leverage SdkClientOption and SdkExecutionAttributes; fallback to simp… (#3692) * Leverage SdkClientOption and SdkExecutionAttributes; fallback to simple ProfileFile * Addressed review comments * Updated changelog entry (#3699) * Fixed review comments * Removed unnecessary logic * Deprecated SdkClientOption PROFILE_FILE * Deprecated SdkExecutionAttribute PROFILE_FILE * Updated changelog entry
…le-plugin Pin OpenRewrite Gradle plugin version
Motivation and Context
The ProfileCredentialsProvider loads once and doesn't refresh/reload if the ProfileFile it was generated from got updated, which is unlike V1, in which the ProfileCredentialsProvider has support for this try to reload every Duration X (customizable) if the profile file it was generated from have a newer last modified date.
Should fix #1754, similar to PR #1760.
Also addresses #1327.
Modifications
Modified
ProfileFile
so that is has both the ability to detect if there are disk updates and to reload itself as a new instance. The included modifications are:Builder.BuildDetails
as a container of build information.ProfileFile
did not keep track of thePath
orInputStream
used to generate it.reload
method to return a new instance when the file has been modified, or the same instance otherwise.Modified
ProfileCredentialsProvider
for automatically reloading credentials.AtomicReference
objects.CachedSupplier
is used to avoid hitting the filesystem wheneverresolveCredentials
is called.ProfileCredentialsProvider.Builder
interface for setting credentials refreshing intervals.Testing
Unit tested
Screenshots (if appropriate)
Types of changes
Checklist
mvn install
succeedsscripts/new-change
script and following the instructions. Commit the new file created by the script in.changes/next-release
with your changes.License