-
Notifications
You must be signed in to change notification settings - Fork 83
feat: S3 Express support #1906
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
base: main
Are you sure you want to change the base?
feat: S3 Express support #1906
Conversation
...ore/AWSSDKCommon/Sources/AWSSDKCommon/FileBasedConfiguration/CRTFileBasedConfiguration.swift
Outdated
Show resolved
Hide resolved
@@ -49,7 +50,7 @@ public class AWSSigV4Signer: SmithyHTTPAuthAPI.Signer { | |||
) | |||
} | |||
|
|||
guard let identity = identity as? AWSCredentialIdentity else { | |||
guard let identity = (identity as? AWSCredentialIdentity) ?? (identity as? S3ExpressIdentity)?.awsCredentialIdentity else { |
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.
When the CRT signer signs in S3 Express mode, it intakes the S3 Express credentials using the same Credentials type as with any other signing operation. So, when an S3ExpressIdentity
is supplied, convert it to an AWS credential identity to pass into CRT (see private helper method below.)
import protocol SmithyHTTPAuthAPI.Signer | ||
import struct Smithy.Attributes | ||
|
||
public struct SigV4S3ExpressAuthScheme: AuthScheme { |
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 AuthScheme
for S3 Express is basically the sigv4 auth scheme, but with a different scheme ID & signing algorithm.
|
||
import struct Smithy.AttributeKey | ||
|
||
public enum AWSIdentityPropertyKeys { |
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.
These keys are used to pass data & components into identity resolvers in a type-safe manner.
clientConfig: DefaultClientConfiguration, | ||
bucket: String | ||
) async throws -> S3ExpressIdentity | ||
} |
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 protocol provides an interface for the S3 Express identity resolver to call S3 CreateSession
to get credentials, without depending on the code-generated S3 client.
A SwiftIntegration on the S3 service provides conformance with this protocol.
|
||
import protocol SmithyIdentityAPI.Identity | ||
import struct Foundation.Date | ||
|
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 type to hold a S3 Express identity.
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.
Though this type is more or less the same as AWSCredentialIdentity
(minus only the account ID), a separate type is used for S3 Express credentials because the two credential types are not interchangeable, and separate types helps make clear which credentials are being provided.
import struct Smithy.Attributes | ||
import protocol SmithyIdentityAPI.IdentityResolver | ||
|
||
public protocol S3ExpressIdentityResolver: IdentityResolver where IdentityT == S3ExpressIdentity {} |
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 convenience declaration for the type of an S3 Express-specialized identity resolver.
authOption.signingProperties.set(key: SmithyHTTPAuthAPI.SigningPropertyKeys.signingRegion, value: param.signingRegion) | ||
authOption.identityProperties.set(key: AWSSDKIdentity.AWSIdentityPropertyKeys.bucket, value: serviceParams.bucket) | ||
authOption.identityProperties.set(key: AWSSDKIdentity.AWSIdentityPropertyKeys.s3ExpressClient, value: S3ExpressCreateSessionClient()) | ||
validAuthOptions.append(authOption) |
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.
[Note: this is generated code]
An auth option is added for S3 Express. The bucket name and a S3ExpressCreateSessionClient
are added to identity properties for use by the S3 Express identity resolver.
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.
S3ExpressCreateSessionClient
, which is added into identity properties above, is a wrapper around the S3 service client that allows runtime code to perform the S3 CreateSession
operation for the purpose of obtaining S3 Express credentials, while preventing runtime from having to depend on the S3 client directly.
@@ -22522,6 +22523,7 @@ extension GetObjectInput { | |||
.withExpiration(value: expiration) | |||
.withIdentityResolver(value: config.awsCredentialIdentityResolver, schemeID: "aws.auth#sigv4") | |||
.withIdentityResolver(value: config.awsCredentialIdentityResolver, schemeID: "aws.auth#sigv4a") | |||
.withIdentityResolver(value: config.s3ExpressIdentityResolver, schemeID: "aws.auth#sigv4-s3express") |
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.
[Note: this is generated code]
For this & several other presigned URLs:
- The client config is added to context so it can be used for the S3 CreateSession operation to get credentials later on.
- The S3 Express identity resolver is added to the operation.
@@ -44,7 +45,7 @@ public class DefaultAWSAuthSchemePlugin: ClientRuntime.Plugin { | |||
public func configureClient(clientConfiguration: ClientRuntime.ClientConfiguration) throws { | |||
if let config = clientConfiguration as? S3Client.S3ClientConfiguration { | |||
config.authSchemeResolver = DefaultS3AuthSchemeResolver() | |||
config.authSchemes = [AWSSDKHTTPAuth.SigV4AuthScheme(), AWSSDKHTTPAuth.SigV4AAuthScheme()] | |||
config.authSchemes = [AWSSDKHTTPAuth.SigV4AuthScheme(), AWSSDKHTTPAuth.SigV4AAuthScheme(), AWSSDKHTTPAuth.SigV4S3ExpressAuthScheme()] |
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.
[Note: this is generated code]
SigV4S3ExpressAuthScheme
is added to the service's auth schemes
|
||
if (ctx.service.isS3) { | ||
updatedAuthSchemeList += writer.format("\$N()", AWSSDKHTTPAuthTypes.SigV4S3ExpressAuthScheme) | ||
} |
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.
SigV4S3ExpressAuthScheme
is added, but only to the S3 service client.
@@ -36,6 +37,9 @@ abstract class AWSHTTPProtocolCustomizations : DefaultHTTPProtocolCustomizations | |||
} | |||
writer.write(" .withIdentityResolver(value: config.awsCredentialIdentityResolver, schemeID: \$S)", "aws.auth#sigv4") | |||
writer.write(" .withIdentityResolver(value: config.awsCredentialIdentityResolver, schemeID: \$S)", "aws.auth#sigv4a") | |||
if (ctx.service.isS3) { | |||
writer.write(" .withIdentityResolver(value: config.s3ExpressIdentityResolver, schemeID: \$S)", "aws.auth#sigv4-s3express") | |||
} |
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.
S3 Express identity resolver is added to the context for an operation, but only to the S3 service client.
) | ||
write("validAuthOptions.append(authOption)") | ||
dedent() | ||
} |
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.
In the S3 rules-based auth scheme resolver only, the S3 Express auth option is added.
(See the generated code above for more discussion of the content here.)
ctx: SwiftCodegenContext, | ||
protocolGenerationContext: ProtocolGenerator.GenerationContext, | ||
delegator: SwiftDelegator | ||
) { |
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 method generates a file that creates a concrete implementation of the S3ExpressCreateSessionClient
protocol.
An instance of this class is passed into the S3 credential provider, and allows that credential provider to call S3 without actually depending on S3.
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.
.package( | ||
id: "aws-sdk-swift.AWSSDKIdentity", | ||
exact: "0.0.1" | ||
), |
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.
Here & below, the generated AWSS3 service client is shown. There are no changes in any other AWS service client.
import protocol ClientRuntime.DefaultClientConfiguration | ||
import struct AWSSDKIdentity.S3ExpressIdentity | ||
|
||
|
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 a code-generated, concrete implementation of the AWSSDKIdentity.S3ExpressCreateSessionClient
protocol.
This serves as a "dependency inversion" wrapper to allow AWSSDKIdentity
to call the S3 client without depending on it.
The method below takes a DefaultClientConfiguration
instead of the concrete S3 configuration type, then it casts it to S3 config. Because this wrapper is only ever generated for S3, the cast to S3Client.Config
should never fail.
@@ -125,6 +128,34 @@ class RulesBasedAuthSchemeResolverGenerator { | |||
) | |||
write("validAuthOptions.append(sigV4Option)") | |||
dedent() |
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.
When S3 Express is used as the auth scheme, all the identity properties below need to be set so the S3 Express identity resolver has what it needs to get S3 Express 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.
Some Qs
...gen/src/main/kotlin/software/amazon/smithy/aws/swift/codegen/AWSHttpProtocolServiceClient.kt
Show resolved
Hide resolved
...ity/Sources/AWSSDKIdentity/S3Express/IdentityResolver/DefaultS3ExpressIdentityResolver.swift
Show resolved
Hide resolved
private func scheduleRefreshTask() { | ||
// Cancel any previous refresh task | ||
self.refreshTask?.cancel() | ||
|
||
// Task captures self weakly, to prevent refreshing a cache that has been purged. | ||
// self is then bound strongly both before & after the credential refresh wait. | ||
self.refreshTask = Task { [weak self] in | ||
guard let selfBeforeWait = self else { return } | ||
let expiration: Date | ||
do { | ||
expiration = try await selfBeforeWait.retrieveTask.value.expiration ?? Date.distantPast | ||
} catch { | ||
// If there was an error retrieving credentials, throw it to the caller | ||
// and remove this cached element. | ||
// A subsequent access to credentials will create a new cached element. | ||
await selfBeforeWait.removeSelfFromCache() | ||
throw error | ||
} | ||
|
||
// Asynchronously wait until the credentials are within buffer time of expiring. | ||
// Use the modern `ContinuousClock` time measurement if available. | ||
let now = Date() | ||
if #available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) { | ||
let interval = expiration.timeIntervalSince(now) - Self.buffer | ||
let duration = Duration(secondsComponent: Int64(interval), attosecondsComponent: 0) | ||
let clock = ContinuousClock.now.advanced(by: duration) | ||
let tolerance = Duration(secondsComponent: 1, attosecondsComponent: 0) | ||
try await Task.sleep(until: clock, tolerance: tolerance, clock: .continuous) | ||
} else { | ||
let interval = expiration.timeIntervalSince(now) - Self.buffer | ||
try await Task.sleep(nanoseconds: UInt64(interval) * 1_000_000_000) | ||
} | ||
|
||
// wait has now passed. Perform the credential refresh if credentials have been accessed, | ||
// else this cached element will be purged for not being used. | ||
guard !Task.isCancelled, let selfAfterWait = self else { return } | ||
await selfAfterWait.performRefreshIfNeeded() | ||
} | ||
} | ||
|
||
private func performRefreshIfNeeded() async { | ||
switch status { | ||
case .initial: | ||
// This is the very first access to these credentials. | ||
// If there isn't another access before refresh, these credentials will get purged. | ||
status = .unaccessed | ||
case .unaccessed: | ||
// Credential hasn't been accessed since initial access. | ||
// Purge this cached element from the cache. | ||
await removeSelfFromCache() | ||
case .accessed: | ||
// Credential has been accessed since last refresh | ||
// Refresh again. | ||
self.status = .unaccessed | ||
retrieveTask = Self.newRetrieveTask(identityProperties: identityProperties) | ||
scheduleRefreshTask() | ||
} |
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.
Q: What was the reason a simpler approach (where credentials themselves are directly cached & refreshed each time it's accessed if it's within refresh buffer window) was not taken?
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.
Answered offline; because specifications require us to refresh credentials automatically in the background to avoid having to refresh credential at request time. Doing so reduces duration of API call.
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.
As discussed verbally, S3 Express credentials are refreshed on a timer, prior to expiration, so that fresh credentials are always available, preventing a S3 Express request from having to wait for credential refresh before it can proceed.
Description of changes
Provides customizations needed to utilize the S3 Express feature.
Companion S3 PR:
Includes integration tests:
GetObject
&PutObject
presigned S3 Express URLs.New/existing dependencies impact assessment, if applicable
No new dependencies were added to this change.
By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license.