Skip to content

Commit

Permalink
feat!: Account ID-based endpoints (#1841)
Browse files Browse the repository at this point in the history
  • Loading branch information
jbelkins authored Jan 16, 2025
1 parent 8ab0a85 commit ae7d140
Show file tree
Hide file tree
Showing 26 changed files with 515 additions and 116 deletions.
2 changes: 2 additions & 0 deletions IntegrationTests/Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ let package = Package(
private var integrationTestTargets: [Target] {
let integrationTests = [
"AWSCloudFrontKeyValueStore",
"AWSDynamoDB",
"AWSEC2",
"AWSECS",
"AWSEventBridge",
Expand Down Expand Up @@ -112,3 +113,4 @@ private func integrationTestTarget(_ name: String) -> Target {
resources: [.process("Resources")]
)
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
//
// Copyright Amazon.com Inc. or its affiliates.
// All Rights Reserved.
//
// SPDX-License-Identifier: Apache-2.0
//

import XCTest
import AWSDynamoDB
import SmithyTestUtil
import SmithyIdentity
import enum AWSClientRuntime.AccountIDEndpointMode
import enum ClientRuntime.EndpointError

final class AccountIDEndpointModeTests: XCTestCase {
private let accountID = "0123456789"

// MARK: - Tests

// MARK: nil

func test_nilMode_prefersByDefault() async throws {
let subject = try await subject(accountIDEndpointMode: nil, setAccountID: true)

do {
_ = try await subject.getItem(input: GetItemInput())
XCTFail("Request should have thrown")
} catch TestCheckError.actual(let request) {
XCTAssert(request.endpoint.host.contains(accountID))
} catch {
XCTFail("Request should have succeeded, threw error: \(error)")
}
}

func test_nilMode_createsEndpointWithoutAccountID() async throws {
let subject = try await subject(accountIDEndpointMode: nil, setAccountID: false)

do {
_ = try await subject.getItem(input: GetItemInput())
XCTFail("Request should have thrown")
} catch TestCheckError.actual(let request) {
XCTAssertFalse(request.endpoint.host.contains(accountID))
} catch {
XCTFail("Request failed on unexpected error")
}
}

// MARK: preferred

func test_preferredMode_prefersByDefault() async throws {
let subject = try await subject(accountIDEndpointMode: .preferred, setAccountID: true)

do {
_ = try await subject.getItem(input: GetItemInput())
XCTFail("Request should have thrown")
} catch TestCheckError.actual(let request) {
XCTAssert(request.endpoint.host.contains(accountID))
} catch {
XCTFail("Request should have succeeded, threw error: \(error)")
}
}

func test_preferredMode_createsEndpointWithoutAccountID() async throws {
let subject = try await subject(accountIDEndpointMode: .preferred, setAccountID: false)

do {
_ = try await subject.getItem(input: GetItemInput())
XCTFail("Request should have thrown")
} catch TestCheckError.actual(let request) {
XCTAssertFalse(request.endpoint.host.contains(accountID))
} catch {
XCTFail("Request failed on unexpected error")
}
}

// MARK: required

func test_requiredMode_createsEndpointWithAccountID() async throws {
let subject = try await subject(accountIDEndpointMode: .required, setAccountID: true)

do {
_ = try await subject.getItem(input: GetItemInput())
XCTFail("Request should have thrown")
} catch TestCheckError.actual(let request) {
XCTAssert(request.endpoint.host.contains(accountID))
} catch {
XCTFail("Request should have succeeded, threw error: \(error)")
}
}

func test_requiredMode_failsWhenRequiredButNotPresent() async throws {
let subject = try await subject(accountIDEndpointMode: .required, setAccountID: false)

do {
_ = try await subject.getItem(input: GetItemInput())
XCTFail("Request should have thrown")
} catch EndpointError.unresolved {
// No action, test succeeded
} catch {
XCTFail("Request failed on unexpected error")
}
}

// MARK: disabled

func test_disabledMode_createsEndpointWithoutAccountIDWhenNil() async throws {
let subject = try await subject(accountIDEndpointMode: .disabled, setAccountID: false)

do {
_ = try await subject.getItem(input: GetItemInput())
XCTFail("Request should have thrown")
} catch TestCheckError.actual(let request) {
XCTAssertFalse(request.endpoint.host.contains(accountID))
} catch {
XCTFail("Request failed on unexpected error")
}
}

func test_disabledMode_createsEndpointWithoutAccountIDWhenSupplied() async throws {
let subject = try await subject(accountIDEndpointMode: .disabled, setAccountID: true)

do {
_ = try await subject.getItem(input: GetItemInput())
XCTFail("Request should have thrown")
} catch TestCheckError.actual(let request) {
XCTAssertFalse(request.endpoint.host.contains(accountID))
} catch {
XCTFail("Request failed on unexpected error")
}
}

// MARK: - Private methods

private func subject(
accountIDEndpointMode: AccountIDEndpointMode?,
setAccountID: Bool
) async throws -> DynamoDBClient {
let accountID = setAccountID ? self.accountID : nil
let credentials = AWSCredentialIdentity(accessKey: "abc", secret: "def", accountID: accountID)
let resolver = try StaticAWSCredentialIdentityResolver(credentials)
let config = try await DynamoDBClient.Config(
awsCredentialIdentityResolver: resolver,
region: "us-east-1",
accountIdEndpointMode: accountIDEndpointMode,
httpClientEngine: ProtocolTestClient()
)
return DynamoDBClient(config: config)
}
}
Empty file.
55 changes: 31 additions & 24 deletions IntegrationTests/XCTestPlans/AWSIntegrationTestsOnCI.xctestplan
Original file line number Diff line number Diff line change
Expand Up @@ -17,43 +17,43 @@
{
"target" : {
"containerPath" : "container:",
"identifier" : "AWSCognitoIdentityIntegrationTests",
"name" : "AWSCognitoIdentityIntegrationTests"
"identifier" : "AWSSQSIntegrationTests",
"name" : "AWSSQSIntegrationTests"
}
},
{
"target" : {
"containerPath" : "container:",
"identifier" : "AWSSTSIntegrationTests",
"name" : "AWSSTSIntegrationTests"
"identifier" : "AWSKinesisIntegrationTests",
"name" : "AWSKinesisIntegrationTests"
}
},
{
"target" : {
"containerPath" : "container:",
"identifier" : "AWSTranscribeStreamingIntegrationTests",
"name" : "AWSTranscribeStreamingIntegrationTests"
"identifier" : "AWSEC2IntegrationTests",
"name" : "AWSEC2IntegrationTests"
}
},
{
"target" : {
"containerPath" : "container:",
"identifier" : "AWSECSIntegrationTests",
"name" : "AWSECSIntegrationTests"
"identifier" : "AWSSTSIntegrationTests",
"name" : "AWSSTSIntegrationTests"
}
},
{
"target" : {
"containerPath" : "container:",
"identifier" : "AWSGlacierIntegrationTests",
"name" : "AWSGlacierIntegrationTests"
"identifier" : "AWSDynamoDBIntegrationTests",
"name" : "AWSDynamoDBIntegrationTests"
}
},
{
"target" : {
"containerPath" : "container:",
"identifier" : "AWSMediaConvertIntegrationTests",
"name" : "AWSMediaConvertIntegrationTests"
"identifier" : "AWSECSIntegrationTests",
"name" : "AWSECSIntegrationTests"
}
},
{
Expand All @@ -66,43 +66,50 @@
{
"target" : {
"containerPath" : "container:",
"identifier" : "AWSSQSIntegrationTests",
"name" : "AWSSQSIntegrationTests"
"identifier" : "AWSS3IntegrationTests",
"name" : "AWSS3IntegrationTests"
}
},
{
"target" : {
"containerPath" : "container:",
"identifier" : "AWSEventBridgeIntegrationTests",
"name" : "AWSEventBridgeIntegrationTests"
"identifier" : "AWSCognitoIdentityIntegrationTests",
"name" : "AWSCognitoIdentityIntegrationTests"
}
},
{
"target" : {
"containerPath" : "container:",
"identifier" : "AWSEC2IntegrationTests",
"name" : "AWSEC2IntegrationTests"
"identifier" : "AWSTranscribeStreamingIntegrationTests",
"name" : "AWSTranscribeStreamingIntegrationTests"
}
},
{
"target" : {
"containerPath" : "container:",
"identifier" : "AWSKinesisIntegrationTests",
"name" : "AWSKinesisIntegrationTests"
"identifier" : "AWSRoute53IntegrationTests",
"name" : "AWSRoute53IntegrationTests"
}
},
{
"target" : {
"containerPath" : "container:",
"identifier" : "AWSRoute53IntegrationTests",
"name" : "AWSRoute53IntegrationTests"
"identifier" : "AWSGlacierIntegrationTests",
"name" : "AWSGlacierIntegrationTests"
}
},
{
"target" : {
"containerPath" : "container:",
"identifier" : "AWSS3IntegrationTests",
"name" : "AWSS3IntegrationTests"
"identifier" : "AWSMediaConvertIntegrationTests",
"name" : "AWSMediaConvertIntegrationTests"
}
},
{
"target" : {
"containerPath" : "container:",
"identifier" : "AWSEventBridgeIntegrationTests",
"name" : "AWSEventBridgeIntegrationTests"
}
}
],
Expand Down
2 changes: 1 addition & 1 deletion Package.version.next
Original file line number Diff line number Diff line change
@@ -1 +1 @@
1.1.2
1.2.0
Original file line number Diff line number Diff line change
Expand Up @@ -182,4 +182,19 @@ public class AWSClientConfigDefaultsProvider {
fileBasedConfig: fileBasedConfig
)
}

public static func accountIDEndpointMode(
_ accountIDEndpointMode: AccountIDEndpointMode? = nil
) throws -> AccountIDEndpointMode {
let fileBasedConfig = try CRTFileBasedConfiguration.make()
if let accountIDEndpointMode {
return accountIDEndpointMode
} else {
return AWSEndpointConfig.accountIDEndpointMode(
configValue: accountIDEndpointMode,
profileName: nil,
fileBasedConfig: fileBasedConfig
)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,22 @@ import struct Smithy.SwiftLogger
@_spi(FileBasedConfig) import AWSSDKCommon

public enum AWSEndpointConfig {

static func accountIDEndpointMode(
configValue: AccountIDEndpointMode?,
profileName: String?,
fileBasedConfig: FileBasedConfiguration
) -> AccountIDEndpointMode {
FieldResolver(
configValue: configValue,
envVarName: "AWS_ACCOUNT_ID_ENDPOINT_MODE",
configFieldName: "account_id_endpoint_mode",
fileBasedConfig: fileBasedConfig,
profileName: profileName,
converter: { AccountIDEndpointMode(rawValue: $0) }
).value ?? .preferred
}

static func configuredEndpoint(
sdkID: String,
ignoreConfiguredEndpointURLs: Bool?,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
//
// Copyright Amazon.com Inc. or its affiliates.
// All Rights Reserved.
//
// SPDX-License-Identifier: Apache-2.0
//

/// Determines how & whether an account ID-based endpoint will be used for requests.
public enum AccountIDEndpointMode: String, Equatable {
// Note : these case names match string values for the accountIdEndpointMode endpoint param.
// Do not rename them

/// An account ID-based endpoint will be used if an account ID is available.
///
/// This is the default mode.
case preferred // the default case

/// An account ID-based endpoint will never be used.
///
/// The request will fail if a non-account ID-based endpoint is not available.
case disabled

/// An account ID-based endpoint will always be used.
///
/// The request will fail if an account ID-based endpoint cannot be constructed.
case required
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
//
// Copyright Amazon.com Inc. or its affiliates.
// All Rights Reserved.
//
// SPDX-License-Identifier: Apache-2.0
//

import struct Smithy.Attributes
import struct Smithy.AttributeKey
import class Smithy.Context
import class Smithy.ContextBuilder

public extension Context {

var accountIDEndpointMode: AccountIDEndpointMode? {
get { get(key: accountIDEndpointModeKey) }
set { set(key: accountIDEndpointModeKey, value: newValue) }
}
}

public extension ContextBuilder {

@discardableResult
func withAccountIDEndpointMode(value: AccountIDEndpointMode?) -> Self {
attributes.set(key: accountIDEndpointModeKey, value: value)
return self
}
}

private let accountIDEndpointModeKey = AttributeKey<AccountIDEndpointMode>(name: "AccountIDEndpointMode")
Loading

0 comments on commit ae7d140

Please sign in to comment.