diff --git a/codegen/smithy-aws-typescript-codegen/src/main/java/software/amazon/smithy/aws/typescript/codegen/AddAccountIdEndpointModeRuntimeConfig.java b/codegen/smithy-aws-typescript-codegen/src/main/java/software/amazon/smithy/aws/typescript/codegen/AddAccountIdEndpointModeRuntimeConfig.java new file mode 100644 index 000000000000..fcd413bacc0a --- /dev/null +++ b/codegen/smithy-aws-typescript-codegen/src/main/java/software/amazon/smithy/aws/typescript/codegen/AddAccountIdEndpointModeRuntimeConfig.java @@ -0,0 +1,109 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + + package software.amazon.smithy.aws.typescript.codegen; + + import static software.amazon.smithy.aws.typescript.codegen.AwsTraitsUtils.isAwsService; + import static software.amazon.smithy.aws.typescript.codegen.AwsTraitsUtils.isSigV4Service; + + import java.util.HashMap; + import java.util.Map; + import java.util.Optional; + import java.util.function.Consumer; + import java.util.logging.Logger; + import software.amazon.smithy.codegen.core.SymbolProvider; + import software.amazon.smithy.model.Model; + import software.amazon.smithy.model.shapes.ServiceShape; + import software.amazon.smithy.rulesengine.traits.EndpointRuleSetTrait; + import software.amazon.smithy.typescript.codegen.LanguageTarget; + import software.amazon.smithy.typescript.codegen.TypeScriptDependency; + import software.amazon.smithy.typescript.codegen.TypeScriptSettings; + import software.amazon.smithy.typescript.codegen.TypeScriptWriter; + import software.amazon.smithy.typescript.codegen.endpointsV2.RuleSetParameterFinder; + import software.amazon.smithy.typescript.codegen.integration.TypeScriptIntegration; + import software.amazon.smithy.utils.SmithyInternalApi; + + /** + * Generates accountIdEndpointMode configuration field for service clients + * that have the AccountIdEndpointMode built-in param in the ruleset. + */ + @SmithyInternalApi + public final class AddAccountIdEndpointModeRuntimeConfig implements TypeScriptIntegration { + + private static final Logger LOGGER = Logger.getLogger(AddAccountIdEndpointModeRuntimeConfig.class.getName()); + + @Override + public void addConfigInterfaceFields( + TypeScriptSettings settings, + Model model, + SymbolProvider symbolProvider, + TypeScriptWriter writer + ) { + if (isAwsService(settings, model)) { + ServiceShape service = settings.getService(model); + Optional endpointRuleSetTrait = service.getTrait(EndpointRuleSetTrait.class); + if (endpointRuleSetTrait.isPresent()) { + RuleSetParameterFinder ruleSetParameterFinder = new RuleSetParameterFinder(service); + if (ruleSetParameterFinder.getBuiltInParams().containsKey("AccountIdEndpointMode")) { + writer.addDependency(AwsDependency.AWS_SDK_CORE); + // TODO: change to addImportSubmodule when available; smithy-ts, #pull-1280 + writer.addImport("AccountIdEndpointMode", "AccountIdEndpointMode", + "@aws-sdk/core/account-id-endpoint"); + writer.writeDocs("Defines if the AWS AccountId will be used for endpoint routing."); + writer.write("accountIdEndpointMode?: AccountIdEndpointMode | " + + "__Provider;\n"); + } + } + } + } + + @Override + public Map> getRuntimeConfigWriters( + TypeScriptSettings settings, + Model model, + SymbolProvider symbolProvider, + LanguageTarget target + ) { + ServiceShape service = settings.getService(model); + Map> runtimeConfigs = new HashMap<>(); + if (isAwsService(settings, model) || isSigV4Service(settings, model)) { + Optional endpointRuleSetTrait = service.getTrait(EndpointRuleSetTrait.class); + if (endpointRuleSetTrait.isPresent()) { + RuleSetParameterFinder ruleSetParameterFinder = new RuleSetParameterFinder(service); + if (ruleSetParameterFinder.getBuiltInParams().containsKey("AccountIdEndpointMode")) { + switch (target) { + case BROWSER: + runtimeConfigs.put("accountIdEndpointMode", writer -> { + writer.addDependency(AwsDependency.AWS_SDK_CORE); + // TODO: change to addImportSubmodule when available + writer.addImport("DEFAULT_ACCOUNT_ID_ENDPOINT_MODE", "DEFAULT_ACCOUNT_ID_ENDPOINT_MODE", + "@aws-sdk/core/account-id-endpoint"); + writer.write("(() => Promise.resolve(DEFAULT_ACCOUNT_ID_ENDPOINT_MODE))"); + }); + break; + case NODE: + runtimeConfigs.put("accountIdEndpointMode", writer -> { + writer.addDependency(TypeScriptDependency.NODE_CONFIG_PROVIDER); + writer.addImport("loadConfig", "loadNodeConfig", + TypeScriptDependency.NODE_CONFIG_PROVIDER); + writer.addDependency(AwsDependency.AWS_SDK_CORE); + // TODO: change to addImportSubmodule when available + writer.addImport("NODE_ACCOUNT_ID_ENDPOINT_MODE_CONFIG_OPTIONS", + "NODE_ACCOUNT_ID_ENDPOINT_MODE_CONFIG_OPTIONS", + "@aws-sdk/core/account-id-endpoint"); + writer.write( + "loadNodeConfig(NODE_ACCOUNT_ID_ENDPOINT_MODE_CONFIG_OPTIONS)"); + }); + break; + default: + LOGGER.warning("AccountIdEndpointMode config not supported for target: " + target); + break; + } + } + } + } + return runtimeConfigs; + } + } diff --git a/codegen/smithy-aws-typescript-codegen/src/main/java/software/amazon/smithy/aws/typescript/codegen/AddOmittedEndpointParams.java b/codegen/smithy-aws-typescript-codegen/src/main/java/software/amazon/smithy/aws/typescript/codegen/AddOmittedEndpointParams.java new file mode 100644 index 000000000000..108ce1320d2b --- /dev/null +++ b/codegen/smithy-aws-typescript-codegen/src/main/java/software/amazon/smithy/aws/typescript/codegen/AddOmittedEndpointParams.java @@ -0,0 +1,25 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + + package software.amazon.smithy.aws.typescript.codegen; + + import software.amazon.smithy.typescript.codegen.TypeScriptCodegenContext; + import software.amazon.smithy.typescript.codegen.endpointsV2.OmitEndpointParams; + import software.amazon.smithy.typescript.codegen.integration.TypeScriptIntegration; + import software.amazon.smithy.utils.SetUtils; + import software.amazon.smithy.utils.SmithyInternalApi; + + @SmithyInternalApi + public class AddOmittedEndpointParams implements TypeScriptIntegration { + + @Override + public void customize(TypeScriptCodegenContext codegenContext) { + setParamForOmission(); + } + + private void setParamForOmission() { + OmitEndpointParams.addOmittedParams(SetUtils.of("AccountId")); + } + } diff --git a/codegen/smithy-aws-typescript-codegen/src/main/resources/META-INF/services/software.amazon.smithy.typescript.codegen.integration.TypeScriptIntegration b/codegen/smithy-aws-typescript-codegen/src/main/resources/META-INF/services/software.amazon.smithy.typescript.codegen.integration.TypeScriptIntegration index 8a4c954bb6c2..3c9369d52ede 100644 --- a/codegen/smithy-aws-typescript-codegen/src/main/resources/META-INF/services/software.amazon.smithy.typescript.codegen.integration.TypeScriptIntegration +++ b/codegen/smithy-aws-typescript-codegen/src/main/resources/META-INF/services/software.amazon.smithy.typescript.codegen.integration.TypeScriptIntegration @@ -1,5 +1,6 @@ software.amazon.smithy.aws.typescript.codegen.AddEndpointsV2ParameterNameMap software.amazon.smithy.aws.typescript.codegen.AddAwsRuntimeConfig +software.amazon.smithy.aws.typescript.codegen.AddAccountIdEndpointModeRuntimeConfig software.amazon.smithy.aws.typescript.codegen.AddBuiltinPlugins software.amazon.smithy.aws.typescript.codegen.AddAwsAuthPlugin software.amazon.smithy.aws.typescript.codegen.AddTokenAuthPlugin diff --git a/packages/core/account-id-endpoint.js b/packages/core/account-id-endpoint.js new file mode 100644 index 000000000000..b2550f7ccd26 --- /dev/null +++ b/packages/core/account-id-endpoint.js @@ -0,0 +1,6 @@ + +/** + * Do not edit: + * This is a compatibility redirect for contexts that do not understand package.json exports field. + */ +module.exports = require("./dist-cjs/submodules/account-id-endpoint/index.js"); diff --git a/packages/core/package.json b/packages/core/package.json index 3c2e78dd337e..deb6f484fbce 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -46,6 +46,13 @@ "require": "./dist-cjs/submodules/httpAuthSchemes/index.js", "types": "./dist-types/submodules/httpAuthSchemes/index.d.ts" }, + "./account-id-endpoint": { + "module": "./dist-es/submodules/account-id-endpoint/index.js", + "node": "./dist-cjs/submodules/account-id-endpoint/index.js", + "import": "./dist-es/submodules/account-id-endpoint/index.js", + "require": "./dist-cjs/submodules/account-id-endpoint/index.js", + "types": "./dist-types/submodules/account-id-endpoint/index.d.ts" + }, "./protocols": { "module": "./dist-es/submodules/protocols/index.js", "node": "./dist-cjs/submodules/protocols/index.js", @@ -58,7 +65,8 @@ "dist-*/**", "./client.js", "./httpAuthSchemes.js", - "./protocols.js" + "./protocols.js", + "./account-id-endpoint.js" ], "sideEffects": false, "author": { @@ -73,7 +81,9 @@ "@smithy/smithy-client": "^3.1.10", "@smithy/types": "^3.3.0", "fast-xml-parser": "4.2.5", - "tslib": "^2.6.2" + "tslib": "^2.6.2", + "@smithy/node-config-provider": "^3.1.4", + "@smithy/util-middleware": "^3.0.3" }, "devDependencies": { "@tsconfig/recommended": "1.0.1", diff --git a/packages/core/src/submodules/account-id-endpoint/AccountIdEndpointModeConfigResolver.ts b/packages/core/src/submodules/account-id-endpoint/AccountIdEndpointModeConfigResolver.ts new file mode 100644 index 000000000000..5ede2063600c --- /dev/null +++ b/packages/core/src/submodules/account-id-endpoint/AccountIdEndpointModeConfigResolver.ts @@ -0,0 +1,56 @@ +import { Provider } from "@smithy/types"; +import { normalizeProvider } from "@smithy/util-middleware"; + +import { + AccountIdEndpointMode, + DEFAULT_ACCOUNT_ID_ENDPOINT_MODE, + validateAccountIdEndpointMode, +} from "./AccountIdEndpointModeConstants"; + +/** + * @public + */ +export interface AccountIdEndpointModeInputConfig { + /** + * The account ID endpoint mode to use. + */ + accountIdEndpointMode?: AccountIdEndpointMode | Provider; +} + +/** + * @internal + */ +interface PreviouslyResolved {} + +/** + * @internal + */ +export interface AccountIdEndpointModeResolvedConfig { + /** + * Resolved value for input config {config.accountIdEndpointMode} + */ + accountIdEndpointMode: Provider; +} + +/** + * @internal + */ +export const resolveAccountIdEndpointModeConfig = ( + input: T & AccountIdEndpointModeInputConfig & PreviouslyResolved +): T & AccountIdEndpointModeResolvedConfig => { + return { + ...input, + accountIdEndpointMode: async () => { + const accountIdEndpointModeProvider = normalizeProvider( + input.accountIdEndpointMode ?? DEFAULT_ACCOUNT_ID_ENDPOINT_MODE + ); + const accIdMode = await accountIdEndpointModeProvider(); + if (!validateAccountIdEndpointMode(accIdMode)) { + throw new Error( + `Invalid value for accountIdEndpointMode: ${accIdMode}. Valid values are: "required", "preferred", "disabled".` + ); + } + return accIdMode; + }, + }; +}; diff --git a/packages/core/src/submodules/account-id-endpoint/AccountIdEndpointModeConstants.spec.ts b/packages/core/src/submodules/account-id-endpoint/AccountIdEndpointModeConstants.spec.ts new file mode 100644 index 000000000000..68a0d826171e --- /dev/null +++ b/packages/core/src/submodules/account-id-endpoint/AccountIdEndpointModeConstants.spec.ts @@ -0,0 +1,43 @@ +import { validateAccountIdEndpointMode } from "./AccountIdEndpointModeConstants"; + +describe("validateAccountIdEndpointMode", () => { + it('should return true for "disabled"', () => { + const result = validateAccountIdEndpointMode("disabled"); + expect(result).toBe(true); + }); + + it('should return true for "preferred"', () => { + const result = validateAccountIdEndpointMode("preferred"); + expect(result).toBe(true); + }); + + it('should return true for "required"', () => { + const result = validateAccountIdEndpointMode("required"); + expect(result).toBe(true); + }); + + it("should return false for an invalid value", () => { + const result = validateAccountIdEndpointMode("invalidValue"); + expect(result).toBe(false); + }); + + it("should return false for an empty string", () => { + const result = validateAccountIdEndpointMode(""); + expect(result).toBe(false); + }); + + it("should return false for a number", () => { + const result = validateAccountIdEndpointMode(123); + expect(result).toBe(false); + }); + + it("should return false for null", () => { + const result = validateAccountIdEndpointMode(null); + expect(result).toBe(false); + }); + + it("should return false for undefined", () => { + const result = validateAccountIdEndpointMode(undefined); + expect(result).toBe(false); + }); +}); diff --git a/packages/core/src/submodules/account-id-endpoint/AccountIdEndpointModeConstants.ts b/packages/core/src/submodules/account-id-endpoint/AccountIdEndpointModeConstants.ts new file mode 100644 index 000000000000..6725bf492dee --- /dev/null +++ b/packages/core/src/submodules/account-id-endpoint/AccountIdEndpointModeConstants.ts @@ -0,0 +1,12 @@ +export type AccountIdEndpointMode = "disabled" | "preferred" | "required"; + +export const DEFAULT_ACCOUNT_ID_ENDPOINT_MODE = "preferred"; + +export const ACCOUNT_ID_ENDPOINT_MODE_VALUES: AccountIdEndpointMode[] = ["disabled", "preferred", "required"]; + +/** + * @internal + */ +export function validateAccountIdEndpointMode(value: any): value is AccountIdEndpointMode { + return ACCOUNT_ID_ENDPOINT_MODE_VALUES.includes(value); +} diff --git a/packages/core/src/submodules/account-id-endpoint/NodeAccountIdEndpointModeConfigOptions.spec.ts b/packages/core/src/submodules/account-id-endpoint/NodeAccountIdEndpointModeConfigOptions.spec.ts new file mode 100644 index 000000000000..2e70fffc2416 --- /dev/null +++ b/packages/core/src/submodules/account-id-endpoint/NodeAccountIdEndpointModeConfigOptions.spec.ts @@ -0,0 +1,68 @@ +import { DEFAULT_ACCOUNT_ID_ENDPOINT_MODE } from "./AccountIdEndpointModeConstants"; +import { + CONFIG_ACCOUNT_ID_ENDPOINT_MODE, + ENV_ACCOUNT_ID_ENDPOINT_MODE, + NODE_ACCOUNT_ID_ENDPOINT_MODE_CONFIG_OPTIONS, +} from "./NodeAccountIdEndpointModeConfigOptions"; + +describe("NODE_ACCOUNT_ID_ENDPOINT_MODE_CONFIG_OPTIONS", () => { + const originalEnv = process.env; + + beforeEach(() => { + jest.resetModules(); + process.env = { ...originalEnv }; + }); + + afterEach(() => { + process.env = originalEnv; + }); + + describe("environmentVariableSelector", () => { + it("should return the value set in environment variables", () => { + const testValue = "preferred"; + process.env[ENV_ACCOUNT_ID_ENDPOINT_MODE] = testValue; + const selector = NODE_ACCOUNT_ID_ENDPOINT_MODE_CONFIG_OPTIONS.environmentVariableSelector; + expect(selector(process.env)).toEqual(testValue); + }); + + it("should throw an error if the environment variable is set to an invalid value", () => { + process.env[ENV_ACCOUNT_ID_ENDPOINT_MODE] = "InvalidValue"; + const selector = NODE_ACCOUNT_ID_ENDPOINT_MODE_CONFIG_OPTIONS.environmentVariableSelector; + expect(() => selector(process.env)).toThrow("Invalid AccountIdEndpointMode value"); + }); + + it("should not throw an error if the environment variable is not set", () => { + delete process.env[ENV_ACCOUNT_ID_ENDPOINT_MODE]; + const selector = NODE_ACCOUNT_ID_ENDPOINT_MODE_CONFIG_OPTIONS.environmentVariableSelector; + expect(() => selector(process.env)).not.toThrow(); + }); + }); + + describe("configFileSelector", () => { + it("should return the value set in the configuration file", () => { + const testValue = "required"; + const profile = { [CONFIG_ACCOUNT_ID_ENDPOINT_MODE]: testValue }; + const selector = NODE_ACCOUNT_ID_ENDPOINT_MODE_CONFIG_OPTIONS.configFileSelector; + expect(selector(profile)).toEqual(testValue); + }); + + it("should throw an error if the configuration file contains an invalid value", () => { + const profile = { [CONFIG_ACCOUNT_ID_ENDPOINT_MODE]: "InvalidValue" }; + const selector = NODE_ACCOUNT_ID_ENDPOINT_MODE_CONFIG_OPTIONS.configFileSelector; + expect(() => selector(profile)).toThrow("Invalid AccountIdEndpointMode value"); + }); + + it("should not throw an error if the configuration file does not contain the setting", () => { + const profile = {}; + const selector = NODE_ACCOUNT_ID_ENDPOINT_MODE_CONFIG_OPTIONS.configFileSelector; + expect(() => selector(profile)).not.toThrow(); + }); + }); + + describe("default", () => { + it("should return the default value", () => { + const defaultValue = NODE_ACCOUNT_ID_ENDPOINT_MODE_CONFIG_OPTIONS.default; + expect(defaultValue).toEqual(DEFAULT_ACCOUNT_ID_ENDPOINT_MODE); + }); + }); +}); diff --git a/packages/core/src/submodules/account-id-endpoint/NodeAccountIdEndpointModeConfigOptions.ts b/packages/core/src/submodules/account-id-endpoint/NodeAccountIdEndpointModeConfigOptions.ts new file mode 100644 index 000000000000..c0b7c14d76fd --- /dev/null +++ b/packages/core/src/submodules/account-id-endpoint/NodeAccountIdEndpointModeConfigOptions.ts @@ -0,0 +1,44 @@ +import { LoadedConfigSelectors } from "@smithy/node-config-provider"; + +import { + AccountIdEndpointMode, + DEFAULT_ACCOUNT_ID_ENDPOINT_MODE, + validateAccountIdEndpointMode, +} from "./AccountIdEndpointModeConstants"; + +const err = "Invalid AccountIdEndpointMode value"; + +const _throw = (message: string): never => { + throw new Error(message); +}; + +/** + * @internal + */ +export const ENV_ACCOUNT_ID_ENDPOINT_MODE = "AWS_ACCOUNT_ID_ENDPOINT_MODE"; + +/** + * @internal + */ +export const CONFIG_ACCOUNT_ID_ENDPOINT_MODE = "account_id_endpoint_mode"; + +/** + * @internal + */ +export const NODE_ACCOUNT_ID_ENDPOINT_MODE_CONFIG_OPTIONS: LoadedConfigSelectors = { + environmentVariableSelector: (env) => { + const value = env[ENV_ACCOUNT_ID_ENDPOINT_MODE]; + if (value && !validateAccountIdEndpointMode(value)) { + _throw(err); + } + return value as AccountIdEndpointMode; + }, + configFileSelector: (profile) => { + const value = profile[CONFIG_ACCOUNT_ID_ENDPOINT_MODE]; + if (value && !validateAccountIdEndpointMode(value)) { + _throw(err); + } + return value as AccountIdEndpointMode; + }, + default: DEFAULT_ACCOUNT_ID_ENDPOINT_MODE, +}; diff --git a/packages/core/src/submodules/account-id-endpoint/README.md b/packages/core/src/submodules/account-id-endpoint/README.md new file mode 100644 index 000000000000..48257841a80d --- /dev/null +++ b/packages/core/src/submodules/account-id-endpoint/README.md @@ -0,0 +1,9 @@ +# @aws-sdk/core/account-id-endpoint + +> An internal package + +This submodule provides functionality for AccountId based endpoint routing. + +## Usage + +You probably shouldn't, at least directly. diff --git a/packages/core/src/submodules/account-id-endpoint/index.ts b/packages/core/src/submodules/account-id-endpoint/index.ts new file mode 100644 index 000000000000..52af11df31b6 --- /dev/null +++ b/packages/core/src/submodules/account-id-endpoint/index.ts @@ -0,0 +1,3 @@ +export * from "./AccountIdEndpointModeConfigResolver"; +export * from "./AccountIdEndpointModeConstants"; +export * from "./NodeAccountIdEndpointModeConfigOptions"; diff --git a/packages/core/tsconfig.cjs.json b/packages/core/tsconfig.cjs.json index 2c52f5b4acc6..0bf5a311e706 100644 --- a/packages/core/tsconfig.cjs.json +++ b/packages/core/tsconfig.cjs.json @@ -6,7 +6,8 @@ "paths": { "@aws-sdk/core/client": ["./src/submodules/client/index.ts"], "@aws-sdk/core/httpAuthSchemes": ["./src/submodules/httpAuthSchemes/index.ts"], - "@aws-sdk/core/protocols": ["./src/submodules/protocols/index.ts"] + "@aws-sdk/core/protocols": ["./src/submodules/protocols/index.ts"], + "@aws-sdk/core/account-id-endpoint": ["./src/submodules/account-id-endpoint/index.ts"] } }, "extends": "../../tsconfig.cjs.json", diff --git a/packages/core/tsconfig.es.json b/packages/core/tsconfig.es.json index ddd0ab53bf6d..6d9cfcbcd7ff 100644 --- a/packages/core/tsconfig.es.json +++ b/packages/core/tsconfig.es.json @@ -6,7 +6,8 @@ "paths": { "@aws-sdk/core/client": ["./src/submodules/client/index.ts"], "@aws-sdk/core/httpAuthSchemes": ["./src/submodules/httpAuthSchemes/index.ts"], - "@aws-sdk/core/protocols": ["./src/submodules/protocols/index.ts"] + "@aws-sdk/core/protocols": ["./src/submodules/protocols/index.ts"], + "@aws-sdk/core/account-id-endpoint": ["./src/submodules/account-id-endpoint/index.ts"] } }, "extends": "../../tsconfig.es.json", diff --git a/packages/core/tsconfig.types.json b/packages/core/tsconfig.types.json index 2cacd51b596c..b38bac4f6eb6 100644 --- a/packages/core/tsconfig.types.json +++ b/packages/core/tsconfig.types.json @@ -6,7 +6,8 @@ "paths": { "@aws-sdk/core/client": ["./src/submodules/client/index.ts"], "@aws-sdk/core/httpAuthSchemes": ["./src/submodules/httpAuthSchemes/index.ts"], - "@aws-sdk/core/protocols": ["./src/submodules/protocols/index.ts"] + "@aws-sdk/core/protocols": ["./src/submodules/protocols/index.ts"], + "@aws-sdk/core/account-id-endpoint": ["./src/submodules/account-id-endpoint/index.ts"] } }, "extends": "../../tsconfig.types.json",