Skip to content

Commit

Permalink
feat(core): add AccountIdEndpointMode config option (#6036)
Browse files Browse the repository at this point in the history
* feat(core): add AccountIdEndpointMode config

* chore(core): add deps for @aws-sdk/core package.json

* chore(codegen): minor refactor and imports addition

* chore(core): refactor accountId for submodules

* chore(core): refactor update

* chore(core): rm blankspace

* chore(core): package.json update

* fix(codegen): minor fixes for accIdEpMode code generator

* chore(core): accountId module export field

* chore(core): add accountId dir entry point

* chore(codegen): checkstyle fixes

* chore(codegen): checkstyle fixes

* chore(core): tsconfig files updates

* chore: checkstyle fix

* chore(core): add core deps

* chore(codegen): add TypeScript Integration for codegen

* chore(codegen): add ad-hoc import for account-id-endpoints in core

* chore(codegen): ci fix
  • Loading branch information
siddsriv authored Jul 24, 2024
1 parent a673c81 commit 1cb4edc
Show file tree
Hide file tree
Showing 14 changed files with 369 additions and 5 deletions.
Original file line number Diff line number Diff line change
@@ -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> 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<AccountIdEndpointMode>;\n");
}
}
}
}

@Override
public Map<String, Consumer<TypeScriptWriter>> getRuntimeConfigWriters(
TypeScriptSettings settings,
Model model,
SymbolProvider symbolProvider,
LanguageTarget target
) {
ServiceShape service = settings.getService(model);
Map<String, Consumer<TypeScriptWriter>> runtimeConfigs = new HashMap<>();
if (isAwsService(settings, model) || isSigV4Service(settings, model)) {
Optional<EndpointRuleSetTrait> 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;
}
}
Original file line number Diff line number Diff line change
@@ -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
Expand Down
6 changes: 6 additions & 0 deletions packages/core/account-id-endpoint.js
Original file line number Diff line number Diff line change
@@ -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");
14 changes: 12 additions & 2 deletions packages/core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -58,7 +65,8 @@
"dist-*/**",
"./client.js",
"./httpAuthSchemes.js",
"./protocols.js"
"./protocols.js",
"./account-id-endpoint.js"
],
"sideEffects": false,
"author": {
Expand All @@ -73,7 +81,9 @@
"@smithy/smithy-client": "^3.1.8",
"@smithy/types": "^3.3.0",
"fast-xml-parser": "4.2.5",
"tslib": "^2.6.2"
"tslib": "^2.6.2",
"@smithy/node-config-provider": "^3.0.0",
"@smithy/util-middleware": "^3.0.0"
},
"devDependencies": {
"@tsconfig/recommended": "1.0.1",
Expand Down
Original file line number Diff line number Diff line change
@@ -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<AccountIdEndpointMode>;
}

/**
* @internal
*/
interface PreviouslyResolved {}

/**
* @internal
*/
export interface AccountIdEndpointModeResolvedConfig {
/**
* Resolved value for input config {config.accountIdEndpointMode}
*/
accountIdEndpointMode: Provider<AccountIdEndpointMode>;
}

/**
* @internal
*/
export const resolveAccountIdEndpointModeConfig = <T>(
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;
},
};
};
Original file line number Diff line number Diff line change
@@ -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);
});
});
Original file line number Diff line number Diff line change
@@ -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);
}
Original file line number Diff line number Diff line change
@@ -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);
});
});
});
Original file line number Diff line number Diff line change
@@ -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<AccountIdEndpointMode> = {
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,
};
9 changes: 9 additions & 0 deletions packages/core/src/submodules/account-id-endpoint/README.md
Original file line number Diff line number Diff line change
@@ -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.
Loading

0 comments on commit 1cb4edc

Please sign in to comment.