From 7c6a7901f222273f7e82f169eaaf23b08c803596 Mon Sep 17 00:00:00 2001 From: Trivikram Kamat <16024985+trivikr@users.noreply.github.com> Date: Tue, 24 Aug 2021 11:23:59 -0700 Subject: [PATCH] feat(config-resolver): add getRegionInfo helper functions (#2701) --- packages/config-resolver/src/index.ts | 1 + .../regionInfo/getHostnameTemplate.spec.ts | 45 ++++ .../src/regionInfo/getHostnameTemplate.ts | 14 ++ .../src/regionInfo/getRegionInfo.spec.ts | 203 ++++++++++++++++++ .../src/regionInfo/getRegionInfo.ts | 27 +++ .../regionInfo/getResolvedHostname.spec.ts | 61 ++++++ .../src/regionInfo/getResolvedHostname.ts | 21 ++ .../regionInfo/getResolvedPartition.spec.ts | 31 +++ .../src/regionInfo/getResolvedPartition.ts | 13 ++ .../config-resolver/src/regionInfo/index.ts | 1 + 10 files changed, 417 insertions(+) create mode 100644 packages/config-resolver/src/regionInfo/getHostnameTemplate.spec.ts create mode 100644 packages/config-resolver/src/regionInfo/getHostnameTemplate.ts create mode 100644 packages/config-resolver/src/regionInfo/getRegionInfo.spec.ts create mode 100644 packages/config-resolver/src/regionInfo/getRegionInfo.ts create mode 100644 packages/config-resolver/src/regionInfo/getResolvedHostname.spec.ts create mode 100644 packages/config-resolver/src/regionInfo/getResolvedHostname.ts create mode 100644 packages/config-resolver/src/regionInfo/getResolvedPartition.spec.ts create mode 100644 packages/config-resolver/src/regionInfo/getResolvedPartition.ts create mode 100644 packages/config-resolver/src/regionInfo/index.ts diff --git a/packages/config-resolver/src/index.ts b/packages/config-resolver/src/index.ts index 47d04a39807c..16c4a7b91f37 100644 --- a/packages/config-resolver/src/index.ts +++ b/packages/config-resolver/src/index.ts @@ -1,3 +1,4 @@ export * from "./CustomEndpointsConfig"; export * from "./EndpointsConfig"; export * from "./RegionConfig"; +export * from "./regionInfo"; diff --git a/packages/config-resolver/src/regionInfo/getHostnameTemplate.spec.ts b/packages/config-resolver/src/regionInfo/getHostnameTemplate.spec.ts new file mode 100644 index 000000000000..75933759a7c4 --- /dev/null +++ b/packages/config-resolver/src/regionInfo/getHostnameTemplate.spec.ts @@ -0,0 +1,45 @@ +import { getHostnameTemplate } from "./getHostnameTemplate"; +import { getResolvedPartition, PartitionHash } from "./getResolvedPartition"; + +jest.mock("./getResolvedPartition"); + +const AWS_TEMPLATE = "{signingService}.{region}.amazonaws.com"; + +describe(getHostnameTemplate.name, () => { + const mockRegion = "mockRegion"; + const mockPartition = "mockPartition"; + const mockHostname = "{region}.mockHostname.com"; + const mockSigningService = "mockSigningService"; + + beforeEach(() => { + (getResolvedPartition as jest.Mock).mockReturnValue(mockPartition); + }); + + afterEach(() => { + expect(getResolvedPartition).toHaveBeenCalledTimes(1); + jest.clearAllMocks(); + }); + + it("returns hostname template if present in partitionHash", () => { + const mockPartitionHash: PartitionHash = { + [mockPartition]: { + regions: [mockRegion, `${mockRegion}2`, `${mockRegion}3`], + hostname: mockHostname, + }, + }; + + expect( + getHostnameTemplate(mockRegion, { signingService: mockSigningService, partitionHash: mockPartitionHash }) + ).toEqual(mockHostname); + expect(getResolvedPartition).toHaveBeenCalledWith(mockRegion, { partitionHash: mockPartitionHash }); + }); + + it("returns default hostname template if not present in partitionHash", () => { + const mockPartitionHash: PartitionHash = {}; + + expect( + getHostnameTemplate(mockRegion, { signingService: mockSigningService, partitionHash: mockPartitionHash }) + ).toEqual(AWS_TEMPLATE.replace("{signingService}", mockSigningService)); + expect(getResolvedPartition).toHaveBeenCalledWith(mockRegion, { partitionHash: mockPartitionHash }); + }); +}); diff --git a/packages/config-resolver/src/regionInfo/getHostnameTemplate.ts b/packages/config-resolver/src/regionInfo/getHostnameTemplate.ts new file mode 100644 index 000000000000..c7d7f2d14ab8 --- /dev/null +++ b/packages/config-resolver/src/regionInfo/getHostnameTemplate.ts @@ -0,0 +1,14 @@ +import { getResolvedPartition, GetResolvedPartitionOptions } from "./getResolvedPartition"; + +const AWS_TEMPLATE = "{signingService}.{region}.amazonaws.com"; + +export interface GetHostnameTemplateOptions extends GetResolvedPartitionOptions { + /** + * The name to be used while signing the service request. + */ + signingService: string; +} + +export const getHostnameTemplate = (region: string, { signingService, partitionHash }: GetHostnameTemplateOptions) => + partitionHash[getResolvedPartition(region, { partitionHash })]?.hostname ?? + AWS_TEMPLATE.replace("{signingService}", signingService); diff --git a/packages/config-resolver/src/regionInfo/getRegionInfo.spec.ts b/packages/config-resolver/src/regionInfo/getRegionInfo.spec.ts new file mode 100644 index 000000000000..a3f8cb2792c3 --- /dev/null +++ b/packages/config-resolver/src/regionInfo/getRegionInfo.spec.ts @@ -0,0 +1,203 @@ +import { getRegionInfo } from "./getRegionInfo"; +import { getResolvedHostname, RegionHash } from "./getResolvedHostname"; +import { getResolvedPartition, PartitionHash } from "./getResolvedPartition"; + +jest.mock("./getResolvedHostname"); +jest.mock("./getResolvedPartition"); + +describe(getRegionInfo.name, () => { + const mockPartition = "mockPartition"; + const mockSigningService = "mockSigningService"; + + const mockRegion = "mockRegion"; + const mockHostname = "{region}.mockHostname.com"; + const mockEndpointRegion = "mockEndpointRegion"; + const mockEndpointHostname = "{region}.mockEndpointHostname.com"; + + enum RegionCase { + REGION = "Region", + ENDPOINT = "Endpoint", + REGION_AND_ENDPOINT = "Region and Endpoint", + } + + const getMockRegionHash = (regionCase: RegionCase): RegionHash => ({ + ...((regionCase === RegionCase.REGION || regionCase === RegionCase.REGION_AND_ENDPOINT) && { + [mockRegion]: { + hostname: mockHostname, + }, + }), + ...((regionCase === RegionCase.ENDPOINT || regionCase === RegionCase.REGION_AND_ENDPOINT) && { + [mockEndpointRegion]: { + hostname: mockEndpointHostname, + }, + }), + }); + + const getMockPartitionHash = (regionCase: RegionCase): PartitionHash => ({ + [mockPartition]: { + regions: [mockRegion, `${mockRegion}2`, `${mockRegion}3`], + ...((regionCase === RegionCase.REGION || regionCase === RegionCase.REGION_AND_ENDPOINT) && { + hostname: mockHostname, + }), + ...((regionCase === RegionCase.ENDPOINT || regionCase === RegionCase.REGION_AND_ENDPOINT) && { + endpoint: mockEndpointRegion, + }), + }, + }); + + const getMockResolvedRegion = (regionCase: RegionCase): string => + regionCase === RegionCase.REGION ? mockRegion : mockEndpointRegion; + + const getMockResolvedPartitionOptions = (partitionHash) => ({ partitionHash }); + + const getMockResolvedHostnameOptions = (regionHash, getResolvedPartitionOptions) => ({ + ...getResolvedPartitionOptions, + signingService: mockSigningService, + regionHash, + }); + + beforeEach(() => { + (getResolvedHostname as jest.Mock).mockReturnValue(mockHostname); + (getResolvedPartition as jest.Mock).mockReturnValue(mockPartition); + }); + + afterEach(() => { + expect(getResolvedHostname).toHaveBeenCalledTimes(1); + expect(getResolvedPartition).toHaveBeenCalledTimes(1); + jest.clearAllMocks(); + }); + + describe("returns data based on options passed", () => { + it.each(Object.values(RegionCase))("%s", (regionCase) => { + const mockRegionHash = getMockRegionHash(regionCase); + const mockPartitionHash = getMockPartitionHash(regionCase); + + const mockGetResolvedPartitionOptions = getMockResolvedPartitionOptions(mockPartitionHash); + const mockGetResolvedHostnameOptions = getMockResolvedHostnameOptions( + mockRegionHash, + mockGetResolvedPartitionOptions + ); + + expect(getRegionInfo(mockRegion, mockGetResolvedHostnameOptions)).toEqual({ + signingService: mockSigningService, + hostname: mockHostname, + partition: mockPartition, + }); + + expect(getResolvedHostname).toHaveBeenCalledWith( + getMockResolvedRegion(regionCase), + mockGetResolvedHostnameOptions + ); + expect(getResolvedPartition).toHaveBeenCalledWith(mockRegion, mockGetResolvedPartitionOptions); + }); + }); + + describe("returns signingRegion if present in regionHash", () => { + const getMockRegionHashWithSigningRegion = ( + regionCase: RegionCase, + mockRegionHash: RegionHash, + mockSigningRegion: string + ): RegionHash => ({ + ...mockRegionHash, + ...((regionCase === RegionCase.REGION || regionCase === RegionCase.REGION_AND_ENDPOINT) && { + [mockRegion]: { + ...mockRegionHash[mockRegion], + signingRegion: mockSigningRegion, + }, + }), + ...((regionCase === RegionCase.ENDPOINT || regionCase === RegionCase.REGION_AND_ENDPOINT) && { + [mockEndpointRegion]: { + ...mockRegionHash[mockEndpointRegion], + signingRegion: mockSigningRegion, + }, + }), + }); + + it.each(Object.values(RegionCase))("%s", (regionCase) => { + const mockSigningRegion = "mockSigningRegion"; + const mockRegionHash = getMockRegionHash(regionCase); + const mockPartitionHash = getMockPartitionHash(regionCase); + + const mockGetResolvedPartitionOptions = getMockResolvedPartitionOptions(mockPartitionHash); + const mockGetResolvedHostnameOptions = getMockResolvedHostnameOptions( + mockRegionHash, + mockGetResolvedPartitionOptions + ); + + const mockRegionHashWithSigningRegion = getMockRegionHashWithSigningRegion( + regionCase, + mockRegionHash, + mockSigningRegion + ); + + expect( + getRegionInfo(mockRegion, { ...mockGetResolvedHostnameOptions, regionHash: mockRegionHashWithSigningRegion }) + ).toEqual({ + signingService: mockSigningService, + hostname: mockHostname, + partition: mockPartition, + signingRegion: mockSigningRegion, + }); + + expect(getResolvedHostname).toHaveBeenCalledWith(getMockResolvedRegion(regionCase), { + ...mockGetResolvedHostnameOptions, + regionHash: mockRegionHashWithSigningRegion, + }); + expect(getResolvedPartition).toHaveBeenCalledWith(mockRegion, mockGetResolvedPartitionOptions); + }); + }); + + describe("returns signingService if present in regionHash", () => { + const getMockRegionHashWithSigningService = ( + regionCase: RegionCase, + mockRegionHash: RegionHash, + mockSigningService: string + ): RegionHash => ({ + ...mockRegionHash, + ...((regionCase === RegionCase.REGION || regionCase === RegionCase.REGION_AND_ENDPOINT) && { + [mockRegion]: { + ...mockRegionHash[mockRegion], + signingService: mockSigningService, + }, + }), + ...((regionCase === RegionCase.ENDPOINT || regionCase === RegionCase.REGION_AND_ENDPOINT) && { + [mockEndpointRegion]: { + ...mockRegionHash[mockEndpointRegion], + signingService: mockSigningService, + }, + }), + }); + + it.each(Object.values(RegionCase))("%s", (regionCase) => { + const mockSigningServiceInRegionHash = "mockSigningServiceInRegionHash"; + const mockRegionHash = getMockRegionHash(regionCase); + const mockPartitionHash = getMockPartitionHash(regionCase); + + const mockGetResolvedPartitionOptions = getMockResolvedPartitionOptions(mockPartitionHash); + const mockGetResolvedHostnameOptions = getMockResolvedHostnameOptions( + mockRegionHash, + mockGetResolvedPartitionOptions + ); + + const mockRegionHashWithSigningRegion = getMockRegionHashWithSigningService( + regionCase, + mockRegionHash, + mockSigningServiceInRegionHash + ); + + expect( + getRegionInfo(mockRegion, { ...mockGetResolvedHostnameOptions, regionHash: mockRegionHashWithSigningRegion }) + ).toEqual({ + signingService: mockSigningServiceInRegionHash, + hostname: mockHostname, + partition: mockPartition, + }); + + expect(getResolvedHostname).toHaveBeenCalledWith(getMockResolvedRegion(regionCase), { + ...mockGetResolvedHostnameOptions, + regionHash: mockRegionHashWithSigningRegion, + }); + expect(getResolvedPartition).toHaveBeenCalledWith(mockRegion, mockGetResolvedPartitionOptions); + }); + }); +}); diff --git a/packages/config-resolver/src/regionInfo/getRegionInfo.ts b/packages/config-resolver/src/regionInfo/getRegionInfo.ts new file mode 100644 index 000000000000..ed6ff958f3d7 --- /dev/null +++ b/packages/config-resolver/src/regionInfo/getRegionInfo.ts @@ -0,0 +1,27 @@ +import { RegionInfo } from "@aws-sdk/types"; + +import { getResolvedHostname, GetResolvedHostnameOptions, RegionHash } from "./getResolvedHostname"; +import { getResolvedPartition, PartitionHash } from "./getResolvedPartition"; + +export { RegionHash, PartitionHash }; + +export interface GetRegionInfoOptions extends GetResolvedHostnameOptions {} + +export const getRegionInfo = ( + region: string, + { signingService, regionHash, partitionHash }: GetRegionInfoOptions +): RegionInfo => { + const partition = getResolvedPartition(region, { partitionHash }); + const resolvedRegion = partitionHash[partition]?.endpoint ?? region; + return { + partition, + signingService, + hostname: getResolvedHostname(resolvedRegion, { signingService, regionHash, partitionHash }), + ...(regionHash[resolvedRegion]?.signingRegion && { + signingRegion: regionHash[resolvedRegion].signingRegion, + }), + ...(regionHash[resolvedRegion]?.signingService && { + signingService: regionHash[resolvedRegion].signingService, + }), + }; +}; diff --git a/packages/config-resolver/src/regionInfo/getResolvedHostname.spec.ts b/packages/config-resolver/src/regionInfo/getResolvedHostname.spec.ts new file mode 100644 index 000000000000..9a633a40bcb3 --- /dev/null +++ b/packages/config-resolver/src/regionInfo/getResolvedHostname.spec.ts @@ -0,0 +1,61 @@ +import { getHostnameTemplate } from "./getHostnameTemplate"; +import { getResolvedHostname, RegionHash } from "./getResolvedHostname"; +import { PartitionHash } from "./getResolvedPartition"; + +jest.mock("./getHostnameTemplate"); + +describe(getResolvedHostname.name, () => { + const mockSigningService = "mockSigningService"; + const mockRegion = "mockRegion"; + const mockPartition = "mockPartition"; + const mockHostname = "{region}.mockHostname.com"; + + afterEach(() => { + jest.clearAllMocks(); + }); + + it("returns hostname if available in regionHash", () => { + const mockRegionHash: RegionHash = { + [mockRegion]: { + hostname: mockHostname, + }, + }; + const mockPartitionHash: PartitionHash = {}; + + expect( + getResolvedHostname(mockRegion, { + signingService: mockSigningService, + regionHash: mockRegionHash, + partitionHash: mockPartitionHash, + }) + ).toBe(mockHostname); + expect(getHostnameTemplate).not.toHaveBeenCalled(); + }); + + it("returns hostname from hostname template when not available in regionHash", () => { + const mockRegionHash: RegionHash = {}; + + (getHostnameTemplate as jest.Mock).mockReturnValue(mockHostname); + + const mockPartitionHash: PartitionHash = { + [mockPartition]: { + regions: [mockRegion, `${mockRegion}2`, `${mockRegion}3`], + hostname: mockHostname, + }, + }; + + expect( + getResolvedHostname(mockRegion, { + signingService: mockSigningService, + regionHash: mockRegionHash, + partitionHash: mockPartitionHash, + }) + ).toBe(mockHostname.replace("{region}", mockRegion)); + + expect(getHostnameTemplate).toHaveBeenCalledTimes(1); + expect(getHostnameTemplate).toHaveBeenCalledWith(mockRegion, { + signingService: mockSigningService, + partitionHash: mockPartitionHash, + }); + }); +}); diff --git a/packages/config-resolver/src/regionInfo/getResolvedHostname.ts b/packages/config-resolver/src/regionInfo/getResolvedHostname.ts new file mode 100644 index 000000000000..581e77a8d250 --- /dev/null +++ b/packages/config-resolver/src/regionInfo/getResolvedHostname.ts @@ -0,0 +1,21 @@ +import { RegionInfo } from "@aws-sdk/types"; + +import { getHostnameTemplate, GetHostnameTemplateOptions } from "./getHostnameTemplate"; +import { GetResolvedPartitionOptions } from "./getResolvedPartition"; + +export type RegionHash = { [key: string]: Partial> }; + +export interface GetResolvedHostnameOptions extends GetHostnameTemplateOptions, GetResolvedPartitionOptions { + /** + * The hash of region with the information specific to that region. + * The information can include hostname, signingService and signingRegion. + */ + regionHash: RegionHash; +} + +export const getResolvedHostname = ( + region: string, + { signingService, regionHash, partitionHash }: GetResolvedHostnameOptions +) => + regionHash[region]?.hostname ?? + getHostnameTemplate(region, { signingService, partitionHash }).replace("{region}", region); diff --git a/packages/config-resolver/src/regionInfo/getResolvedPartition.spec.ts b/packages/config-resolver/src/regionInfo/getResolvedPartition.spec.ts new file mode 100644 index 000000000000..8b15ce571030 --- /dev/null +++ b/packages/config-resolver/src/regionInfo/getResolvedPartition.spec.ts @@ -0,0 +1,31 @@ +import { getResolvedPartition, PartitionHash } from "./getResolvedPartition"; + +describe(getResolvedPartition.name, () => { + const mockRegion = "mockRegion"; + const mockPartition = "mockPartition"; + const mockHostname = "mockHostname"; + + it("returns the partition if region is present in partitionHash", () => { + const mockPartitionHash: PartitionHash = { + [mockPartition]: { + regions: [mockRegion, `${mockRegion}2`, `${mockRegion}3`], + hostname: mockHostname, + }, + }; + expect(getResolvedPartition(mockRegion, { partitionHash: mockPartitionHash })).toBe(mockPartition); + }); + + it("returns aws if region is not present in any partition", () => { + const mockPartitionHash: PartitionHash = { + [`${mockPartition}2`]: { + regions: [`${mockRegion}2`, `${mockRegion}3`], + hostname: mockHostname, + }, + }; + expect(getResolvedPartition(mockRegion, { partitionHash: mockPartitionHash })).toBe("aws"); + }); + + it("returns aws if partitionHash is empty", () => { + expect(getResolvedPartition(mockRegion, { partitionHash: undefined })).toBe("aws"); + }); +}); diff --git a/packages/config-resolver/src/regionInfo/getResolvedPartition.ts b/packages/config-resolver/src/regionInfo/getResolvedPartition.ts new file mode 100644 index 000000000000..11a92466983e --- /dev/null +++ b/packages/config-resolver/src/regionInfo/getResolvedPartition.ts @@ -0,0 +1,13 @@ +export type PartitionHash = { [key: string]: { regions: string[]; hostname?: string; endpoint?: string } }; + +export interface GetResolvedPartitionOptions { + /** + * The hash of partition with the information specific to that partition. + * The information includes the list of regions belonging to that partition, + * and the hostname to be used for the partition. + */ + partitionHash: PartitionHash; +} + +export const getResolvedPartition = (region: string, { partitionHash }: GetResolvedPartitionOptions) => + Object.keys(partitionHash || {}).find((key) => partitionHash[key].regions.includes(region)) ?? "aws"; diff --git a/packages/config-resolver/src/regionInfo/index.ts b/packages/config-resolver/src/regionInfo/index.ts new file mode 100644 index 000000000000..3884c2d71332 --- /dev/null +++ b/packages/config-resolver/src/regionInfo/index.ts @@ -0,0 +1 @@ +export * from "./getRegionInfo";