From 8a215cdb29e3f6f15e54da9b44ed02868d0d9257 Mon Sep 17 00:00:00 2001 From: Adam Fowler Date: Tue, 20 Jun 2023 16:04:01 +0100 Subject: [PATCH] Remove EventLoops from CredentialProviders (#554) * Make credential providers async * Fix tests Also add support for initialising an ExpiringValue with an updating closure. Needed to add another state to get this working. * getTaskProviderTask -> getCredentialProviderTask * Format copyright 2023 * validation changes * Fix issues with credential providers Add error state for ExpiringValue where previous getValue returned an error * Don't throw error during credential provider selection If credential provider fails to provide a credential don't throw an error when you are shutting it down * Shutdown test for rotating credential provider And fixed a bug * Added testDeferredCredentialProviderSetupShutdown --- .swiftformat | 4 +- Sources/SotoCore/AWSClient.swift | 62 ++------ .../SotoCore/Concurrency/ExpiringValue.swift | 73 +++++++-- .../ConfigFileCredentialProvider.swift | 46 +++--- .../Credential/Credential+IsEmpty.swift | 2 +- .../Credential/CredentialProvider+async.swift | 34 ---- .../Credential/CredentialProvider.swift | 10 +- .../Credential/CredentialProviderError.swift | 2 +- .../CredentialProviderSelector.swift | 56 ++----- .../DeferredCredentialProvider.swift | 61 +++----- .../Credential/ExpiringCredential.swift | 15 +- .../MetaDataCredentialProvider.swift | 147 ++++++++---------- .../Credential/NullCredentialProvider.swift | 6 +- .../RotatingCredentialProvider.swift | 86 +++------- .../RuntimeSelectorCredentialProvider.swift | 58 ++++--- .../SotoCore/Credential/STSAssumeRole.swift | 23 +-- .../StaticCredential+CredentialProvider.swift | 6 +- .../StaticCredential+Environment.swift | 2 +- Sources/SotoCore/HTTP/AsyncHTTPClient.swift | 10 +- .../SotoCore/Waiters/AWSClient+Waiter.swift | 2 +- Tests/SotoCoreTests/AWSClientTests.swift | 2 +- .../Concurrency/ExpiringValueTests.swift | 12 ++ .../Sequence+concurrentMapTests.swift | 2 +- .../ConfigFileCredentialProviderTests.swift | 17 +- .../Credential/ConfigFileLoaderTests.swift | 23 ++- .../Credential/CredentialProviderTests.swift | 47 ++++-- .../MetaDataCredentialProviderTests.swift | 8 +- .../RotatingCredentialProviderTests.swift | 65 +++++--- ...ntimeSelectorCredentialProviderTests.swift | 37 ++--- .../Credential/STSAssumeRoleTests.swift | 8 +- .../StaticCredential+EnvironmentTests.swift | 2 +- Tests/SotoCoreTests/MiddlewareTests.swift | 2 +- scripts/validate.sh | 5 +- 33 files changed, 421 insertions(+), 514 deletions(-) delete mode 100644 Sources/SotoCore/Credential/CredentialProvider+async.swift diff --git a/.swiftformat b/.swiftformat index 82d81a1b1..12243eed5 100644 --- a/.swiftformat +++ b/.swiftformat @@ -1,8 +1,8 @@ # Minimum swiftformat version ---minversion 0.47.4 +--minversion 0.51.0 # Swift version ---swiftversion 5.1 +--swiftversion 5.5 # file options --exclude .build diff --git a/Sources/SotoCore/AWSClient.swift b/Sources/SotoCore/AWSClient.swift index b6672205a..afe95082e 100644 --- a/Sources/SotoCore/AWSClient.swift +++ b/Sources/SotoCore/AWSClient.swift @@ -115,61 +115,26 @@ public final class AWSClient: Sendable { /// /// - Throws: AWSClient.ClientError.alreadyShutdown: You have already shutdown the client public func syncShutdown() throws { - let errorStorageLock = NIOLock() - var errorStorage: Error? + let errorStorage: NIOLockedValueBox = .init(nil) let continuation = DispatchWorkItem {} - self.shutdown(queue: DispatchQueue(label: "aws-client.shutdown")) { error in - if let error = error { - errorStorageLock.withLock { + Task { + do { + try await shutdown() + } catch { + errorStorage.withLockedValue { errorStorage in errorStorage = error } } continuation.perform() } continuation.wait() - try errorStorageLock.withLock { + try errorStorage.withLockedValue { errorStorage in if let error = errorStorage { throw error } } } - /// Shutdown AWSClient asynchronously. - /// - /// Before an `AWSClient` is deleted you need to call this function or the synchronous - /// version `syncShutdown` to do a clean shutdown of the client. It cleans up `CredentialProvider` tasks and shuts down - /// the HTTP client if it was created by the `AWSClient`. Given we could be destroying the `EventLoopGroup` the client - /// uses, we have to use a `DispatchQueue` to run some of this work on. - /// - /// - Parameters: - /// - queue: Dispatch Queue to run shutdown on - /// - callback: Callback called when shutdown is complete. If there was an error it will return with Error in callback - public func shutdown(queue: DispatchQueue = .global(), _ callback: @escaping (Error?) -> Void) { - guard self.isShutdown.compareExchange(expected: false, desired: true, ordering: .relaxed).exchanged else { - callback(ClientError.alreadyShutdown) - return - } - let eventLoop = eventLoopGroup.next() - // ignore errors from credential provider. Don't need shutdown erroring because no providers were available - credentialProvider.shutdown(on: eventLoop).whenComplete { _ in - // if httpClient was created by AWSClient then it is required to shutdown the httpClient. - switch self.httpClientProvider { - case .createNew, .createNewWithEventLoopGroup: - self.httpClient.shutdown(queue: queue) { error in - if let error = error { - self.clientLogger.log(level: self.options.errorLogLevel, "Error shutting down HTTP client", metadata: [ - "aws-error": "\(error)", - ]) - } - callback(error) - } - - case .shared: - callback(nil) - } - } - } - // MARK: Member structs/enums /// Errors returned by `AWSClient` code @@ -246,7 +211,7 @@ extension AWSClient { } // shutdown credential provider ignoring any errors as credential provider that doesn't initialize // can cause the shutdown process to fail - try? await self.credentialProvider.shutdown(on: self.eventLoopGroup.any()).get() + try? await self.credentialProvider.shutdown() // if httpClient was created by AWSClient then it is required to shutdown the httpClient. switch self.httpClientProvider { case .createNew, .createNewWithEventLoopGroup: @@ -485,7 +450,7 @@ extension AWSClient { logger.log(level: self.options.requestLogLevel, "AWS Request") do { // get credentials - let credential = try await credentialProvider.getCredential(on: self.eventLoopGroup.any(), logger: logger).get() + let credential = try await credentialProvider.getCredential(logger: logger) // construct signer let signer = AWSSigner(credentials: credential, name: config.signingName, region: config.region.rawValue) // create request and sign with signer @@ -573,12 +538,7 @@ extension AWSClient { /// - logger: optional logger to use /// - Returns: Credential public func getCredential(logger: Logger = AWSClient.loggingDisabled) async throws -> Credential { - let eventLoop = self.eventLoopGroup.any() - if let asyncCredentialProvider = self.credentialProvider as? AsyncCredentialProvider { - return try await asyncCredentialProvider.getCredential(on: eventLoop, logger: logger) - } else { - return try await self.credentialProvider.getCredential(on: eventLoop, logger: logger).get() - } + try await self.credentialProvider.getCredential(logger: logger) } /// Generate a signed URL @@ -643,7 +603,7 @@ extension AWSClient { } func createSigner(serviceConfig: AWSServiceConfig, logger: Logger) async throws -> AWSSigner { - let credential = try await credentialProvider.getCredential(on: eventLoopGroup.next(), logger: logger).get() + let credential = try await credentialProvider.getCredential(logger: logger) return AWSSigner(credentials: credential, name: serviceConfig.signingName, region: serviceConfig.region.rawValue) } } diff --git a/Sources/SotoCore/Concurrency/ExpiringValue.swift b/Sources/SotoCore/Concurrency/ExpiringValue.swift index 2e84db674..6356886bd 100644 --- a/Sources/SotoCore/Concurrency/ExpiringValue.swift +++ b/Sources/SotoCore/Concurrency/ExpiringValue.swift @@ -25,12 +25,18 @@ actor ExpiringValue { enum State { /// No value is stored case noValue + /// Initial call waiting on a value to be generated. Cannot use `waitingOnValue`` in + /// initial call as it means we would have to setup it up before all stored properties + /// have been initialized + case initialWaitingOnValue(Task<(T, Date), Error>) /// Waiting on a value to be generated case waitingOnValue(Task) /// Is holding a value case withValue(T, Date) /// Is holding a value, and there is a task in progress to update it case withValueAndWaiting(T, Date, Task) + /// Error + case error(Error) } var state: State @@ -46,51 +52,98 @@ actor ExpiringValue { self.state = .withValue(initialValue, expires) } + init(threshold: TimeInterval = 2, getExpiringValue: @escaping @Sendable () async throws -> (T, Date)) { + self.threshold = threshold + let task = Task { + try await getExpiringValue() + } + self.state = .initialWaitingOnValue(task) + } + func getValue(getExpiringValue: @escaping @Sendable () async throws -> (T, Date)) async throws -> T { + let task: Task switch self.state { case .noValue: - let task = self.getValueTask(getExpiringValue) + task = try self.getValueTask(getExpiringValue) self.state = .waitingOnValue(task) - return try await task.value - case .waitingOnValue(let task): - return try await task.value + case .initialWaitingOnValue(let task): + return try await withTaskCancellationHandler { + switch await task.result { + case .success(let result): + self.state = .withValue(result.0, result.1) + return result.0 + case .failure(let error): + self.state = .error(error) + throw error + } + } onCancel: { + task.cancel() + } + + case .waitingOnValue(let waitingOnTask): + task = waitingOnTask case .withValue(let value, let expires): if expires.timeIntervalSinceNow < 0 { // value has expired, create new task to update value and // return the result of that task - let task = self.getValueTask(getExpiringValue) + task = try self.getValueTask(getExpiringValue) self.state = .waitingOnValue(task) - return try await task.value } else if expires.timeIntervalSinceNow < self.threshold { // value is about to expire, create new task to update value and // return current value - let task = self.getValueTask(getExpiringValue) + let task = try self.getValueTask(getExpiringValue) self.state = .withValueAndWaiting(value, expires, task) return value } else { return value } - case .withValueAndWaiting(let value, let expires, let task): + case .withValueAndWaiting(let value, let expires, let waitingOnTask): if expires.timeIntervalSinceNow < 0 { // as value has expired wait for task to finish and return result - return try await task.value + task = waitingOnTask } else { // value hasn't expired so return current value return value } + + case .error(let error): + throw error + } + return try await withTaskCancellationHandler { + switch await task.result { + case .success(let value): + return value + case .failure(let error): + self.state = .error(error) + throw error + } + } onCancel: { + task.cancel() } } /// Create task that will return a new version of the value and a date it will expire /// - Parameter getExpiringValue: Function return value and expiration date - func getValueTask(_ getExpiringValue: @escaping @Sendable () async throws -> (T, Date)) -> Task { + func getValueTask(_ getExpiringValue: @escaping @Sendable () async throws -> (T, Date)) throws -> Task { + try Task.checkCancellation() return Task { let (value, expires) = try await getExpiringValue() self.state = .withValue(value, expires) return value } } + + func cancel() { + switch self.state { + case .initialWaitingOnValue(let task): + task.cancel() + case .waitingOnValue(let task), .withValueAndWaiting(_, _, let task): + task.cancel() + default: + break + } + } } diff --git a/Sources/SotoCore/Credential/ConfigFileCredentialProvider.swift b/Sources/SotoCore/Credential/ConfigFileCredentialProvider.swift index f2438483c..1076c0c8d 100644 --- a/Sources/SotoCore/Credential/ConfigFileCredentialProvider.swift +++ b/Sources/SotoCore/Credential/ConfigFileCredentialProvider.swift @@ -2,7 +2,7 @@ // // This source file is part of the Soto for AWS open source project // -// Copyright (c) 2017-2022 the Soto project authors +// Copyright (c) 2017-2023 the Soto project authors // Licensed under Apache License v2.0 // // See LICENSE.txt for license information @@ -18,13 +18,7 @@ import NIOCore import SotoSignerV4 final class ConfigFileCredentialProvider: CredentialProviderSelector { - /// promise to find a credential provider - let startupPromise: EventLoopPromise - /// lock for access to _internalProvider. - let lock = NIOLock() - /// internal version of internal provider. Should access this through `internalProvider` - var _internalProvider: CredentialProvider? - + private let getProviderTask: Task init( credentialsFilePath: String, configFilePath: String, @@ -32,14 +26,24 @@ final class ConfigFileCredentialProvider: CredentialProviderSelector { context: CredentialProviderFactory.Context, endpoint: String? = nil ) { - self.startupPromise = context.eventLoop.makePromise(of: CredentialProvider.self) - self.startupPromise.futureResult.whenSuccess { result in - self.internalProvider = result + self.getProviderTask = Task { + let profile = profile ?? Environment["AWS_PROFILE"] ?? ConfigFileLoader.defaultProfile + return try await ConfigFileCredentialProvider.credentialProvider( + from: credentialsFilePath, + configFilePath: configFilePath, + for: profile, + context: context, + endpoint: endpoint + ) } + } - let profile = profile ?? Environment["AWS_PROFILE"] ?? ConfigFileLoader.defaultProfile - Self.credentialProvider(from: credentialsFilePath, configFilePath: configFilePath, for: profile, context: context, endpoint: endpoint) - .cascade(to: self.startupPromise) + func getCredentialProviderTask() async throws -> CredentialProvider { + try await self.getProviderTask.value + } + + func cancelCredentialProviderTask() { + self.getProviderTask.cancel() } /// Credential provider from shared credentials and profile configuration files @@ -57,16 +61,14 @@ final class ConfigFileCredentialProvider: CredentialProviderSelector { for profile: String, context: CredentialProviderFactory.Context, endpoint: String? - ) -> EventLoopFuture { - return ConfigFileLoader.loadSharedCredentials( + ) async throws -> CredentialProvider { + let sharedCredentials = try await ConfigFileLoader.loadSharedCredentials( credentialsFilePath: credentialsFilePath, configFilePath: configFilePath, profile: profile, context: context - ) - .flatMapThrowing { sharedCredentials in - return try self.credentialProvider(from: sharedCredentials, context: context, endpoint: endpoint) - } + ).get() + return try self.credentialProvider(from: sharedCredentials, context: context, endpoint: endpoint) } /// Generate credential provider based on shared credentials and profile configuration @@ -100,7 +102,3 @@ final class ConfigFileCredentialProvider: CredentialProviderSelector { } } } - -// can use @unchecked Sendable here as `_internalProvider`` is accessed via `internalProvider` which -// protects access with a `NIOLock` -extension ConfigFileCredentialProvider: @unchecked Sendable {} diff --git a/Sources/SotoCore/Credential/Credential+IsEmpty.swift b/Sources/SotoCore/Credential/Credential+IsEmpty.swift index 7340bd923..603977752 100644 --- a/Sources/SotoCore/Credential/Credential+IsEmpty.swift +++ b/Sources/SotoCore/Credential/Credential+IsEmpty.swift @@ -2,7 +2,7 @@ // // This source file is part of the Soto for AWS open source project // -// Copyright (c) 2017-2020 the Soto project authors +// Copyright (c) 2017-2023 the Soto project authors // Licensed under Apache License v2.0 // // See LICENSE.txt for license information diff --git a/Sources/SotoCore/Credential/CredentialProvider+async.swift b/Sources/SotoCore/Credential/CredentialProvider+async.swift deleted file mode 100644 index ed2e59018..000000000 --- a/Sources/SotoCore/Credential/CredentialProvider+async.swift +++ /dev/null @@ -1,34 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the Soto for AWS open source project -// -// Copyright (c) 2017-2022 the Soto project authors -// Licensed under Apache License v2.0 -// -// See LICENSE.txt for license information -// See CONTRIBUTORS.txt for the list of Soto project authors -// -// SPDX-License-Identifier: Apache-2.0 -// -//===----------------------------------------------------------------------===// - -import Logging -import NIOCore -import SotoSignerV4 - -/// Async Protocol for providing AWS credentials -public protocol AsyncCredentialProvider: CredentialProvider { - /// Return credential - /// - Parameters: - /// - eventLoop: EventLoop to run on - /// - logger: Logger to use - func getCredential(on eventLoop: EventLoop, logger: Logger) async throws -> Credential -} - -extension AsyncCredentialProvider { - public func getCredential(on eventLoop: EventLoop, logger: Logger) -> EventLoopFuture { - let promise = eventLoop.makePromise(of: Credential.self) - promise.completeWithTask { try await self.getCredential(on: eventLoop, logger: logger) } - return promise.futureResult - } -} diff --git a/Sources/SotoCore/Credential/CredentialProvider.swift b/Sources/SotoCore/Credential/CredentialProvider.swift index b59c905cb..2453ca5f4 100644 --- a/Sources/SotoCore/Credential/CredentialProvider.swift +++ b/Sources/SotoCore/Credential/CredentialProvider.swift @@ -2,7 +2,7 @@ // // This source file is part of the Soto for AWS open source project // -// Copyright (c) 2017-2022 the Soto project authors +// Copyright (c) 2017-2023 the Soto project authors // Licensed under Apache License v2.0 // // See LICENSE.txt for license information @@ -24,17 +24,15 @@ public protocol CredentialProvider: Sendable, CustomStringConvertible { /// - Parameters: /// - eventLoop: EventLoop to run on /// - logger: Logger to use - func getCredential(on eventLoop: EventLoop, logger: Logger) -> EventLoopFuture + func getCredential(logger: Logger) async throws -> Credential /// Shutdown credential provider /// - Parameter eventLoop: EventLoop to use when shutiting down - func shutdown(on eventLoop: EventLoop) -> EventLoopFuture + func shutdown() async throws } extension CredentialProvider { - public func shutdown(on eventLoop: EventLoop) -> EventLoopFuture { - return eventLoop.makeSucceededFuture(()) - } + public func shutdown() async throws {} public var description: String { return "\(type(of: self))" } } diff --git a/Sources/SotoCore/Credential/CredentialProviderError.swift b/Sources/SotoCore/Credential/CredentialProviderError.swift index 281f2ec35..999b25922 100644 --- a/Sources/SotoCore/Credential/CredentialProviderError.swift +++ b/Sources/SotoCore/Credential/CredentialProviderError.swift @@ -2,7 +2,7 @@ // // This source file is part of the Soto for AWS open source project // -// Copyright (c) 2017-2020 the Soto project authors +// Copyright (c) 2017-2023 the Soto project authors // Licensed under Apache License v2.0 // // See LICENSE.txt for license information diff --git a/Sources/SotoCore/Credential/CredentialProviderSelector.swift b/Sources/SotoCore/Credential/CredentialProviderSelector.swift index 62a514645..4681a2ad0 100644 --- a/Sources/SotoCore/Credential/CredentialProviderSelector.swift +++ b/Sources/SotoCore/Credential/CredentialProviderSelector.swift @@ -2,7 +2,7 @@ // // This source file is part of the Soto for AWS open source project // -// Copyright (c) 2017-2022 the Soto project authors +// Copyright (c) 2017-2023 the Soto project authors // Licensed under Apache License v2.0 // // See LICENSE.txt for license information @@ -17,55 +17,27 @@ import NIOConcurrencyHelpers import NIOCore import SotoSignerV4 -/// Protocol for CredentialProvider that uses an internal CredentialProvider -/// -/// When conforming to this protocol once you have the internal provider it should be supplying to -/// the startupPromise and you should set `internalProvider` when the setupPromise -/// result is available. -/// ``` -/// init(providers: [CredentialProviderFactory], context: CredentialProviderFactory.Context) { -/// self.startupPromise = context.eventLoop.makePromise(of: CredentialProvider.self) -/// self.startupPromise.futureResult.whenSuccess { result in -/// self.internalProvider = result -/// } -/// self.setupInternalProvider(providers: providers, context: context) -/// } -/// ``` +/// Protocol for CredentialProvider that uses an internal CredentialProvider generated by another Task protocol CredentialProviderSelector: CredentialProvider, AnyObject { - /// promise to find a credential provider - var startupPromise: EventLoopPromise { get } - var lock: NIOLock { get } - var _internalProvider: CredentialProvider? { get set } + func getCredentialProviderTask() async throws -> CredentialProvider + func cancelCredentialProviderTask() } extension CredentialProviderSelector { - /// the provider chosen to supply credentials - var internalProvider: CredentialProvider? { - get { - self.lock.withLock { - _internalProvider - } + func getCredential(logger: Logger) async throws -> Credential { + try await withTaskCancellationHandler { + let provider = try await getCredentialProviderTask() + return try await provider.getCredential(logger: logger) } - set { - self.lock.withLock { - _internalProvider = newValue - } + onCancel: { + cancelCredentialProviderTask() } } - func shutdown(on eventLoop: EventLoop) -> EventLoopFuture { - return self.startupPromise.futureResult.flatMap { provider in - provider.shutdown(on: eventLoop) - }.hop(to: eventLoop) - } - - func getCredential(on eventLoop: EventLoop, logger: Logger) -> EventLoopFuture { - if let provider = internalProvider { - return provider.getCredential(on: eventLoop, logger: logger) - } - - return self.startupPromise.futureResult.hop(to: eventLoop).flatMap { provider in - return provider.getCredential(on: eventLoop, logger: logger) + func shutdown() async throws { + cancelCredentialProviderTask() + if let provider = try? await getCredentialProviderTask() { + try await provider.shutdown() } } } diff --git a/Sources/SotoCore/Credential/DeferredCredentialProvider.swift b/Sources/SotoCore/Credential/DeferredCredentialProvider.swift index 96cd42b72..5582d455b 100644 --- a/Sources/SotoCore/Credential/DeferredCredentialProvider.swift +++ b/Sources/SotoCore/Credential/DeferredCredentialProvider.swift @@ -2,7 +2,7 @@ // // This source file is part of the Soto for AWS open source project // -// Copyright (c) 2017-2022 the Soto project authors +// Copyright (c) 2017-2023 the Soto project authors // Licensed under Apache License v2.0 // // See LICENSE.txt for license information @@ -21,67 +21,46 @@ import NIOCore /// Used for wrapping another credential provider whose `getCredential` method doesn't return instantly and /// is only needed to be called once. After the wrapped `CredentialProvider` has generated a credential this is /// returned instead of calling the wrapped `CredentialProvider's` `getCredentials` again. -public class DeferredCredentialProvider: CredentialProvider { - let lock = NIOLock() - var credential: Credential? { - get { - self.lock.withLock { - self.internalCredential - } - } - set { - self.lock.withLock { - self.internalCredential = newValue - } - } - } - +public final class DeferredCredentialProvider: CredentialProvider { + private let getCredentialTask: Task private let provider: CredentialProvider - private let startupPromise: EventLoopPromise - private var internalCredential: Credential? /// Create `DeferredCredentialProvider`. /// - Parameters: /// - eventLoop: EventLoop that getCredential should run on /// - provider: Credential provider to wrap public init(context: CredentialProviderFactory.Context, provider: CredentialProvider) { - self.startupPromise = context.eventLoop.makePromise(of: Credential.self) + let description = "\(type(of: self))(\(provider.description))" + self.getCredentialTask = Task { + let credentials = try await provider.getCredential(logger: context.logger) + context.logger.debug("AWS credentials ready", metadata: ["aws-credential-provider": .string(description)]) + return credentials + } self.provider = provider - provider.getCredential(on: context.eventLoop, logger: context.logger) - .flatMapErrorThrowing { _ in throw CredentialProviderError.noProvider } - .map { credential in - self.credential = credential - context.logger.debug("AWS credentials ready", metadata: ["aws-credential-provider": .string("\(self)")]) - return credential - } - .cascade(to: self.startupPromise) } /// Shutdown credential provider - public func shutdown(on eventLoop: EventLoop) -> EventLoopFuture { - return self.startupPromise.futureResult - .and(self.provider.shutdown(on: eventLoop)) - .map { _ in } - .hop(to: eventLoop) + public func shutdown() async throws { + self.getCredentialTask.cancel() + // ensure internal credential provider is not still running + _ = try? await self.getCredential(logger: AWSClient.loggingDisabled) + try await self.provider.shutdown() } /// Return credentials. If still in process of the getting credentials then return future result of `startupPromise` /// otherwise return credentials store in class /// - Parameter eventLoop: EventLoop to run off /// - Returns: EventLoopFuture that will hold credentials - public func getCredential(on eventLoop: EventLoop, logger: Logger) -> EventLoopFuture { - if let credential = self.credential { - return eventLoop.makeSucceededFuture(credential) + public func getCredential(logger: Logger) async throws -> Credential { + try await withTaskCancellationHandler { + try await self.getCredentialTask.value + } + onCancel: { + self.getCredentialTask.cancel() } - - return self.startupPromise.futureResult.hop(to: eventLoop) } } extension DeferredCredentialProvider: CustomStringConvertible { public var description: String { return "\(type(of: self))(\(self.provider.description))" } } - -// can use @unchecked Sendable here as `internalCredential` is accessed via `credential` which -// protects access with a `NIOLock` -extension DeferredCredentialProvider: @unchecked Sendable {} diff --git a/Sources/SotoCore/Credential/ExpiringCredential.swift b/Sources/SotoCore/Credential/ExpiringCredential.swift index a0315a9d1..402ecb0a5 100644 --- a/Sources/SotoCore/Credential/ExpiringCredential.swift +++ b/Sources/SotoCore/Credential/ExpiringCredential.swift @@ -2,7 +2,7 @@ // // This source file is part of the Soto for AWS open source project // -// Copyright (c) 2017-2020 the Soto project authors +// Copyright (c) 2017-2023 the Soto project authors // Licensed under Apache License v2.0 // // See LICENSE.txt for license information @@ -22,11 +22,15 @@ import SotoSignerV4 /// Credential provider whose credentials expire over tiem. public protocol ExpiringCredential: Credential { - /// Will credential expire within a certain time - func isExpiring(within: TimeInterval) -> Bool + var expiration: Date { get } } public extension ExpiringCredential { + /// Will credential expire within a certain time + func isExpiring(within interval: TimeInterval) -> Bool { + return self.expiration.timeIntervalSinceNow < interval + } + /// Has credential expired var isExpired: Bool { isExpiring(within: 0) @@ -42,11 +46,6 @@ public struct RotatingCredential: ExpiringCredential { self.expiration = expiration } - /// Will credential expire within a certain time - public func isExpiring(within interval: TimeInterval) -> Bool { - return self.expiration.timeIntervalSinceNow < interval - } - public let accessKeyId: String public let secretAccessKey: String public let sessionToken: String? diff --git a/Sources/SotoCore/Credential/MetaDataCredentialProvider.swift b/Sources/SotoCore/Credential/MetaDataCredentialProvider.swift index 26eef41c6..a0c5b64b2 100644 --- a/Sources/SotoCore/Credential/MetaDataCredentialProvider.swift +++ b/Sources/SotoCore/Credential/MetaDataCredentialProvider.swift @@ -2,7 +2,7 @@ // // This source file is part of the Soto for AWS open source project // -// Copyright (c) 2017-2022 the Soto project authors +// Copyright (c) 2017-2023 the Soto project authors // Licensed under Apache License v2.0 // // See LICENSE.txt for license information @@ -35,14 +35,12 @@ import SotoSignerV4 protocol MetaDataClient: CredentialProvider { associatedtype MetaData: ExpiringCredential & Decodable - func getMetaData(on eventLoop: EventLoop, logger: Logger) -> EventLoopFuture + func getMetaData(logger: Logger) async throws -> MetaData } extension MetaDataClient { - func getCredential(on eventLoop: EventLoop, logger: Logger) -> EventLoopFuture { - self.getMetaData(on: eventLoop, logger: logger).map { metaData in - metaData - } + func getCredential(logger: Logger) async throws -> Credential { + return try await self.getMetaData(logger: logger) } } @@ -112,25 +110,24 @@ struct ECSMetaDataClient: MetaDataClient { self.endpointURL = "\(host)\(relativeURL)" } - func getMetaData(on eventLoop: EventLoop, logger: Logger) -> EventLoopFuture { - return request(url: endpointURL, timeout: 2, on: eventLoop, logger: logger) - .flatMapThrowing { response in - guard let body = response.body else { - throw MetaDataClientError.missingMetaData - } - return try self.decoder.wrappedValue.decode(MetaData.self, from: body) - } + func getMetaData(logger: Logger) async throws -> ECSMetaData { + let response = try await request(url: endpointURL, timeout: 2, logger: logger) + guard let body = response.body else { + throw MetaDataClientError.missingMetaData + } + return try self.decoder.wrappedValue.decode(MetaData.self, from: body) } - private func request(url: String, timeout: TimeInterval, on eventLoop: EventLoop, logger: Logger) -> EventLoopFuture { + private func request(url: String, timeout: TimeInterval, logger: Logger) async throws -> AWSHTTPResponse { + try Task.checkCancellation() let request = AWSHTTPRequest(url: URL(string: url)!, method: .GET, headers: [:], body: .empty) - return httpClient.execute(request: request, timeout: TimeAmount.seconds(2), on: eventLoop, logger: logger) + return try await httpClient.execute(request: request, timeout: TimeAmount.seconds(2), logger: logger) } } // MARK: InstanceMetaDataServiceProvider -/// Provide AWS credentials for instances +/// Provide AWS credentials for EC2 instances struct InstanceMetaDataClient: MetaDataClient { typealias MetaData = InstanceMetaData @@ -185,84 +182,70 @@ struct InstanceMetaDataClient: MetaDataClient { self.host = host } - func getMetaData(on eventLoop: EventLoop, logger: Logger) -> EventLoopFuture { - return getToken(on: eventLoop, logger: logger) - .map { token in - logger.trace("Found IMDSv2 token") - return HTTPHeaders([(Self.TokenHeaderName, token)]) - } - .flatMapErrorThrowing { _ in - // If we didn't find a session key then assume we are running IMDSv1. - // (we could be running from a Docker container and the hop count for the PUT - // request is still set to 1) - logger.trace("Did not find IMDSv2 token, use IMDSv1") - return HTTPHeaders() - } - .flatMap { headers -> EventLoopFuture<(AWSHTTPResponse, HTTPHeaders)> in - // next we need to request the rolename - self.request( - url: self.credentialURL, - method: .GET, - headers: headers, - on: eventLoop, - logger: logger - ).map { ($0, headers) } - } - .flatMapThrowing { response, headers -> (String, HTTPHeaders) in - // the rolename is in the body - guard response.status == .ok else { - throw MetaDataClientError.unexpectedTokenResponseStatus(status: response.status) - } - - guard var body = response.body, let roleName = body.readString(length: body.readableBytes) else { - throw MetaDataClientError.couldNotGetInstanceRoleName - } - - return (roleName, headers) - } - .flatMap { roleName, headers -> EventLoopFuture in - // request credentials with the rolename - let url = self.credentialURL.appendingPathComponent(roleName) - return self.request(url: url, headers: headers, on: eventLoop, logger: logger) - } - .flatMapThrowing { response in - // decode the repsonse payload into the metadata object - guard let body = response.body else { - throw MetaDataClientError.missingMetaData - } - - return try self.decoder.wrappedValue.decode(InstanceMetaData.self, from: body) - } + func getMetaData(logger: Logger) async throws -> InstanceMetaData { + let headers: HTTPHeaders + do { + let token = try await getToken(logger: logger) + logger.trace("Found IMDSv2 token") + headers = HTTPHeaders([(Self.TokenHeaderName, token)]) + } catch { + // If we didn't find a session key then assume we are running IMDSv1. + // (we could be running from a Docker container and the hop count for the PUT + // request is still set to 1) + logger.trace("Did not find IMDSv2 token, use IMDSv1") + headers = HTTPHeaders() + } + // next we need to request the rolename + let response = try await self.request( + url: self.credentialURL, + method: .GET, + headers: headers, + logger: logger + ) + // the rolename is in the body + guard response.status == .ok else { + throw MetaDataClientError.unexpectedTokenResponseStatus(status: response.status) + } + guard var body = response.body, let roleName = body.readString(length: body.readableBytes) else { + throw MetaDataClientError.couldNotGetInstanceRoleName + } + // request credentials with the rolename + let url = self.credentialURL.appendingPathComponent(roleName) + let credentialResponse = try await self.request(url: url, headers: headers, logger: logger) + + // decode the repsonse payload into the metadata object + guard let body = credentialResponse.body else { + throw MetaDataClientError.missingMetaData + } + + return try self.decoder.wrappedValue.decode(InstanceMetaData.self, from: body) } - func getToken(on eventLoop: EventLoop, logger: Logger) -> EventLoopFuture { - return request( + func getToken(logger: Logger) async throws -> String { + let response = try await request( url: self.tokenURL, method: .PUT, - headers: HTTPHeaders([Self.TokenTimeToLiveHeader]), timeout: .seconds(2), - on: eventLoop, + headers: HTTPHeaders([Self.TokenTimeToLiveHeader]), logger: logger - ).flatMapThrowing { response in - guard response.status == .ok else { - throw MetaDataClientError.unexpectedTokenResponseStatus(status: response.status) - } - - guard var body = response.body, let token = body.readString(length: body.readableBytes) else { - throw MetaDataClientError.couldNotReadTokenFromResponse - } - return token + ) + guard response.status == .ok else { + throw MetaDataClientError.unexpectedTokenResponseStatus(status: response.status) + } + + guard var body = response.body, let token = body.readString(length: body.readableBytes) else { + throw MetaDataClientError.couldNotReadTokenFromResponse } + return token } private func request( url: URL, method: HTTPMethod = .GET, headers: HTTPHeaders = .init(), - timeout: TimeAmount = .seconds(2), - on eventLoop: EventLoop, logger: Logger - ) -> EventLoopFuture { + ) async throws -> AWSHTTPResponse { + try Task.checkCancellation() let request = AWSHTTPRequest(url: url, method: method, headers: headers, body: .empty) - return httpClient.execute(request: request, timeout: timeout, on: eventLoop, logger: logger) + return try await httpClient.execute(request: request, timeout: TimeAmount.seconds(2), logger: logger) } } diff --git a/Sources/SotoCore/Credential/NullCredentialProvider.swift b/Sources/SotoCore/Credential/NullCredentialProvider.swift index c9a1e65ad..399803b5b 100644 --- a/Sources/SotoCore/Credential/NullCredentialProvider.swift +++ b/Sources/SotoCore/Credential/NullCredentialProvider.swift @@ -2,7 +2,7 @@ // // This source file is part of the Soto for AWS open source project // -// Copyright (c) 2017-2020 the Soto project authors +// Copyright (c) 2017-2023 the Soto project authors // Licensed under Apache License v2.0 // // See LICENSE.txt for license information @@ -20,7 +20,7 @@ import SotoSignerV4 public struct NullCredentialProvider: CredentialProvider { public init() {} - public func getCredential(on eventLoop: EventLoop, logger: Logger) -> EventLoopFuture { - return eventLoop.makeFailedFuture(CredentialProviderError.noProvider) + public func getCredential(logger: Logger) async throws -> Credential { + throw CredentialProviderError.noProvider } } diff --git a/Sources/SotoCore/Credential/RotatingCredentialProvider.swift b/Sources/SotoCore/Credential/RotatingCredentialProvider.swift index 466bcfad1..842950f01 100644 --- a/Sources/SotoCore/Credential/RotatingCredentialProvider.swift +++ b/Sources/SotoCore/Credential/RotatingCredentialProvider.swift @@ -2,7 +2,7 @@ // // This source file is part of the Soto for AWS open source project // -// Copyright (c) 2017-2022 the Soto project authors +// Copyright (c) 2017-2023 the Soto project authors // Licensed under Apache License v2.0 // // See LICENSE.txt for license information @@ -12,6 +12,7 @@ // //===----------------------------------------------------------------------===// +import struct Foundation.Date import struct Foundation.TimeInterval import Logging import NIOConcurrencyHelpers @@ -25,84 +26,47 @@ import SotoSignerV4 /// `getCredential` is called again. If current credentials have not expired they are returned otherwise we wait on new /// credentials being provided. public final class RotatingCredentialProvider: CredentialProvider { - let remainingTokenLifetimeForUse: TimeInterval + let expiringCredential: ExpiringValue public let provider: CredentialProvider - private let lock = NIOConcurrencyHelpers.NIOLock() - private var credential: Credential? - private var credentialFuture: EventLoopFuture? public init(context: CredentialProviderFactory.Context, provider: CredentialProvider, remainingTokenLifetimeForUse: TimeInterval? = nil) { self.provider = provider - self.remainingTokenLifetimeForUse = remainingTokenLifetimeForUse ?? 3 * 60 - _ = refreshCredentials(on: context.eventLoop, logger: context.logger) + self.expiringCredential = .init(threshold: remainingTokenLifetimeForUse ?? 3 * 60) { + try await Self.getCredentialAndExpiration(provider: provider, logger: context.logger) + } } /// Shutdown credential provider - public func shutdown(on eventLoop: EventLoop) -> EventLoopFuture { - return self.lock.withLock { - if let future = credentialFuture { - return future.and(provider.shutdown(on: eventLoop)).map { _ in }.hop(to: eventLoop) - } - return provider.shutdown(on: eventLoop) + public func shutdown() async throws { + await expiringCredential.cancel() + // ensure internal credential provider is not still running + _ = try? await expiringCredential.getValue { + try Task.checkCancellation() + preconditionFailure("Cannot get here") } + try await provider.shutdown() } - public func getCredential(on eventLoop: EventLoop, logger: Logger) -> EventLoopFuture { - self.lock.lock() - let cred = credential - self.lock.unlock() - - switch cred { - case .none: - return self.refreshCredentials(on: eventLoop, logger: logger) - case .some(let cred as ExpiringCredential): - if cred.isExpiring(within: remainingTokenLifetimeForUse) { - // the credentials are expiring... let's refresh - return self.refreshCredentials(on: eventLoop, logger: logger) - } - - return eventLoop.makeSucceededFuture(cred) - case .some(let cred): - // we don't have expiring credentials - return eventLoop.makeSucceededFuture(cred) + public func getCredential(logger: Logger) async throws -> Credential { + return try await expiringCredential.getValue { + try await Self.getCredentialAndExpiration(provider: self.provider, logger: logger) } } - private func refreshCredentials(on eventLoop: EventLoop, logger: Logger) -> EventLoopFuture { - self.lock.lock() - defer { self.lock.unlock() } - - if let future = credentialFuture { - // a refresh is already running - if future.eventLoop !== eventLoop { - // We want to hop back to the event loop we came in case - // the refresh is resolved on another EventLoop. - return future.hop(to: eventLoop) - } - return future + static func getCredentialAndExpiration(provider: CredentialProvider, logger: Logger) async throws -> (Credential, Date) { + logger.debug("Refeshing AWS credentials", metadata: ["aws-credential-provider": .string("\(self)(\(provider.description))")]) + try Task.checkCancellation() + let credential = try await provider.getCredential(logger: logger) + logger.debug("AWS credentials ready", metadata: ["aws-credential-provider": .string("\(self)(\(provider.description))")]) + if let expiringCredential = credential as? ExpiringCredential { + return (expiringCredential, expiringCredential.expiration) + } else { + return (credential, Date.distantFuture) } - - logger.debug("Refeshing AWS credentials", metadata: ["aws-credential-provider": .string("\(self)")]) - - credentialFuture = self.provider.getCredential(on: eventLoop, logger: logger) - .map { credential -> (Credential) in - // update the internal credential locked - self.lock.withLock { - self.credentialFuture = nil - self.credential = credential - logger.debug("AWS credentials ready", metadata: ["aws-credential-provider": .string("\(self)")]) - } - return credential - } - - return credentialFuture! } } extension RotatingCredentialProvider: CustomStringConvertible { public var description: String { return "\(type(of: self))(\(provider.description))" } } - -// can use @unchecked Sendable here as access is protected by 'NIOLock' -extension RotatingCredentialProvider: @unchecked Sendable {} diff --git a/Sources/SotoCore/Credential/RuntimeSelectorCredentialProvider.swift b/Sources/SotoCore/Credential/RuntimeSelectorCredentialProvider.swift index 27f09e0d3..b101a7da3 100644 --- a/Sources/SotoCore/Credential/RuntimeSelectorCredentialProvider.swift +++ b/Sources/SotoCore/Credential/RuntimeSelectorCredentialProvider.swift @@ -2,7 +2,7 @@ // // This source file is part of the Soto for AWS open source project // -// Copyright (c) 2017-2022 the Soto project authors +// Copyright (c) 2017-2023 the Soto project authors // Licensed under Apache License v2.0 // // See LICENSE.txt for license information @@ -20,46 +20,40 @@ import SotoSignerV4 /// get credentials from a list of possible credential providers. Goes through list of providers from start to end /// attempting to get credentials. Once it finds a `CredentialProvider` that supplies credentials use that /// one -class RuntimeSelectorCredentialProvider: CredentialProviderSelector { - /// promise to find a credential provider - let startupPromise: EventLoopPromise - let lock = NIOLock() - var _internalProvider: CredentialProvider? +final class RuntimeSelectorCredentialProvider: CredentialProviderSelector { + private let getProviderTask: Task init(providers: [CredentialProviderFactory], context: CredentialProviderFactory.Context) { - self.startupPromise = context.eventLoop.makePromise(of: CredentialProvider.self) - self.startupPromise.futureResult.whenSuccess { result in - self.internalProvider = result + self.getProviderTask = Task { + try await Self.setupInternalProvider(providers: providers, context: context) } - self.setupInternalProvider(providers: providers, context: context) + } + + func getCredentialProviderTask() async throws -> CredentialProvider { + try await self.getProviderTask.value + } + + func cancelCredentialProviderTask() { + self.getProviderTask.cancel() } /// goes through list of providers. If provider is able to provide credentials then use that one, otherwise move onto the next /// provider in the list - private func setupInternalProvider(providers: [CredentialProviderFactory], context: CredentialProviderFactory.Context) { - func _setupInternalProvider(_ index: Int) { - guard index < providers.count else { - self.startupPromise.fail(CredentialProviderError.noProvider) - return - } - let providerFactory = providers[index] + private static func setupInternalProvider( + providers: [CredentialProviderFactory], + context: CredentialProviderFactory.Context + ) async throws -> CredentialProvider { + for providerFactory in providers { let provider = providerFactory.createProvider(context: context) - provider.getCredential(on: context.eventLoop, logger: context.logger).whenComplete { result in - switch result { - case .success: - context.logger.debug("Select credential provider", metadata: ["aws-credential-provider": .string("\(provider)")]) - self.startupPromise.succeed(provider) - case .failure: - context.logger.log(level: context.options.errorLogLevel, "Select credential provider failed") - _setupInternalProvider(index + 1) - } + do { + _ = try await provider.getCredential(logger: context.logger) + context.logger.debug("Select credential provider", metadata: ["aws-credential-provider": .string("\(provider)")]) + return provider + } catch { + try? await provider.shutdown() + context.logger.log(level: context.options.errorLogLevel, "Select credential provider failed", metadata: ["aws-credential-provider": .string("\(provider)")]) } } - - _setupInternalProvider(0) + return NullCredentialProvider() } } - -// can use @unchecked Sendable here as `_internalProvider`` is accessed via `internalProvider` which -// protects access with a `NIOLock` -extension RuntimeSelectorCredentialProvider: @unchecked Sendable {} diff --git a/Sources/SotoCore/Credential/STSAssumeRole.swift b/Sources/SotoCore/Credential/STSAssumeRole.swift index ed44ac465..9d630d80c 100644 --- a/Sources/SotoCore/Credential/STSAssumeRole.swift +++ b/Sources/SotoCore/Credential/STSAssumeRole.swift @@ -2,7 +2,7 @@ // // This source file is part of the Soto for AWS open source project // -// Copyright (c) 2017-2022 the Soto project authors +// Copyright (c) 2017-2023 the Soto project authors // Licensed under Apache License v2.0 // // See LICENSE.txt for license information @@ -85,28 +85,19 @@ struct STSCredentials: AWSDecodableShape, ExpiringCredential { } /// Credential Provider that holds an AWSClient -protocol AsyncCredentialProviderWithClient: AsyncCredentialProvider { +protocol CredentialProviderWithClient: CredentialProvider { var client: AWSClient { get } } -extension AsyncCredentialProviderWithClient { +extension CredentialProviderWithClient { /// shutdown credential provider and client - func shutdown(on eventLoop: EventLoop) -> EventLoopFuture { - // shutdown AWSClient - let promise = eventLoop.makePromise(of: Void.self) - client.shutdown { error in - if let error = error { - promise.completeWith(.failure(error)) - } else { - promise.completeWith(.success(())) - } - } - return promise.futureResult + func shutdown() async throws { + try await client.shutdown() } } /// Internal version of AssumeRole credential provider used by ConfigFileCredentialProvider -struct STSAssumeRoleCredentialProvider: AsyncCredentialProviderWithClient { +struct STSAssumeRoleCredentialProvider: CredentialProviderWithClient { let request: STSAssumeRoleRequest let client: AWSClient let config: AWSServiceConfig @@ -131,7 +122,7 @@ struct STSAssumeRoleCredentialProvider: AsyncCredentialProviderWithClient { ) } - func getCredential(on eventLoop: EventLoop, logger: Logger) async throws -> Credential { + func getCredential(logger: Logger) async throws -> Credential { let response = try await self.assumeRole(self.request, logger: logger) guard let credentials = response.credentials else { throw CredentialProviderError.noProvider diff --git a/Sources/SotoCore/Credential/StaticCredential+CredentialProvider.swift b/Sources/SotoCore/Credential/StaticCredential+CredentialProvider.swift index a415eea8c..a6ea9da0f 100644 --- a/Sources/SotoCore/Credential/StaticCredential+CredentialProvider.swift +++ b/Sources/SotoCore/Credential/StaticCredential+CredentialProvider.swift @@ -2,7 +2,7 @@ // // This source file is part of the Soto for AWS open source project // -// Copyright (c) 2017-2020 the Soto project authors +// Copyright (c) 2017-2023 the Soto project authors // Licensed under Apache License v2.0 // // See LICENSE.txt for license information @@ -17,7 +17,7 @@ import SotoSignerV4 extension StaticCredential: CredentialProvider { /// Return static credential - public func getCredential(on eventLoop: EventLoop, logger: Logger) -> EventLoopFuture { - eventLoop.makeSucceededFuture(self) + public func getCredential(logger: Logger) async throws -> Credential { + return self } } diff --git a/Sources/SotoCore/Credential/StaticCredential+Environment.swift b/Sources/SotoCore/Credential/StaticCredential+Environment.swift index 0e2885487..842e73037 100644 --- a/Sources/SotoCore/Credential/StaticCredential+Environment.swift +++ b/Sources/SotoCore/Credential/StaticCredential+Environment.swift @@ -2,7 +2,7 @@ // // This source file is part of the Soto for AWS open source project // -// Copyright (c) 2017-2020 the Soto project authors +// Copyright (c) 2017-2023 the Soto project authors // Licensed under Apache License v2.0 // // See LICENSE.txt for license information diff --git a/Sources/SotoCore/HTTP/AsyncHTTPClient.swift b/Sources/SotoCore/HTTP/AsyncHTTPClient.swift index b10b2807a..ff70eab4a 100644 --- a/Sources/SotoCore/HTTP/AsyncHTTPClient.swift +++ b/Sources/SotoCore/HTTP/AsyncHTTPClient.swift @@ -109,19 +109,21 @@ extension AsyncHTTPClient.HTTPClient { func execute( request: AWSHTTPRequest, timeout: TimeAmount, - on eventLoop: EventLoop, + on eventLoop: EventLoop? = nil, logger: Logger ) async throws -> AWSHTTPResponse { - try await self.execute(request: request, timeout: timeout, on: eventLoop, logger: logger).get() + let eventLoop = eventLoop ?? self.eventLoopGroup.any() + return try await self.execute(request: request, timeout: timeout, on: eventLoop, logger: logger).get() } func execute( request: AWSHTTPRequest, timeout: TimeAmount, - on eventLoop: EventLoop, + on eventLoop: EventLoop? = nil, logger: Logger, stream: @escaping AWSResponseStream ) async throws -> AWSHTTPResponse { - try await self.execute(request: request, timeout: timeout, on: eventLoop, logger: logger, stream: stream).get() + let eventLoop = eventLoop ?? self.eventLoopGroup.any() + return try await self.execute(request: request, timeout: timeout, on: eventLoop, logger: logger, stream: stream).get() } } diff --git a/Sources/SotoCore/Waiters/AWSClient+Waiter.swift b/Sources/SotoCore/Waiters/AWSClient+Waiter.swift index 1d80f52ce..5fbdb902a 100644 --- a/Sources/SotoCore/Waiters/AWSClient+Waiter.swift +++ b/Sources/SotoCore/Waiters/AWSClient+Waiter.swift @@ -114,7 +114,7 @@ extension AWSClient { attempt += 1 let result: Result do { - result = try .success(await waiter.command(input, logger)) + result = try await .success(waiter.command(input, logger)) } catch { result = .failure(error) } diff --git a/Tests/SotoCoreTests/AWSClientTests.swift b/Tests/SotoCoreTests/AWSClientTests.swift index e088b58a3..a00861513 100644 --- a/Tests/SotoCoreTests/AWSClientTests.swift +++ b/Tests/SotoCoreTests/AWSClientTests.swift @@ -2,7 +2,7 @@ // // This source file is part of the Soto for AWS open source project // -// Copyright (c) 2017-2022 the Soto project authors +// Copyright (c) 2017-2023 the Soto project authors // Licensed under Apache License v2.0 // // See LICENSE.txt for license information diff --git a/Tests/SotoCoreTests/Concurrency/ExpiringValueTests.swift b/Tests/SotoCoreTests/Concurrency/ExpiringValueTests.swift index f1dace00d..9919807c0 100644 --- a/Tests/SotoCoreTests/Concurrency/ExpiringValueTests.swift +++ b/Tests/SotoCoreTests/Concurrency/ExpiringValueTests.swift @@ -87,4 +87,16 @@ final class ExpiringValueTests: XCTestCase { } XCTAssertEqual(callCount.load(ordering: .relaxed), 1) } + + /// Test value returned from closure is given back + func testInitialClosure() async throws { + let expiringValue = ExpiringValue { + try await Task.sleep(nanoseconds: 1000) + return (1, Date() + 3) + } + let value = try await expiringValue.getValue { + return (2, Date()) + } + XCTAssertEqual(value, 1) + } } diff --git a/Tests/SotoCoreTests/Concurrency/Sequence+concurrentMapTests.swift b/Tests/SotoCoreTests/Concurrency/Sequence+concurrentMapTests.swift index ac1a9fca6..62a525e97 100644 --- a/Tests/SotoCoreTests/Concurrency/Sequence+concurrentMapTests.swift +++ b/Tests/SotoCoreTests/Concurrency/Sequence+concurrentMapTests.swift @@ -77,7 +77,7 @@ final class MapTests: XCTestCase { struct TaskError: Error {} do { - _ = try await(1...8).concurrentMap { element -> Int in + _ = try await (1...8).concurrentMap { element -> Int in if element == 4 { throw TaskError() } diff --git a/Tests/SotoCoreTests/Credential/ConfigFileCredentialProviderTests.swift b/Tests/SotoCoreTests/Credential/ConfigFileCredentialProviderTests.swift index 74d0e9756..68bc2a154 100644 --- a/Tests/SotoCoreTests/Credential/ConfigFileCredentialProviderTests.swift +++ b/Tests/SotoCoreTests/Credential/ConfigFileCredentialProviderTests.swift @@ -2,7 +2,7 @@ // // This source file is part of the Soto for AWS open source project // -// Copyright (c) 2017-2020 the Soto project authors +// Copyright (c) 2017-2023 the Soto project authors // Licensed under Apache License v2.0 // // See LICENSE.txt for license information @@ -43,7 +43,7 @@ class ConfigFileCredentialProviderTests: XCTestCase { XCTAssertEqual((provider as? StaticCredential)?.accessKeyId, "foo") XCTAssertEqual((provider as? StaticCredential)?.secretAccessKey, "bar") - try await provider.shutdown(on: context.eventLoop).get() + try await provider.shutdown() try await httpClient.shutdown() XCTAssertNoThrow(try eventLoopGroup.syncShutdownGracefully()) } @@ -65,7 +65,7 @@ class ConfigFileCredentialProviderTests: XCTestCase { XCTAssertTrue(provider is STSAssumeRoleCredentialProvider) XCTAssertEqual((provider as? STSAssumeRoleCredentialProvider)?.request.roleArn, "arn") - try await provider.shutdown(on: context.eventLoop).get() + try await provider.shutdown() try await httpClient.shutdown() XCTAssertNoThrow(try eventLoopGroup.syncShutdownGracefully()) } @@ -92,7 +92,7 @@ class ConfigFileCredentialProviderTests: XCTestCase { let provider = factory.createProvider(context: .init(httpClient: httpClient, eventLoop: eventLoop, logger: TestEnvironment.logger, options: .init())) - let credential: Credential = try await provider.getCredential(on: eventLoop, logger: TestEnvironment.logger).get() + let credential: Credential = try await provider.getCredential(logger: TestEnvironment.logger) XCTAssertEqual(credential.accessKeyId, "AWSACCESSKEYID") XCTAssertEqual(credential.secretAccessKey, "AWSSECRETACCESSKEY") } @@ -120,7 +120,7 @@ class ConfigFileCredentialProviderTests: XCTestCase { let provider = factory.createProvider(context: .init(httpClient: httpClient, eventLoop: eventLoop, logger: TestEnvironment.logger, options: .init())) - let credential: Credential = try await provider.getCredential(on: eventLoop, logger: TestEnvironment.logger).get() + let credential: Credential = try await provider.getCredential(logger: TestEnvironment.logger) XCTAssertEqual(credential.accessKeyId, "TESTPROFILE-AWSACCESSKEYID") XCTAssertEqual(credential.secretAccessKey, "TESTPROFILE-AWSSECRETACCESSKEY") } @@ -139,7 +139,7 @@ class ConfigFileCredentialProviderTests: XCTestCase { let provider = factory.createProvider(context: .init(httpClient: httpClient, eventLoop: eventLoop, logger: TestEnvironment.logger, options: .init())) do { - _ = try await provider.getCredential(on: eventLoop, logger: TestEnvironment.logger).get() + _ = try await provider.getCredential(logger: TestEnvironment.logger) XCTFail("Should throw error") } catch { XCTAssertEqual(error as? CredentialProviderError, .noProvider) @@ -183,7 +183,7 @@ class ConfigFileCredentialProviderTests: XCTestCase { switch sharedCredentials { case .assumeRole(let aRoleArn, _, _, let sourceCredentialProvider): - let credentials = try await sourceCredentialProvider.createProvider(context: context).getCredential(on: context.eventLoop, logger: context.logger).get() + let credentials = try await sourceCredentialProvider.createProvider(context: context).getCredential(logger: context.logger) XCTAssertEqual(credentials.accessKeyId, accessKey) XCTAssertEqual(credentials.secretAccessKey, secretKey) XCTAssertEqual(aRoleArn, roleArn) @@ -255,9 +255,8 @@ class ConfigFileCredentialProviderTests: XCTestCase { // Retrieve credentials async let futureCredentials: Credential = client.credentialProvider.getCredential( - on: client.eventLoopGroup.next(), logger: TestEnvironment.logger - ).get() + ) try testServer.processRaw { _ in let output = STSAssumeRoleResponse(credentials: stsCredentials) diff --git a/Tests/SotoCoreTests/Credential/ConfigFileLoaderTests.swift b/Tests/SotoCoreTests/Credential/ConfigFileLoaderTests.swift index ebce90ac0..4f3862ef9 100644 --- a/Tests/SotoCoreTests/Credential/ConfigFileLoaderTests.swift +++ b/Tests/SotoCoreTests/Credential/ConfigFileLoaderTests.swift @@ -2,7 +2,7 @@ // // This source file is part of the Soto for AWS open source project // -// Copyright (c) 2017-2020 the Soto project authors +// Copyright (c) 2017-2023 the Soto project authors // Licensed under Apache License v2.0 // // See LICENSE.txt for license information @@ -47,7 +47,7 @@ class ConfigFileLoadersTests: XCTestCase { aws_secret_access_key= \(secretKey) """ - let credentialsPath = try save(content: credentialsFile, prefix: "credentials") + let credentialsPath = try save(content: credentialsFile, prefix: #function) let (context, eventLoopGroup, httpClient) = try makeContext() defer { @@ -93,7 +93,7 @@ class ConfigFileLoadersTests: XCTestCase { region = us-west-1 """ - let credentialsPath = try save(content: credentialsFile, prefix: "credentials") + let credentialsPath = try save(content: credentialsFile, prefix: #function) let configPath = try save(content: configFile, prefix: "config") let (context, eventLoopGroup, httpClient) = try makeContext() @@ -111,11 +111,9 @@ class ConfigFileLoadersTests: XCTestCase { context: context ).get() - defer { XCTAssertNoThrow(try httpClient.syncShutdown()) } - switch sharedCredentials { case .assumeRole(let aRoleArn, let aSessionName, let region, let sourceCredentialProvider): - let credentials = try await sourceCredentialProvider.createProvider(context: context).getCredential(on: context.eventLoop, logger: context.logger).get() + let credentials = try await sourceCredentialProvider.createProvider(context: context).getCredential(logger: context.logger) XCTAssertEqual(credentials.accessKeyId, accessKey) XCTAssertEqual(credentials.secretAccessKey, secretKey) XCTAssertEqual(aRoleArn, roleArn) @@ -135,7 +133,7 @@ class ConfigFileLoadersTests: XCTestCase { credential_source = Ec2InstanceMetadata """ - let credentialsPath = try save(content: credentialsFile, prefix: "credentials") + let credentialsPath = try save(content: credentialsFile, prefix: #function) let (context, eventLoopGroup, httpClient) = try makeContext() defer { @@ -157,6 +155,7 @@ class ConfigFileLoadersTests: XCTestCase { XCTAssertEqual(aRoleArn, roleArn) let rotatingCredentials: RotatingCredentialProvider = try XCTUnwrap(credentialProvider as? RotatingCredentialProvider) XCTAssert(rotatingCredentials.provider is InstanceMetaDataClient) + try await credentialProvider.shutdown() default: XCTFail("Expected credential source") } @@ -170,7 +169,7 @@ class ConfigFileLoadersTests: XCTestCase { aws_secret_access_key= \(secretKey) """ - let credentialsPath = try save(content: credentialsFile, prefix: "credentials") + let credentialsPath = try save(content: credentialsFile, prefix: #function) let (context, eventLoopGroup, httpClient) = try makeContext() defer { @@ -201,7 +200,7 @@ class ConfigFileLoadersTests: XCTestCase { aws_access_key_id = \(accessKey) """ - let credentialsPath = try save(content: credentialsFile, prefix: "credentials") + let credentialsPath = try save(content: credentialsFile, prefix: #function) let (context, eventLoopGroup, httpClient) = try makeContext() defer { @@ -237,7 +236,7 @@ class ConfigFileLoadersTests: XCTestCase { source_profile = \(sourceProfile) """ - let credentialsPath = try save(content: credentialsFile, prefix: "credentials") + let credentialsPath = try save(content: credentialsFile, prefix: #function) let (context, eventLoopGroup, httpClient) = try makeContext() defer { @@ -273,7 +272,7 @@ class ConfigFileLoadersTests: XCTestCase { source_profile = \(sourceProfile) """ - let credentialsPath = try save(content: credentialsFile, prefix: "credentials") + let credentialsPath = try save(content: credentialsFile, prefix: #function) let (context, eventLoopGroup, httpClient) = try makeContext() defer { @@ -304,7 +303,7 @@ class ConfigFileLoadersTests: XCTestCase { role_arn = \(roleArn) """ - let credentialsPath = try save(content: credentialsFile, prefix: "credentials") + let credentialsPath = try save(content: credentialsFile, prefix: #function) let (context, eventLoopGroup, httpClient) = try makeContext() defer { diff --git a/Tests/SotoCoreTests/Credential/CredentialProviderTests.swift b/Tests/SotoCoreTests/Credential/CredentialProviderTests.swift index b8a71e3c8..a4a104478 100644 --- a/Tests/SotoCoreTests/Credential/CredentialProviderTests.swift +++ b/Tests/SotoCoreTests/Credential/CredentialProviderTests.swift @@ -2,7 +2,7 @@ // // This source file is part of the Soto for AWS open source project // -// Copyright (c) 2017-2022 the Soto project authors +// Copyright (c) 2017-2023 the Soto project authors // Licensed under Apache License v2.0 // // See LICENSE.txt for license information @@ -27,19 +27,18 @@ final class CredentialProviderTests: XCTestCase { let group = MultiThreadedEventLoopGroup(numberOfThreads: 1) defer { XCTAssertNoThrow(try group.syncShutdownGracefully()) } - let loop = group.next() - let credential = try await provider.getCredential(on: loop, logger: TestEnvironment.logger).get() + let credential = try await provider.getCredential(logger: TestEnvironment.logger) XCTAssertEqual(credential as? StaticCredential, provider) } // make sure getCredential in client CredentialProvider doesnt get called more than once func testDeferredCredentialProvider() async throws { - final class MyCredentialProvider: AsyncCredentialProvider { + final class MyCredentialProvider: CredentialProvider { let credentialProviderCalled = ManagedAtomic(0) init() {} - func getCredential(on eventLoop: EventLoop, logger: Logger) async throws -> Credential { + func getCredential(logger: Logger) async throws -> Credential { self.credentialProviderCalled.wrappingIncrement(ordering: .sequentiallyConsistent) return StaticCredential(accessKeyId: "ACCESSKEYID", secretAccessKey: "SECRETACCESSKET") } @@ -52,11 +51,32 @@ final class CredentialProviderTests: XCTestCase { let context = CredentialProviderFactory.Context(httpClient: httpClient, eventLoop: eventLoop, logger: TestEnvironment.logger, options: .init()) let myCredentialProvider = MyCredentialProvider() let deferredProvider = DeferredCredentialProvider(context: context, provider: myCredentialProvider) - _ = try await deferredProvider.getCredential(on: eventLoop, logger: TestEnvironment.logger).get() - _ = try await deferredProvider.getCredential(on: eventLoop, logger: TestEnvironment.logger).get() + _ = try await deferredProvider.getCredential(logger: TestEnvironment.logger) + _ = try await deferredProvider.getCredential(logger: TestEnvironment.logger) XCTAssertEqual(myCredentialProvider.credentialProviderCalled.load(ordering: .sequentiallyConsistent), 1) } + // Verify DeferredCredential provider handlers setup and immediate shutdown + func testDeferredCredentialProviderSetupShutdown() async throws { + final class MyCredentialProvider: CredentialProvider { + init() {} + func getCredential(logger: Logger) async throws -> Credential { + try await Task.sleep(nanoseconds: 5_000_000_000) + XCTFail("Should not get here") + return StaticCredential(accessKeyId: "ACCESSKEYID", secretAccessKey: "SECRETACCESSKET") + } + } + let eventLoopGroup = MultiThreadedEventLoopGroup(numberOfThreads: 1) + defer { XCTAssertNoThrow(try eventLoopGroup.syncShutdownGracefully()) } + let httpClient = HTTPClient(eventLoopGroupProvider: .shared(eventLoopGroup)) + defer { XCTAssertNoThrow(try httpClient.syncShutdown()) } + let eventLoop = eventLoopGroup.next() + let context = CredentialProviderFactory.Context(httpClient: httpClient, eventLoop: eventLoop, logger: TestEnvironment.logger, options: .init()) + let myCredentialProvider = MyCredentialProvider() + let deferredProvider = DeferredCredentialProvider(context: context, provider: myCredentialProvider) + try await deferredProvider.shutdown() + } + func testConfigFileSuccess() async throws { let credentials = """ [default] @@ -77,7 +97,7 @@ final class CredentialProviderTests: XCTestCase { let provider = factory.createProvider(context: .init(httpClient: httpClient, eventLoop: eventLoop, logger: TestEnvironment.logger, options: .init())) - let credential = try await provider.getCredential(on: eventLoop, logger: TestEnvironment.logger).get() + let credential = try await provider.getCredential(logger: TestEnvironment.logger) XCTAssertEqual(credential.accessKeyId, "AWSACCESSKEYID") XCTAssertEqual(credential.secretAccessKey, "AWSSECRETACCESSKEY") } @@ -96,7 +116,7 @@ final class CredentialProviderTests: XCTestCase { let provider = factory.createProvider(context: .init(httpClient: httpClient, eventLoop: eventLoop, logger: TestEnvironment.logger, options: .init())) do { - _ = try await provider.getCredential(on: eventLoop, logger: TestEnvironment.logger).get() + _ = try await provider.getCredential(logger: TestEnvironment.logger) XCTFail("Should provide credential") } catch { XCTAssertEqual(error as? CredentialProviderError, .noProvider) @@ -108,13 +128,12 @@ final class CredentialProviderTests: XCTestCase { let hasShutdown = ManagedAtomic(false) init() {} - func getCredential(on eventLoop: EventLoop, logger: Logger) -> EventLoopFuture { - return eventLoop.makeSucceededFuture(StaticCredential(accessKeyId: "", secretAccessKey: "")) + func getCredential(logger: Logger) async throws -> Credential { + return StaticCredential(accessKeyId: "", secretAccessKey: "") } - func shutdown(on eventLoop: EventLoop) -> EventLoopFuture { + func shutdown() async throws { self.hasShutdown.store(true, ordering: .sequentiallyConsistent) - return eventLoop.makeSucceededFuture(()) } } let eventLoopGroup = MultiThreadedEventLoopGroup(numberOfThreads: 1) @@ -126,7 +145,7 @@ final class CredentialProviderTests: XCTestCase { let testCredentialProvider = TestCredentialProvider() let deferredProvider = DeferredCredentialProvider(context: context, provider: testCredentialProvider) - try await deferredProvider.shutdown(on: eventLoopGroup.next()).get() + try await deferredProvider.shutdown() XCTAssertEqual(testCredentialProvider.hasShutdown.load(ordering: .sequentiallyConsistent), true) } } diff --git a/Tests/SotoCoreTests/Credential/MetaDataCredentialProviderTests.swift b/Tests/SotoCoreTests/Credential/MetaDataCredentialProviderTests.swift index 5e92540e1..d728e6762 100644 --- a/Tests/SotoCoreTests/Credential/MetaDataCredentialProviderTests.swift +++ b/Tests/SotoCoreTests/Credential/MetaDataCredentialProviderTests.swift @@ -2,7 +2,7 @@ // // This source file is part of the Soto for AWS open source project // -// Copyright (c) 2017-2020 the Soto project authors +// Copyright (c) 2017-2023 the Soto project authors // Licensed under Apache License v2.0 // // See LICENSE.txt for license information @@ -37,7 +37,7 @@ class MetaDataCredentialProviderTests: XCTestCase { defer { Environment.unset(name: ECSMetaDataClient.RelativeURIEnvironmentName) } let client = ECSMetaDataClient(httpClient: httpClient, host: testServer.address) - async let metaDataTask = client!.getMetaData(on: loop, logger: TestEnvironment.logger).get() + async let metaDataTask = client!.getMetaData(logger: TestEnvironment.logger) // run fake server XCTAssertNoThrow(try testServer.ecsMetadataServer(path: path)) @@ -85,7 +85,7 @@ class MetaDataCredentialProviderTests: XCTestCase { defer { Environment.unset(name: ECSMetaDataClient.RelativeURIEnvironmentName) } let client = InstanceMetaDataClient(httpClient: httpClient, host: testServer.address) - async let metaDataTask = client.getMetaData(on: loop, logger: TestEnvironment.logger).get() + async let metaDataTask = client.getMetaData(logger: TestEnvironment.logger) // run fake server XCTAssertNoThrow(try testServer.ec2MetadataServer(version: .v2)) @@ -112,7 +112,7 @@ class MetaDataCredentialProviderTests: XCTestCase { let client = InstanceMetaDataClient(httpClient: httpClient, host: testServer.address) - async let metaDataTask = client.getMetaData(on: loop, logger: TestEnvironment.logger).get() + async let metaDataTask = client.getMetaData(logger: TestEnvironment.logger) // run fake server XCTAssertNoThrow(try testServer.ec2MetadataServer(version: .v1)) diff --git a/Tests/SotoCoreTests/Credential/RotatingCredentialProviderTests.swift b/Tests/SotoCoreTests/Credential/RotatingCredentialProviderTests.swift index 48c10f2fd..710560938 100644 --- a/Tests/SotoCoreTests/Credential/RotatingCredentialProviderTests.swift +++ b/Tests/SotoCoreTests/Credential/RotatingCredentialProviderTests.swift @@ -2,7 +2,7 @@ // // This source file is part of the Soto for AWS open source project // -// Copyright (c) 2017-2022 the Soto project authors +// Copyright (c) 2017-2023 the Soto project authors // Licensed under Apache License v2.0 // // See LICENSE.txt for license information @@ -27,19 +27,46 @@ import SotoTestUtils import XCTest class RotatingCredentialProviderTests: XCTestCase { - final class RotatingCredentialTestClient: AsyncCredentialProvider { - typealias TestCallback = @Sendable () -> ExpiringCredential + final class RotatingCredentialTestClient: CredentialProvider { + typealias TestCallback = @Sendable () async throws -> ExpiringCredential let callback: TestCallback init(_ callback: @escaping TestCallback) { self.callback = callback } - func getCredential(on eventLoop: EventLoop, logger: Logger) async throws -> Credential { - self.callback() + func getCredential(logger: Logger) async throws -> Credential { + try await self.callback() } } + func testSetupShutdown() async throws { + let group = MultiThreadedEventLoopGroup(numberOfThreads: 1) + defer { XCTAssertNoThrow(try group.syncShutdownGracefully()) } + let httpClient = HTTPClient(eventLoopGroupProvider: .shared(group)) + defer { XCTAssertNoThrow(try httpClient.syncShutdown()) } + let loop = group.next() + + let client = RotatingCredentialTestClient { + try await Task.sleep(nanoseconds: 5_000_000_000) + XCTFail("Should not get here") + return TestExpiringCredential( + accessKeyId: "abc123", + secretAccessKey: "abc123", + sessionToken: "abc123", + expiration: Date(timeIntervalSinceNow: 24 * 60 * 60) + ) + } + let context = CredentialProviderFactory.Context( + httpClient: httpClient, + eventLoop: loop, + logger: Logger(label: "soto"), + options: .init() + ) + let provider = RotatingCredentialProvider(context: context, provider: client) + try await provider.shutdown() + } + func testGetCredentialAndReuseIfStillValid() async throws { let group = MultiThreadedEventLoopGroup(numberOfThreads: 1) defer { XCTAssertNoThrow(try group.syncShutdownGracefully()) } @@ -63,7 +90,7 @@ class RotatingCredentialProviderTests: XCTestCase { let provider = RotatingCredentialProvider(context: context, provider: client) // get credentials for first time - var returned = try await provider.getCredential(on: loop, logger: Logger(label: "soto")).get() + var returned = try await provider.getCredential(logger: Logger(label: "soto")) XCTAssertEqual(returned.accessKeyId, cred.accessKeyId) XCTAssertEqual(returned.secretAccessKey, cred.secretAccessKey) @@ -71,7 +98,7 @@ class RotatingCredentialProviderTests: XCTestCase { XCTAssertEqual((returned as? TestExpiringCredential)?.expiration, cred.expiration) // get credentials a second time, callback must not be hit - returned = try await provider.getCredential(on: loop, logger: Logger(label: "soto")).get() + returned = try await provider.getCredential(logger: Logger(label: "soto")) XCTAssertEqual(returned.accessKeyId, cred.accessKeyId) XCTAssertEqual(returned.secretAccessKey, cred.secretAccessKey) @@ -109,7 +136,7 @@ class RotatingCredentialProviderTests: XCTestCase { try await withThrowingTaskGroup(of: Void.self) { group in for _ in 0.. Bool { - if let expiration = self.expiration { - return expiration.timeIntervalSinceNow < interval - } - return false - } } diff --git a/Tests/SotoCoreTests/Credential/RuntimeSelectorCredentialProviderTests.swift b/Tests/SotoCoreTests/Credential/RuntimeSelectorCredentialProviderTests.swift index 83c554e62..65207065a 100644 --- a/Tests/SotoCoreTests/Credential/RuntimeSelectorCredentialProviderTests.swift +++ b/Tests/SotoCoreTests/Credential/RuntimeSelectorCredentialProviderTests.swift @@ -2,7 +2,7 @@ // // This source file is part of the Soto for AWS open source project // -// Copyright (c) 2017-2020 the Soto project authors +// Copyright (c) 2017-2023 the Soto project authors // Licensed under Apache License v2.0 // // See LICENSE.txt for license information @@ -22,7 +22,7 @@ class RuntimeSelectorCredentialProviderTests: XCTestCase { let client = createAWSClient(credentialProvider: .selector(.custom { _ in return NullCredentialProvider() })) defer { XCTAssertNoThrow(try client.syncShutdown()) } do { - _ = try await client.credentialProvider.getCredential(on: client.eventLoopGroup.next(), logger: TestEnvironment.logger).get() + _ = try await client.credentialProvider.getCredential(logger: TestEnvironment.logger) XCTFail("Shouldn't get here") } catch { XCTAssertEqual(error as? CredentialProviderError, CredentialProviderError.noProvider) @@ -45,11 +45,11 @@ class RuntimeSelectorCredentialProviderTests: XCTestCase { let client = createAWSClient(credentialProvider: .selector(.environment, .empty)) defer { XCTAssertNoThrow(try client.syncShutdown()) } - let credential = try await client.credentialProvider.getCredential(on: client.eventLoopGroup.next(), logger: TestEnvironment.logger).get() + let credential = try await client.credentialProvider.getCredential(logger: TestEnvironment.logger) XCTAssertEqual(credential.accessKeyId, accessKeyId) XCTAssertEqual(credential.secretAccessKey, secretAccessKey) XCTAssertEqual(credential.sessionToken, sessionToken) - let internalProvider = try XCTUnwrap((client.credentialProvider as? RuntimeSelectorCredentialProvider)?.internalProvider) + let internalProvider = try await (client.credentialProvider as? RuntimeSelectorCredentialProvider)?.getCredentialProviderTask() XCTAssert(internalProvider is StaticCredential) Environment.unset(name: "AWS_ACCESS_KEY_ID") @@ -64,7 +64,7 @@ class RuntimeSelectorCredentialProviderTests: XCTestCase { let client = createAWSClient(credentialProvider: provider) defer { XCTAssertNoThrow(try client.syncShutdown()) } do { - _ = try await client.credentialProvider.getCredential(on: client.eventLoopGroup.next(), logger: TestEnvironment.logger).get() + _ = try await client.credentialProvider.getCredential(logger: TestEnvironment.logger) XCTFail("Should not get here") } catch { XCTAssertEqual(error as? CredentialProviderError, CredentialProviderError.noProvider) @@ -75,11 +75,11 @@ class RuntimeSelectorCredentialProviderTests: XCTestCase { let provider: CredentialProviderFactory = .selector(.empty, .environment) let client = createAWSClient(credentialProvider: provider) defer { XCTAssertNoThrow(try client.syncShutdown()) } - let credential = try await client.credentialProvider.getCredential(on: client.eventLoopGroup.next(), logger: TestEnvironment.logger).get() + let credential = try await client.credentialProvider.getCredential(logger: TestEnvironment.logger) XCTAssertEqual(credential.accessKeyId, "") XCTAssertEqual(credential.secretAccessKey, "") XCTAssertEqual(credential.sessionToken, nil) - let internalProvider = try XCTUnwrap((client.credentialProvider as? RuntimeSelectorCredentialProvider)?.internalProvider) + let internalProvider = try await (client.credentialProvider as? RuntimeSelectorCredentialProvider)?.getCredentialProviderTask() XCTAssert(internalProvider is StaticCredential) } @@ -87,7 +87,7 @@ class RuntimeSelectorCredentialProviderTests: XCTestCase { let provider: CredentialProviderFactory = .selector(.empty) let client = createAWSClient(credentialProvider: provider) defer { XCTAssertNoThrow(try client.syncShutdown()) } - let credential = try await client.credentialProvider.getCredential(on: client.eventLoopGroup.next(), logger: TestEnvironment.logger).get() + let credential = try await client.credentialProvider.getCredential(logger: TestEnvironment.logger) XCTAssert(credential.isEmpty()) XCTAssert(client.credentialProvider is StaticCredential) } @@ -112,7 +112,7 @@ class RuntimeSelectorCredentialProviderTests: XCTestCase { let client = createAWSClient(credentialProvider: provider) defer { XCTAssertNoThrow(try client.syncShutdown()) } - async let credentialTask = client.credentialProvider.getCredential(on: client.eventLoopGroup.next(), logger: TestEnvironment.logger).get() + async let credentialTask = client.credentialProvider.getCredential(logger: TestEnvironment.logger) XCTAssertNoThrow(try testServer.ecsMetadataServer(path: path)) @@ -120,7 +120,7 @@ class RuntimeSelectorCredentialProviderTests: XCTestCase { XCTAssertEqual(credential.accessKeyId, AWSTestServer.ECSMetaData.default.accessKeyId) XCTAssertEqual(credential.secretAccessKey, AWSTestServer.ECSMetaData.default.secretAccessKey) XCTAssertEqual(credential.sessionToken, AWSTestServer.ECSMetaData.default.token) - let internalProvider = try XCTUnwrap((client.credentialProvider as? RuntimeSelectorCredentialProvider)?.internalProvider) + let internalProvider = try await (client.credentialProvider as? RuntimeSelectorCredentialProvider)?.getCredentialProviderTask() XCTAssert(internalProvider is RotatingCredentialProvider) } @@ -132,7 +132,7 @@ class RuntimeSelectorCredentialProviderTests: XCTestCase { let client = createAWSClient(credentialProvider: provider) defer { XCTAssertNoThrow(try client.syncShutdown()) } do { - _ = try await client.credentialProvider.getCredential(on: client.eventLoopGroup.next(), logger: TestEnvironment.logger).get() + _ = try await client.credentialProvider.getCredential(logger: TestEnvironment.logger) XCTFail("Should not get here") } catch { XCTAssertEqual(error as? CredentialProviderError, .noProvider) @@ -149,10 +149,7 @@ class RuntimeSelectorCredentialProviderTests: XCTestCase { let client = createAWSClient(credentialProvider: .selector(customEC2, .empty)) defer { XCTAssertNoThrow(try client.syncShutdown()) } - async let credentialTask = client.credentialProvider.getCredential( - on: client.eventLoopGroup.next(), - logger: TestEnvironment.logger - ).get() + async let credentialTask = client.credentialProvider.getCredential(logger: TestEnvironment.logger) XCTAssertNoThrow(try testServer.ec2MetadataServer(version: .v2)) @@ -160,7 +157,7 @@ class RuntimeSelectorCredentialProviderTests: XCTestCase { XCTAssertEqual(credential.accessKeyId, AWSTestServer.EC2InstanceMetaData.default.accessKeyId) XCTAssertEqual(credential.secretAccessKey, AWSTestServer.EC2InstanceMetaData.default.secretAccessKey) XCTAssertEqual(credential.sessionToken, AWSTestServer.EC2InstanceMetaData.default.token) - let internalProvider = try XCTUnwrap((client.credentialProvider as? RuntimeSelectorCredentialProvider)?.internalProvider) + let internalProvider = try await (client.credentialProvider as? RuntimeSelectorCredentialProvider)?.getCredentialProviderTask() XCTAssert(internalProvider is RotatingCredentialProvider) } @@ -177,19 +174,19 @@ class RuntimeSelectorCredentialProviderTests: XCTestCase { let client = createAWSClient(credentialProvider: .selector(.configFile(credentialsFilePath: filename), .empty)) defer { XCTAssertNoThrow(try client.syncShutdown()) } - let credential = try await client.credentialProvider.getCredential(on: client.eventLoopGroup.next(), logger: TestEnvironment.logger).get() + let credential = try await client.credentialProvider.getCredential(logger: TestEnvironment.logger) XCTAssertEqual(credential.accessKeyId, "AWSACCESSKEYID") XCTAssertEqual(credential.secretAccessKey, "AWSSECRETACCESSKEY") XCTAssertEqual(credential.sessionToken, nil) - let internalProvider = try XCTUnwrap((client.credentialProvider as? RuntimeSelectorCredentialProvider)?.internalProvider) + let internalProvider = try await (client.credentialProvider as? RuntimeSelectorCredentialProvider)?.getCredentialProviderTask() XCTAssert(internalProvider is RotatingCredentialProvider) } func testConfigFileProviderFail() async throws { let client = createAWSClient(credentialProvider: .selector(.configFile(credentialsFilePath: "nonExistentCredentialFile"), .empty)) defer { XCTAssertNoThrow(try client.syncShutdown()) } - _ = try await client.credentialProvider.getCredential(on: client.eventLoopGroup.next(), logger: TestEnvironment.logger).get() - let internalProvider = try XCTUnwrap((client.credentialProvider as? RuntimeSelectorCredentialProvider)?.internalProvider) + _ = try await client.credentialProvider.getCredential(logger: TestEnvironment.logger) + let internalProvider = try await (client.credentialProvider as? RuntimeSelectorCredentialProvider)?.getCredentialProviderTask() XCTAssert(internalProvider is StaticCredential) XCTAssert((internalProvider as? StaticCredential)?.isEmpty() == true) } diff --git a/Tests/SotoCoreTests/Credential/STSAssumeRoleTests.swift b/Tests/SotoCoreTests/Credential/STSAssumeRoleTests.swift index b8adda1fd..fae0d33ac 100644 --- a/Tests/SotoCoreTests/Credential/STSAssumeRoleTests.swift +++ b/Tests/SotoCoreTests/Credential/STSAssumeRoleTests.swift @@ -2,7 +2,7 @@ // // This source file is part of the Soto for AWS open source project // -// Copyright (c) 2017-2020 the Soto project authors +// Copyright (c) 2017-2023 the Soto project authors // Licensed under Apache License v2.0 // // See LICENSE.txt for license information @@ -43,7 +43,7 @@ class STSAssumeRoleTests: XCTestCase { logger: TestEnvironment.logger ) defer { XCTAssertNoThrow(try client.syncShutdown()) } - + async let credentialTask: Credential = client.credentialProvider.getCredential(logger: TestEnvironment.logger) XCTAssertNoThrow(try testServer.processRaw { _ in let output = STSAssumeRoleResponse(credentials: credentials) let xml = try XMLEncoder().encode(output) @@ -51,8 +51,8 @@ class STSAssumeRoleTests: XCTestCase { let response = AWSTestServer.Response(httpStatus: .ok, headers: [:], body: byteBuffer) return .result(response) }) - let result = try await client.credentialProvider.getCredential(on: client.eventLoopGroup.next(), logger: TestEnvironment.logger).get() - let stsCredentials = result as? STSCredentials + let credential = try await credentialTask + let stsCredentials = credential as? STSCredentials XCTAssertEqual(stsCredentials?.accessKeyId, credentials.accessKeyId) XCTAssertEqual(stsCredentials?.secretAccessKey, credentials.secretAccessKey) XCTAssertEqual(stsCredentials?.sessionToken, credentials.sessionToken) diff --git a/Tests/SotoCoreTests/Credential/StaticCredential+EnvironmentTests.swift b/Tests/SotoCoreTests/Credential/StaticCredential+EnvironmentTests.swift index 0746246fe..24c342115 100644 --- a/Tests/SotoCoreTests/Credential/StaticCredential+EnvironmentTests.swift +++ b/Tests/SotoCoreTests/Credential/StaticCredential+EnvironmentTests.swift @@ -2,7 +2,7 @@ // // This source file is part of the Soto for AWS open source project // -// Copyright (c) 2017-2020 the Soto project authors +// Copyright (c) 2017-2023 the Soto project authors // Licensed under Apache License v2.0 // // See LICENSE.txt for license information diff --git a/Tests/SotoCoreTests/MiddlewareTests.swift b/Tests/SotoCoreTests/MiddlewareTests.swift index 0cb30585d..b74fd9060 100644 --- a/Tests/SotoCoreTests/MiddlewareTests.swift +++ b/Tests/SotoCoreTests/MiddlewareTests.swift @@ -48,7 +48,7 @@ class MiddlewareTests: XCTestCase { let error = try XCTUnwrap(error as? CatchRequestError) test(error.request) } - try client.syncShutdown() + try await client.shutdown() } func testMiddlewareAppliedOnce() async throws { diff --git a/scripts/validate.sh b/scripts/validate.sh index 24308dea6..46b08c835 100755 --- a/scripts/validate.sh +++ b/scripts/validate.sh @@ -3,7 +3,7 @@ ## ## This source file is part of the SwiftNIO open source project ## -## Copyright (c) 2017-2021 Apple Inc. and the SwiftNIO project authors +## Copyright (c) 2017-2023 Apple Inc. and the SwiftNIO project authors ## Licensed under Apache License v2.0 ## ## See LICENSE.txt for license information @@ -13,8 +13,7 @@ ## ##===----------------------------------------------------------------------===## -SWIFT_VERSION=5.2 -SWIFTFORMAT_VERSION=0.48.17 +SWIFTFORMAT_VERSION=0.51.10 set -eu here="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"