diff --git a/packages/@aws-cdk/assertions/lib/private/conditions.ts b/packages/@aws-cdk/assertions/lib/private/conditions.ts index e7c4665dee219..6ed10379dea9e 100644 --- a/packages/@aws-cdk/assertions/lib/private/conditions.ts +++ b/packages/@aws-cdk/assertions/lib/private/conditions.ts @@ -2,7 +2,7 @@ import { filterLogicalId, formatFailure, matchSection } from './section'; import { Template } from './template'; export function findConditions(template: Template, logicalId: string, props: any = {}): { [key: string]: { [key: string]: any } } { - const section: { [key: string] : {} } = template.Conditions; + const section: { [key: string] : {} } = template.Conditions ?? {}; const result = matchSection(filterLogicalId(section, logicalId), props); if (!result.match) { @@ -13,7 +13,7 @@ export function findConditions(template: Template, logicalId: string, props: any } export function hasCondition(template: Template, logicalId: string, props: any): string | void { - const section: { [key: string] : {} } = template.Conditions; + const section: { [key: string] : {} } = template.Conditions ?? {}; const result = matchSection(filterLogicalId(section, logicalId), props); if (result.match) { return; diff --git a/packages/@aws-cdk/assertions/lib/private/cyclic.ts b/packages/@aws-cdk/assertions/lib/private/cyclic.ts new file mode 100644 index 0000000000000..07451a126ad28 --- /dev/null +++ b/packages/@aws-cdk/assertions/lib/private/cyclic.ts @@ -0,0 +1,175 @@ +import { Resource, Template } from './template'; + +/** + * Check a template for cyclic dependencies + * + * This will make sure that we don't happily validate templates + * in unit tests that wouldn't deploy to CloudFormation anyway. + */ +export function checkTemplateForCyclicDependencies(template: Template): void { + const logicalIds = new Set(Object.keys(template.Resources ?? {})); + + const dependencies = new Map>(); + for (const [logicalId, resource] of Object.entries(template.Resources ?? {})) { + dependencies.set(logicalId, intersect(findResourceDependencies(resource), logicalIds)); + } + + // We will now progressively remove entries from the map of 'dependencies' that have + // 0 elements in them. If we can't do that anymore and the map isn't empty, we + // have a cyclic dependency. + while (dependencies.size > 0) { + const free = Array.from(dependencies.entries()).filter(([_, deps]) => deps.size === 0); + if (free.length === 0) { + // Oops! + const cycle = findCycle(dependencies); + + const cycleResources: any = {}; + for (const logicalId of cycle) { + cycleResources[logicalId] = template.Resources?.[logicalId]; + } + + throw new Error(`Template is undeployable, these resources have a dependency cycle: ${cycle.join(' -> ')}:\n\n${JSON.stringify(cycleResources, undefined, 2)}`); + } + + for (const [logicalId, _] of free) { + for (const deps of dependencies.values()) { + deps.delete(logicalId); + } + dependencies.delete(logicalId); + } + } +} + +function findResourceDependencies(res: Resource): Set { + return new Set([ + ...toArray(res.DependsOn ?? []), + ...findExpressionDependencies(res.Properties), + ]); +} + +function toArray(x: A | A[]): A[] { + return Array.isArray(x) ? x : [x]; +} + +function findExpressionDependencies(obj: any): Set { + const ret = new Set(); + recurse(obj); + return ret; + + function recurse(x: any): void { + if (!x) { return; } + if (Array.isArray(x)) { + x.forEach(recurse); + } + if (typeof x === 'object') { + const keys = Object.keys(x); + if (keys.length === 1 && keys[0] === 'Ref') { + ret.add(x[keys[0]]); + } else if (keys.length === 1 && keys[0] === 'Fn::GetAtt') { + ret.add(x[keys[0]][0]); + } else if (keys.length === 1 && keys[0] === 'Fn::Sub') { + const argument = x[keys[0]]; + const pattern = Array.isArray(argument) ? argument[0] : argument; + for (const logId of logicalIdsInSubString(pattern)) { + ret.add(logId); + } + const contextDict = Array.isArray(argument) ? argument[1] : undefined; + if (contextDict) { + Object.values(contextDict).forEach(recurse); + } + } else { + Object.values(x).forEach(recurse); + } + } + } +} + +/** + * Return the logical IDs found in a {Fn::Sub} format string + */ +function logicalIdsInSubString(x: string): string[] { + return analyzeSubPattern(x).flatMap((fragment) => { + switch (fragment.type) { + case 'getatt': + case 'ref': + return [fragment.logicalId]; + case 'literal': + return []; + } + }); +} + + +function analyzeSubPattern(pattern: string): SubFragment[] { + const ret: SubFragment[] = []; + let start = 0; + + let ph0 = pattern.indexOf('${', start); + while (ph0 > -1) { + if (pattern[ph0 + 2] === '!') { + // "${!" means "don't actually substitute" + start = ph0 + 3; + ph0 = pattern.indexOf('${', start); + continue; + } + + const ph1 = pattern.indexOf('}', ph0 + 2); + if (ph1 === -1) { + break; + } + const placeholder = pattern.substring(ph0 + 2, ph1); + + if (ph0 > start) { + ret.push({ type: 'literal', content: pattern.substring(start, ph0) }); + } + if (placeholder.includes('.')) { + const [logicalId, attr] = placeholder.split('.'); + ret.push({ type: 'getatt', logicalId: logicalId!, attr: attr! }); + } else { + ret.push({ type: 'ref', logicalId: placeholder }); + } + + start = ph1 + 1; + ph0 = pattern.indexOf('${', start); + } + + if (start < pattern.length - 1) { + ret.push({ type: 'literal', content: pattern.substr(start) }); + } + + return ret; +} + +type SubFragment = + | { readonly type: 'literal'; readonly content: string } + | { readonly type: 'ref'; readonly logicalId: string } + | { readonly type: 'getatt'; readonly logicalId: string; readonly attr: string }; + + +function intersect(xs: Set, ys: Set): Set { + return new Set(Array.from(xs).filter(x => ys.has(x))); +} + +/** + * Find cycles in a graph + * + * Not the fastest, but effective and should be rare + */ +function findCycle(deps: ReadonlyMap>): string[] { + for (const node of deps.keys()) { + const cycle = recurse(node, [node]); + if (cycle) { return cycle; } + } + throw new Error('No cycle found. Assertion failure!'); + + function recurse(node: string, path: string[]): string[] | undefined { + for (const dep of deps.get(node) ?? []) { + if (dep === path[0]) { return [...path, dep]; } + + const cycle = recurse(dep, [...path, dep]); + if (cycle) { return cycle; } + } + + return undefined; + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/assertions/lib/private/mappings.ts b/packages/@aws-cdk/assertions/lib/private/mappings.ts index e080843dd87f8..e8788fb2ef112 100644 --- a/packages/@aws-cdk/assertions/lib/private/mappings.ts +++ b/packages/@aws-cdk/assertions/lib/private/mappings.ts @@ -2,7 +2,7 @@ import { filterLogicalId, formatFailure, matchSection } from './section'; import { Template } from './template'; export function findMappings(template: Template, logicalId: string, props: any = {}): { [key: string]: { [key: string]: any } } { - const section: { [key: string] : {} } = template.Mappings; + const section: { [key: string] : {} } = template.Mappings ?? {}; const result = matchSection(filterLogicalId(section, logicalId), props); if (!result.match) { @@ -13,7 +13,7 @@ export function findMappings(template: Template, logicalId: string, props: any = } export function hasMapping(template: Template, logicalId: string, props: any): string | void { - const section: { [key: string]: {} } = template.Mappings; + const section: { [key: string]: {} } = template.Mappings ?? {}; const result = matchSection(filterLogicalId(section, logicalId), props); if (result.match) { diff --git a/packages/@aws-cdk/assertions/lib/private/outputs.ts b/packages/@aws-cdk/assertions/lib/private/outputs.ts index f00f05bc9bb0f..39509698d0e43 100644 --- a/packages/@aws-cdk/assertions/lib/private/outputs.ts +++ b/packages/@aws-cdk/assertions/lib/private/outputs.ts @@ -2,7 +2,7 @@ import { filterLogicalId, formatFailure, matchSection } from './section'; import { Template } from './template'; export function findOutputs(template: Template, logicalId: string, props: any = {}): { [key: string]: { [key: string]: any } } { - const section = template.Outputs; + const section = template.Outputs ?? {}; const result = matchSection(filterLogicalId(section, logicalId), props); if (!result.match) { @@ -13,7 +13,7 @@ export function findOutputs(template: Template, logicalId: string, props: any = } export function hasOutput(template: Template, logicalId: string, props: any): string | void { - const section: { [key: string]: {} } = template.Outputs; + const section: { [key: string]: {} } = template.Outputs ?? {}; const result = matchSection(filterLogicalId(section, logicalId), props); if (result.match) { return; diff --git a/packages/@aws-cdk/assertions/lib/private/parameters.ts b/packages/@aws-cdk/assertions/lib/private/parameters.ts index b708460caf399..0e73160ea5a75 100644 --- a/packages/@aws-cdk/assertions/lib/private/parameters.ts +++ b/packages/@aws-cdk/assertions/lib/private/parameters.ts @@ -2,7 +2,7 @@ import { filterLogicalId, formatFailure, matchSection } from './section'; import { Template } from './template'; export function findParameters(template: Template, logicalId: string, props: any = {}): { [key: string]: { [key: string]: any } } { - const section: { [key: string] : {} } = template.Parameters; + const section: { [key: string] : {} } = template.Parameters ?? {}; const result = matchSection(filterLogicalId(section, logicalId), props); if (!result.match) { @@ -13,7 +13,7 @@ export function findParameters(template: Template, logicalId: string, props: any } export function hasParameter(template: Template, logicalId: string, props: any): string | void { - const section: { [key: string] : {} } = template.Parameters; + const section: { [key: string] : {} } = template.Parameters ?? {}; const result = matchSection(filterLogicalId(section, logicalId), props); if (result.match) { return; diff --git a/packages/@aws-cdk/assertions/lib/private/resources.ts b/packages/@aws-cdk/assertions/lib/private/resources.ts index 68e8e6c2ddff8..00a57c05f9b26 100644 --- a/packages/@aws-cdk/assertions/lib/private/resources.ts +++ b/packages/@aws-cdk/assertions/lib/private/resources.ts @@ -4,7 +4,7 @@ import { formatFailure, matchSection } from './section'; import { Resource, Template } from './template'; export function findResources(template: Template, type: string, props: any = {}): { [key: string]: { [key: string]: any } } { - const section = template.Resources; + const section = template.Resources ?? {}; const result = matchSection(filterType(section, type), props); if (!result.match) { @@ -15,7 +15,7 @@ export function findResources(template: Template, type: string, props: any = {}) } export function hasResource(template: Template, type: string, props: any): string | void { - const section = template.Resources; + const section = template.Resources ?? {}; const result = matchSection(filterType(section, type), props); if (result.match) { return; @@ -46,14 +46,14 @@ export function hasResourceProperties(template: Template, type: string, props: a } export function countResources(template: Template, type: string): number { - const section = template.Resources; + const section = template.Resources ?? {}; const types = filterType(section, type); return Object.entries(types).length; } function addEmptyProperties(template: Template): Template { - let section = template.Resources; + let section = template.Resources ?? {}; Object.keys(section).map((key) => { if (!section[key].hasOwnProperty('Properties')) { diff --git a/packages/@aws-cdk/assertions/lib/private/template.ts b/packages/@aws-cdk/assertions/lib/private/template.ts index fc5d0cb6b1e01..4aea34a2b5132 100644 --- a/packages/@aws-cdk/assertions/lib/private/template.ts +++ b/packages/@aws-cdk/assertions/lib/private/template.ts @@ -1,15 +1,19 @@ // Partial types for CloudFormation Template export type Template = { - Resources: { [logicalId: string]: Resource }, - Outputs: { [logicalId: string]: Output }, - Mappings: { [logicalId: string]: Mapping }, - Parameters: { [logicalId: string]: Parameter }, - Conditions: { [logicalId: string]: Condition }, + // In actuality this is not optional, but we sometimes don't generate it so we + // need to account for that. + Resources?: { [logicalId: string]: Resource }, + Outputs?: { [logicalId: string]: Output }, + Mappings?: { [logicalId: string]: Mapping }, + Parameters?: { [logicalId: string]: Parameter }, + Conditions?: { [logicalId: string]: Condition }, } export type Resource = { Type: string; + DependsOn?: string | string[]; + Properties?: { [key: string]: any }; [key: string]: any; } diff --git a/packages/@aws-cdk/assertions/lib/template.ts b/packages/@aws-cdk/assertions/lib/template.ts index ec23538eaf4aa..6399d3a971897 100644 --- a/packages/@aws-cdk/assertions/lib/template.ts +++ b/packages/@aws-cdk/assertions/lib/template.ts @@ -4,6 +4,7 @@ import * as fs from 'fs-extra'; import { Match } from './match'; import { Matcher } from './matcher'; import { findConditions, hasCondition } from './private/conditions'; +import { checkTemplateForCyclicDependencies } from './private/cyclic'; import { findMappings, hasMapping } from './private/mappings'; import { findOutputs, hasOutput } from './private/outputs'; import { findParameters, hasParameter } from './private/parameters'; @@ -47,6 +48,7 @@ export class Template { private constructor(template: { [key: string]: any }) { this.template = template as TemplateType; + checkTemplateForCyclicDependencies(this.template); } /** diff --git a/packages/@aws-cdk/assertions/test/template.test.ts b/packages/@aws-cdk/assertions/test/template.test.ts index 92bdb405ab9ce..dcdb73e61da71 100644 --- a/packages/@aws-cdk/assertions/test/template.test.ts +++ b/packages/@aws-cdk/assertions/test/template.test.ts @@ -5,11 +5,11 @@ import { Capture, Match, Template } from '../lib'; describe('Template', () => { test('fromString', () => { const template = Template.fromString(`{ - "Resources": { - "Foo": { + "Resources": { + "Foo": { "Type": "Baz::Qux", "Properties": { "Fred": "Waldo" } - } + } } }`); @@ -79,11 +79,11 @@ describe('Template', () => { describe('fromString', () => { test('default', () => { const assertions = Template.fromString(`{ - "Resources": { - "Foo": { + "Resources": { + "Foo": { "Type": "Baz::Qux", "Properties": { "Fred": "Waldo" } - } + } } }`); assertions.resourceCountIs('Baz::Qux', 1); @@ -1084,6 +1084,25 @@ describe('Template', () => { expect(Object.keys(result).length).toEqual(0); }); }); + + test('throws when given a template with cyclic dependencies', () => { + expect(() => { + Template.fromJSON({ + Resources: { + Res1: { + Type: 'Foo', + Properties: { + Thing: { Ref: 'Res2' }, + }, + }, + Res2: { + Type: 'Foo', + DependsOn: ['Res1'], + }, + }, + }); + }).toThrow(/dependency cycle/); + }); }); function expectToThrow(fn: () => void, msgs: (RegExp | string)[], done: jest.DoneCallback): void { diff --git a/packages/@aws-cdk/aws-cognito-identitypool/lib/identitypool.ts b/packages/@aws-cdk/aws-cognito-identitypool/lib/identitypool.ts index cefab0caa54d9..1fd89f238e0b0 100644 --- a/packages/@aws-cdk/aws-cognito-identitypool/lib/identitypool.ts +++ b/packages/@aws-cdk/aws-cognito-identitypool/lib/identitypool.ts @@ -437,7 +437,12 @@ export class IdentityPool extends Resource implements IIdentityPool { unauthenticatedRole: this.unauthenticatedRole, roleMappings: props.roleMappings, }); - attachment.node.addDependency(this); + + // This added by the original author, but it's causing cyclic dependencies. + // Don't know why this was added in the first place, but I'm disabling it for now and if + // no complaints come from this, we're probably safe to remove it altogether. + // attachment.node.addDependency(this); + Array.isArray(attachment); } /** @@ -461,7 +466,12 @@ export class IdentityPool extends Resource implements IIdentityPool { unauthenticatedRole: this.unauthenticatedRole, roleMappings, }); - attachment.node.addDependency(this); + + // This added by the original author, but it's causing cyclic dependencies. + // Don't know why this was added in the first place, but I'm disabling it for now and if + // no complaints come from this, we're probably safe to remove it altogether. + // attachment.node.addDependency(this); + Array.isArray(attachment); } /** diff --git a/packages/@aws-cdk/aws-cognito-identitypool/test/integ.identitypool.expected.json b/packages/@aws-cdk/aws-cognito-identitypool/test/integ.identitypool.expected.json index b555de31baa1e..b2ec1baf42d81 100644 --- a/packages/@aws-cdk/aws-cognito-identitypool/test/integ.identitypool.expected.json +++ b/packages/@aws-cdk/aws-cognito-identitypool/test/integ.identitypool.expected.json @@ -402,11 +402,6 @@ } }, "DependsOn": [ - "identitypoolAuthenticatedRoleDefaultPolicyCB4D2992", - "identitypoolAuthenticatedRoleB074B49D", - "identitypoolE2A6D099", - "identitypoolUnauthenticatedRoleDefaultPolicyBFACCE98", - "identitypoolUnauthenticatedRoleE61CAC70", "OtherPool7DA7F2F7", "OtherPoolUserPoolAuthenticationProviderClient08F670F8", "PoolD3F588B8", diff --git a/packages/@aws-cdk/pipelines/lib/codepipeline/_codebuild-factory.ts b/packages/@aws-cdk/pipelines/lib/codepipeline/_codebuild-factory.ts index 5d73e23784314..3414554cd5197 100644 --- a/packages/@aws-cdk/pipelines/lib/codepipeline/_codebuild-factory.ts +++ b/packages/@aws-cdk/pipelines/lib/codepipeline/_codebuild-factory.ts @@ -5,7 +5,7 @@ import * as codepipeline from '@aws-cdk/aws-codepipeline'; import * as codepipeline_actions from '@aws-cdk/aws-codepipeline-actions'; import * as ec2 from '@aws-cdk/aws-ec2'; import * as iam from '@aws-cdk/aws-iam'; -import { IDependable, Stack } from '@aws-cdk/core'; +import { IDependable, Stack, Token } from '@aws-cdk/core'; import { Construct, Node } from 'constructs'; import { FileSetLocation, ShellStep, StackOutputReference } from '../blueprint'; import { PipelineQueries } from '../helpers-internal/pipeline-queries'; @@ -271,9 +271,13 @@ export class CodeBuildFactory implements ICodePipelineActionFactory { projectScope = obtainScope(scope, actionName); } + const safePipelineName = Token.isUnresolved(options.pipeline.pipeline.pipelineName) + ? `${Stack.of(options.pipeline).stackName}/${Node.of(options.pipeline.pipeline).id}` + : options.pipeline.pipeline.pipelineName; + const project = new codebuild.PipelineProject(projectScope, this.constructId, { projectName: this.props.projectName, - description: `Pipeline step ${options.pipeline.pipeline.pipelineName}/${stage.stageName}/${actionName}`, + description: `Pipeline step ${safePipelineName}/${stage.stageName}/${actionName}`.substring(0, 255), environment, vpc: projectOptions.vpc, subnetSelection: projectOptions.subnetSelection, diff --git a/packages/@aws-cdk/pipelines/test/codepipeline/codebuild-step.test.ts b/packages/@aws-cdk/pipelines/test/codepipeline/codebuild-step.test.ts index 01069ee7adee0..393f1ffb965ba 100644 --- a/packages/@aws-cdk/pipelines/test/codepipeline/codebuild-step.test.ts +++ b/packages/@aws-cdk/pipelines/test/codepipeline/codebuild-step.test.ts @@ -55,12 +55,7 @@ test('CodeBuild projects have a description', () => { Template.fromStack(pipelineStack).hasResourceProperties( 'AWS::CodeBuild::Project', { - Description: { - 'Fn::Join': [ - '', - ['Pipeline step ', { Ref: 'Pipeline9850B417' }, '/Build/Synth'], - ], - }, + Description: 'Pipeline step PipelineStack/Pipeline/Build/Synth', }, ); }); diff --git a/packages/@aws-cdk/pipelines/test/integ.newpipeline-with-vpc.expected.json b/packages/@aws-cdk/pipelines/test/integ.newpipeline-with-vpc.expected.json index a3f12aa278c48..4bd2e638afb4c 100644 --- a/packages/@aws-cdk/pipelines/test/integ.newpipeline-with-vpc.expected.json +++ b/packages/@aws-cdk/pipelines/test/integ.newpipeline-with-vpc.expected.json @@ -1363,18 +1363,7 @@ "Cache": { "Type": "NO_CACHE" }, - "Description": { - "Fn::Join": [ - "", - [ - "Pipeline step ", - { - "Ref": "Pipeline9850B417" - }, - "/Build/Synth" - ] - ] - }, + "Description": "Pipeline step PipelineStack/Pipeline/Build/Synth", "EncryptionKey": "alias/aws/s3", "VpcConfig": { "SecurityGroupIds": [ @@ -1962,18 +1951,7 @@ "Cache": { "Type": "NO_CACHE" }, - "Description": { - "Fn::Join": [ - "", - [ - "Pipeline step ", - { - "Ref": "Pipeline9850B417" - }, - "/UpdatePipeline/SelfMutate" - ] - ] - }, + "Description": "Pipeline step PipelineStack/Pipeline/UpdatePipeline/SelfMutate", "EncryptionKey": "alias/aws/s3", "VpcConfig": { "SecurityGroupIds": [ @@ -2316,18 +2294,7 @@ "Cache": { "Type": "NO_CACHE" }, - "Description": { - "Fn::Join": [ - "", - [ - "Pipeline step ", - { - "Ref": "Pipeline9850B417" - }, - "/Assets/FileAsset1" - ] - ] - }, + "Description": "Pipeline step PipelineStack/Pipeline/Assets/FileAsset1", "EncryptionKey": "alias/aws/s3", "VpcConfig": { "SecurityGroupIds": [ @@ -2429,18 +2396,7 @@ "Cache": { "Type": "NO_CACHE" }, - "Description": { - "Fn::Join": [ - "", - [ - "Pipeline step ", - { - "Ref": "Pipeline9850B417" - }, - "/Assets/FileAsset2" - ] - ] - }, + "Description": "Pipeline step PipelineStack/Pipeline/Assets/FileAsset2", "EncryptionKey": "alias/aws/s3", "VpcConfig": { "SecurityGroupIds": [ diff --git a/packages/@aws-cdk/pipelines/test/integ.newpipeline.expected.json b/packages/@aws-cdk/pipelines/test/integ.newpipeline.expected.json index 47c1f4d129631..37cd5d99fd7f8 100644 --- a/packages/@aws-cdk/pipelines/test/integ.newpipeline.expected.json +++ b/packages/@aws-cdk/pipelines/test/integ.newpipeline.expected.json @@ -2044,18 +2044,7 @@ "Cache": { "Type": "NO_CACHE" }, - "Description": { - "Fn::Join": [ - "", - [ - "Pipeline step ", - { - "Ref": "Pipeline9850B417" - }, - "/Build/Synth" - ] - ] - }, + "Description": "Pipeline step PipelineStack/Pipeline/Build/Synth", "EncryptionKey": "alias/aws/s3" } }, @@ -2355,18 +2344,7 @@ "Cache": { "Type": "NO_CACHE" }, - "Description": { - "Fn::Join": [ - "", - [ - "Pipeline step ", - { - "Ref": "Pipeline9850B417" - }, - "/UpdatePipeline/SelfMutate" - ] - ] - }, + "Description": "Pipeline step PipelineStack/Pipeline/UpdatePipeline/SelfMutate", "EncryptionKey": "alias/aws/s3" } } diff --git a/packages/@monocdk-experiment/assert/tsconfig.json b/packages/@monocdk-experiment/assert/tsconfig.json index b426f95fcb96a..e1b9688cb975e 100644 --- a/packages/@monocdk-experiment/assert/tsconfig.json +++ b/packages/@monocdk-experiment/assert/tsconfig.json @@ -1,7 +1,7 @@ { "compilerOptions": { - "target":"ES2018", - "lib": ["es2018"], + "target":"ES2019", + "lib": ["es2019"], "module": "CommonJS", "declaration": true, "strict": true, diff --git a/packages/aws-cdk-lib/package.json b/packages/aws-cdk-lib/package.json index e2ec7540fb741..61d5b8455d10f 100644 --- a/packages/aws-cdk-lib/package.json +++ b/packages/aws-cdk-lib/package.json @@ -433,7 +433,6 @@ "./aws-codestarconnections": "./aws-codestarconnections/index.js", "./aws-codestarnotifications": "./aws-codestarnotifications/index.js", "./aws-cognito": "./aws-cognito/index.js", - "./aws-cognito-identitypool": "./aws-cognito-identitypool/index.js", "./aws-config": "./aws-config/index.js", "./aws-connect": "./aws-connect/index.js", "./aws-cur": "./aws-cur/index.js", @@ -472,6 +471,7 @@ "./aws-finspace": "./aws-finspace/index.js", "./aws-fis": "./aws-fis/index.js", "./aws-fms": "./aws-fms/index.js", + "./aws-forecast": "./aws-forecast/index.js", "./aws-frauddetector": "./aws-frauddetector/index.js", "./aws-fsx": "./aws-fsx/index.js", "./aws-gamelift": "./aws-gamelift/index.js", @@ -486,6 +486,7 @@ "./aws-iam": "./aws-iam/index.js", "./aws-imagebuilder": "./aws-imagebuilder/index.js", "./aws-inspector": "./aws-inspector/index.js", + "./aws-inspectorv2": "./aws-inspectorv2/index.js", "./aws-iot": "./aws-iot/index.js", "./aws-iot1click": "./aws-iot1click/index.js", "./aws-iotanalytics": "./aws-iotanalytics/index.js", @@ -499,7 +500,9 @@ "./aws-kendra": "./aws-kendra/index.js", "./aws-kinesis": "./aws-kinesis/index.js", "./aws-kinesisanalytics": "./aws-kinesisanalytics/index.js", + "./aws-kinesisanalyticsv2": "./aws-kinesisanalyticsv2/index.js", "./aws-kinesisfirehose": "./aws-kinesisfirehose/index.js", + "./aws-kinesisvideo": "./aws-kinesisvideo/index.js", "./aws-kms": "./aws-kms/index.js", "./aws-lakeformation": "./aws-lakeformation/index.js", "./aws-lambda": "./aws-lambda/index.js",