From a18adaf9a876bb17c98197fe9785066936c00995 Mon Sep 17 00:00:00 2001 From: epolon Date: Tue, 19 Nov 2024 20:49:53 +0200 Subject: [PATCH 1/9] mid work --- packages/aws-cdk/lib/api/aws-auth/sdk.ts | 44 +++++---- .../api/evaluate-cloudformation-template.ts | 2 +- .../api/hotswap/appsync-mapping-templates.ts | 2 +- .../util/cloudformation/stack-event-poller.ts | 94 +++++++++---------- .../lib/context-providers/load-balancers.ts | 4 +- 5 files changed, 69 insertions(+), 77 deletions(-) diff --git a/packages/aws-cdk/lib/api/aws-auth/sdk.ts b/packages/aws-cdk/lib/api/aws-auth/sdk.ts index 25a04aac6ef0a..d2de5498fd6f8 100644 --- a/packages/aws-cdk/lib/api/aws-auth/sdk.ts +++ b/packages/aws-cdk/lib/api/aws-auth/sdk.ts @@ -52,6 +52,7 @@ import { type DescribeResourceScanCommandInput, type DescribeResourceScanCommandOutput, type DescribeStackEventsCommandInput, + DescribeStackEventsCommandOutput, DescribeStackResourcesCommand, DescribeStackResourcesCommandInput, DescribeStackResourcesCommandOutput, @@ -91,7 +92,6 @@ import { RollbackStackCommand, RollbackStackCommandInput, RollbackStackCommandOutput, - StackEvent, StackResourceSummary, StartResourceScanCommand, type StartResourceScanCommandInput, @@ -311,7 +311,7 @@ import { GetCallerIdentityCommand, STSClient } from '@aws-sdk/client-sts'; import { Upload } from '@aws-sdk/lib-storage'; import { getEndpointFromInstructions } from '@smithy/middleware-endpoint'; import type { NodeHttpHandlerOptions } from '@smithy/node-http-handler'; -import { AwsCredentialIdentity, Logger } from '@smithy/types'; +import { AwsCredentialIdentity, Logger, Paginator } from '@smithy/types'; import { ConfiguredRetryStrategy } from '@smithy/util-retry'; import { WaiterResult } from '@smithy/util-waiter'; import { AccountAccessKeyCache } from './account-cache'; @@ -365,8 +365,7 @@ export interface IAppSyncClient { updateApiKey(input: UpdateApiKeyCommandInput): Promise; updateFunction(input: UpdateFunctionCommandInput): Promise; updateResolver(input: UpdateResolverCommandInput): Promise; - // Pagination functions - listFunctions(input: ListFunctionsCommandInput): Promise; + listAllFunctions(input: ListFunctionsCommandInput): Promise; } export interface ICloudFormationClient { @@ -403,9 +402,8 @@ export interface ICloudFormationClient { updateTerminationProtection( input: UpdateTerminationProtectionCommandInput, ): Promise; - // Pagination functions - describeStackEvents(input: DescribeStackEventsCommandInput): Promise; - listStackResources(input: ListStackResourcesCommandInput): Promise; + describeStackEventsPaginated(input: DescribeStackEventsCommandInput): Paginator; + listAllStackResources(input: ListStackResourcesCommandInput): Promise; } export interface ICloudWatchLogsClient { @@ -455,12 +453,19 @@ export interface IECSClient { } export interface IElasticLoadBalancingV2Client { + /** + * Returns only the first page. Use `.NextMarker` on the result to query for the next page. + * To retrieve all results, use `describeAllListeners`. + */ describeListeners(input: DescribeListenersCommandInput): Promise; + /** + * Returns only the first page. Use `.NextMarker` on the result to query for the next page. + * To retrieve all results, use `describeAllLoadBalancers`. + */ describeLoadBalancers(input: DescribeLoadBalancersCommandInput): Promise; describeTags(input: DescribeTagsCommandInput): Promise; - // Pagination - paginateDescribeListeners(input: DescribeListenersCommandInput): Promise; - paginateDescribeLoadBalancers(input: DescribeLoadBalancersCommandInput): Promise; + describeAllListeners(input: DescribeListenersCommandInput): Promise; + describeAllLoadBalancers(input: DescribeLoadBalancersCommandInput): Promise; } export interface IIAMClient { @@ -587,8 +592,7 @@ export class SDK { updateResolver: (input: UpdateResolverCommandInput): Promise => client.send(new UpdateResolverCommand(input)), - // Pagination Functions - listFunctions: async (input: ListFunctionsCommandInput): Promise => { + listAllFunctions: async (input: ListFunctionsCommandInput): Promise => { const functions = Array(); const paginator = paginateListFunctions({ client }, input); for await (const page of paginator) { @@ -664,15 +668,10 @@ export class SDK { input: UpdateTerminationProtectionCommandInput, ): Promise => client.send(new UpdateTerminationProtectionCommand(input)), - describeStackEvents: async (input: DescribeStackEventsCommandInput): Promise => { - const stackEvents = Array(); - const paginator = paginateDescribeStackEvents({ client }, input); - for await (const page of paginator) { - stackEvents.push(...(page?.StackEvents || [])); - } - return stackEvents; + describeStackEventsPaginated: (input: DescribeStackEventsCommandInput): Paginator => { + return paginateDescribeStackEvents({ client }, input); }, - listStackResources: async (input: ListStackResourcesCommandInput): Promise => { + listAllStackResources: async (input: ListStackResourcesCommandInput): Promise => { const stackResources = Array(); const paginator = paginateListStackResources({ client }, input); for await (const page of paginator) { @@ -789,8 +788,7 @@ export class SDK { client.send(new DescribeLoadBalancersCommand(input)), describeTags: (input: DescribeTagsCommandInput): Promise => client.send(new DescribeTagsCommand(input)), - // Pagination Functions - paginateDescribeListeners: async (input: DescribeListenersCommandInput): Promise => { + describeAllListeners: async (input: DescribeListenersCommandInput): Promise => { const listeners = Array(); const paginator = paginateDescribeListeners({ client }, input); for await (const page of paginator) { @@ -798,7 +796,7 @@ export class SDK { } return listeners; }, - paginateDescribeLoadBalancers: async (input: DescribeLoadBalancersCommandInput): Promise => { + describeAllLoadBalancers: async (input: DescribeLoadBalancersCommandInput): Promise => { const loadBalancers = Array(); const paginator = paginateDescribeLoadBalancers({ client }, input); for await (const page of paginator) { diff --git a/packages/aws-cdk/lib/api/evaluate-cloudformation-template.ts b/packages/aws-cdk/lib/api/evaluate-cloudformation-template.ts index ec2b7c8b29821..35e5e0428bd01 100644 --- a/packages/aws-cdk/lib/api/evaluate-cloudformation-template.ts +++ b/packages/aws-cdk/lib/api/evaluate-cloudformation-template.ts @@ -16,7 +16,7 @@ export class LazyListStackResources implements ListStackResources { public async listStackResources(): Promise { if (this.stackResources === undefined) { - this.stackResources = this.sdk.cloudFormation().listStackResources({ + this.stackResources = this.sdk.cloudFormation().listAllStackResources({ StackName: this.stackName, }); } diff --git a/packages/aws-cdk/lib/api/hotswap/appsync-mapping-templates.ts b/packages/aws-cdk/lib/api/hotswap/appsync-mapping-templates.ts index d05aa56063339..b9e4e81de37d3 100644 --- a/packages/aws-cdk/lib/api/hotswap/appsync-mapping-templates.ts +++ b/packages/aws-cdk/lib/api/hotswap/appsync-mapping-templates.ts @@ -115,7 +115,7 @@ export async function isHotswappableAppSyncChange( delete sdkRequestObject.runtime; } - const functions = await sdk.appsync().listFunctions({ apiId: sdkRequestObject.apiId }); + const functions = await sdk.appsync().listAllFunctions({ apiId: sdkRequestObject.apiId }); const { functionId } = functions.find((fn) => fn.name === physicalName) ?? {}; // Updating multiple functions at the same time or along with graphql schema results in `ConcurrentModificationException` await simpleRetry( diff --git a/packages/aws-cdk/lib/api/util/cloudformation/stack-event-poller.ts b/packages/aws-cdk/lib/api/util/cloudformation/stack-event-poller.ts index 9fa192fea1ce2..b703ff80216d3 100644 --- a/packages/aws-cdk/lib/api/util/cloudformation/stack-event-poller.ts +++ b/packages/aws-cdk/lib/api/util/cloudformation/stack-event-poller.ts @@ -86,49 +86,53 @@ export class StackEventPoller { } private async doPoll(): Promise { + // eslint-disable-next-line no-console + console.error('Polling for events'); const events: ResourceEvent[] = []; try { - const eventList = await this.cfn.describeStackEvents({ + const paginator = this.cfn.describeStackEventsPaginated({ StackName: this.props.stackName, }); - for (const event of eventList) { - // Event from before we were interested in 'em - if (this.props.startTime !== undefined && event.Timestamp!.valueOf() < this.props.startTime) { - return events; - } - - // Already seen this one - if (this.eventIds.has(event.EventId!)) { - return events; - } - this.eventIds.add(event.EventId!); - - // The events for the stack itself are also included next to events about resources; we can test for them in this way. - const isParentStackEvent = event.PhysicalResourceId === event.StackId; - - if (isParentStackEvent && this.props.stackStatuses?.includes(event.ResourceStatus ?? '')) { - return events; - } - - // Fresh event - const resEvent: ResourceEvent = { - event: event, - parentStackLogicalIds: this.props.parentStackLogicalIds ?? [], - isStackEvent: isParentStackEvent, - }; - events.push(resEvent); - - if ( - !isParentStackEvent && - event.ResourceType === 'AWS::CloudFormation::Stack' && - isStackBeginOperationState(event.ResourceStatus) - ) { - // If the event is not for `this` stack and has a physical resource Id, recursively call for events in the nested stack - this.trackNestedStack(event, [...(this.props.parentStackLogicalIds ?? []), event.LogicalResourceId ?? '']); - } - - if (isParentStackEvent && isStackTerminalState(event.ResourceStatus)) { - this.complete = true; + for await (const page of paginator) { + for (const event of page.StackEvents ?? []) { + // Event from before we were interested in 'em + if (this.props.startTime !== undefined && event.Timestamp!.valueOf() < this.props.startTime) { + return events; + } + + // Already seen this one + if (this.eventIds.has(event.EventId!)) { + return events; + } + this.eventIds.add(event.EventId!); + + // The events for the stack itself are also included next to events about resources; we can test for them in this way. + const isParentStackEvent = event.PhysicalResourceId === event.StackId; + + if (isParentStackEvent && this.props.stackStatuses?.includes(event.ResourceStatus ?? '')) { + return events; + } + + // Fresh event + const resEvent: ResourceEvent = { + event: event, + parentStackLogicalIds: this.props.parentStackLogicalIds ?? [], + isStackEvent: isParentStackEvent, + }; + events.push(resEvent); + + if ( + !isParentStackEvent && + event.ResourceType === 'AWS::CloudFormation::Stack' && + isStackBeginOperationState(event.ResourceStatus) + ) { + // If the event is not for `this` stack and has a physical resource Id, recursively call for events in the nested stack + this.trackNestedStack(event, [...(this.props.parentStackLogicalIds ?? []), event.LogicalResourceId ?? '']); + } + + if (isParentStackEvent && isStackTerminalState(event.ResourceStatus)) { + this.complete = true; + } } } } catch (e: any) { @@ -136,17 +140,7 @@ export class StackEventPoller { throw e; } } - // // Also poll all nested stacks we're currently tracking - // for (const [logicalId, poller] of Object.entries(this.nestedStackPollers)) { - // events.push(...(await poller.poll())); - // if (poller.complete) { - // delete this.nestedStackPollers[logicalId]; - // } - // } - - // // Return what we have so far - // events.sort((a, b) => a.event.Timestamp!.valueOf() - b.event.Timestamp!.valueOf()); - // this.events.push(...events); + return events; } diff --git a/packages/aws-cdk/lib/context-providers/load-balancers.ts b/packages/aws-cdk/lib/context-providers/load-balancers.ts index f9f2e53089295..126078d17a0f5 100644 --- a/packages/aws-cdk/lib/context-providers/load-balancers.ts +++ b/packages/aws-cdk/lib/context-providers/load-balancers.ts @@ -146,7 +146,7 @@ class LoadBalancerProvider { private async getLoadBalancers() { const loadBalancerArns = this.filter.loadBalancerArn ? [this.filter.loadBalancerArn] : undefined; const loadBalancers = ( - await this.client.paginateDescribeLoadBalancers({ + await this.client.describeAllLoadBalancers({ LoadBalancerArns: loadBalancerArns, }) ).filter((lb) => lb.Type === this.filter.loadBalancerType); @@ -193,7 +193,7 @@ class LoadBalancerProvider { private async getListenersForLoadBalancers(loadBalancers: LoadBalancer[]): Promise { const listeners: Listener[] = []; for (const loadBalancer of loadBalancers.map((lb) => lb.LoadBalancerArn)) { - listeners.push(...(await this.client.paginateDescribeListeners({ LoadBalancerArn: loadBalancer }))); + listeners.push(...(await this.client.describeAllListeners({ LoadBalancerArn: loadBalancer }))); } return listeners; } From 57d91267ad0a120d5f5c6553dbf16a61727b7e82 Mon Sep 17 00:00:00 2001 From: epolon Date: Wed, 20 Nov 2024 07:15:53 +0200 Subject: [PATCH 2/9] add test --- packages/aws-cdk/lib/api/aws-auth/sdk.ts | 21 +++++---- .../api/evaluate-cloudformation-template.ts | 2 +- .../api/hotswap/appsync-mapping-templates.ts | 2 +- .../util/cloudformation/stack-event-poller.ts | 2 - .../lib/context-providers/load-balancers.ts | 4 +- .../cloudformation/stack-event-poller.test.ts | 46 +++++++++++++++++++ 6 files changed, 63 insertions(+), 14 deletions(-) create mode 100644 packages/aws-cdk/test/api/util/cloudformation/stack-event-poller.test.ts diff --git a/packages/aws-cdk/lib/api/aws-auth/sdk.ts b/packages/aws-cdk/lib/api/aws-auth/sdk.ts index d2de5498fd6f8..3af29db9b598a 100644 --- a/packages/aws-cdk/lib/api/aws-auth/sdk.ts +++ b/packages/aws-cdk/lib/api/aws-auth/sdk.ts @@ -365,7 +365,8 @@ export interface IAppSyncClient { updateApiKey(input: UpdateApiKeyCommandInput): Promise; updateFunction(input: UpdateFunctionCommandInput): Promise; updateResolver(input: UpdateResolverCommandInput): Promise; - listAllFunctions(input: ListFunctionsCommandInput): Promise; + // Pagination functions + listFunctions(input: ListFunctionsCommandInput): Promise; } export interface ICloudFormationClient { @@ -403,7 +404,8 @@ export interface ICloudFormationClient { input: UpdateTerminationProtectionCommandInput, ): Promise; describeStackEventsPaginated(input: DescribeStackEventsCommandInput): Paginator; - listAllStackResources(input: ListStackResourcesCommandInput): Promise; + // Pagination functions + listStackResources(input: ListStackResourcesCommandInput): Promise; } export interface ICloudWatchLogsClient { @@ -464,8 +466,9 @@ export interface IElasticLoadBalancingV2Client { */ describeLoadBalancers(input: DescribeLoadBalancersCommandInput): Promise; describeTags(input: DescribeTagsCommandInput): Promise; - describeAllListeners(input: DescribeListenersCommandInput): Promise; - describeAllLoadBalancers(input: DescribeLoadBalancersCommandInput): Promise; + // Pagination + paginateDescribeListeners(input: DescribeListenersCommandInput): Promise; + paginateDescribeLoadBalancers(input: DescribeLoadBalancersCommandInput): Promise; } export interface IIAMClient { @@ -592,7 +595,8 @@ export class SDK { updateResolver: (input: UpdateResolverCommandInput): Promise => client.send(new UpdateResolverCommand(input)), - listAllFunctions: async (input: ListFunctionsCommandInput): Promise => { + // Pagination Functions + listFunctions: async (input: ListFunctionsCommandInput): Promise => { const functions = Array(); const paginator = paginateListFunctions({ client }, input); for await (const page of paginator) { @@ -671,7 +675,7 @@ export class SDK { describeStackEventsPaginated: (input: DescribeStackEventsCommandInput): Paginator => { return paginateDescribeStackEvents({ client }, input); }, - listAllStackResources: async (input: ListStackResourcesCommandInput): Promise => { + listStackResources: async (input: ListStackResourcesCommandInput): Promise => { const stackResources = Array(); const paginator = paginateListStackResources({ client }, input); for await (const page of paginator) { @@ -788,7 +792,8 @@ export class SDK { client.send(new DescribeLoadBalancersCommand(input)), describeTags: (input: DescribeTagsCommandInput): Promise => client.send(new DescribeTagsCommand(input)), - describeAllListeners: async (input: DescribeListenersCommandInput): Promise => { + // Pagination Functions + paginateDescribeListeners: async (input: DescribeListenersCommandInput): Promise => { const listeners = Array(); const paginator = paginateDescribeListeners({ client }, input); for await (const page of paginator) { @@ -796,7 +801,7 @@ export class SDK { } return listeners; }, - describeAllLoadBalancers: async (input: DescribeLoadBalancersCommandInput): Promise => { + paginateDescribeLoadBalancers: async (input: DescribeLoadBalancersCommandInput): Promise => { const loadBalancers = Array(); const paginator = paginateDescribeLoadBalancers({ client }, input); for await (const page of paginator) { diff --git a/packages/aws-cdk/lib/api/evaluate-cloudformation-template.ts b/packages/aws-cdk/lib/api/evaluate-cloudformation-template.ts index 35e5e0428bd01..ec2b7c8b29821 100644 --- a/packages/aws-cdk/lib/api/evaluate-cloudformation-template.ts +++ b/packages/aws-cdk/lib/api/evaluate-cloudformation-template.ts @@ -16,7 +16,7 @@ export class LazyListStackResources implements ListStackResources { public async listStackResources(): Promise { if (this.stackResources === undefined) { - this.stackResources = this.sdk.cloudFormation().listAllStackResources({ + this.stackResources = this.sdk.cloudFormation().listStackResources({ StackName: this.stackName, }); } diff --git a/packages/aws-cdk/lib/api/hotswap/appsync-mapping-templates.ts b/packages/aws-cdk/lib/api/hotswap/appsync-mapping-templates.ts index b9e4e81de37d3..d05aa56063339 100644 --- a/packages/aws-cdk/lib/api/hotswap/appsync-mapping-templates.ts +++ b/packages/aws-cdk/lib/api/hotswap/appsync-mapping-templates.ts @@ -115,7 +115,7 @@ export async function isHotswappableAppSyncChange( delete sdkRequestObject.runtime; } - const functions = await sdk.appsync().listAllFunctions({ apiId: sdkRequestObject.apiId }); + const functions = await sdk.appsync().listFunctions({ apiId: sdkRequestObject.apiId }); const { functionId } = functions.find((fn) => fn.name === physicalName) ?? {}; // Updating multiple functions at the same time or along with graphql schema results in `ConcurrentModificationException` await simpleRetry( diff --git a/packages/aws-cdk/lib/api/util/cloudformation/stack-event-poller.ts b/packages/aws-cdk/lib/api/util/cloudformation/stack-event-poller.ts index b703ff80216d3..df7f108c31599 100644 --- a/packages/aws-cdk/lib/api/util/cloudformation/stack-event-poller.ts +++ b/packages/aws-cdk/lib/api/util/cloudformation/stack-event-poller.ts @@ -86,8 +86,6 @@ export class StackEventPoller { } private async doPoll(): Promise { - // eslint-disable-next-line no-console - console.error('Polling for events'); const events: ResourceEvent[] = []; try { const paginator = this.cfn.describeStackEventsPaginated({ diff --git a/packages/aws-cdk/lib/context-providers/load-balancers.ts b/packages/aws-cdk/lib/context-providers/load-balancers.ts index 126078d17a0f5..f9f2e53089295 100644 --- a/packages/aws-cdk/lib/context-providers/load-balancers.ts +++ b/packages/aws-cdk/lib/context-providers/load-balancers.ts @@ -146,7 +146,7 @@ class LoadBalancerProvider { private async getLoadBalancers() { const loadBalancerArns = this.filter.loadBalancerArn ? [this.filter.loadBalancerArn] : undefined; const loadBalancers = ( - await this.client.describeAllLoadBalancers({ + await this.client.paginateDescribeLoadBalancers({ LoadBalancerArns: loadBalancerArns, }) ).filter((lb) => lb.Type === this.filter.loadBalancerType); @@ -193,7 +193,7 @@ class LoadBalancerProvider { private async getListenersForLoadBalancers(loadBalancers: LoadBalancer[]): Promise { const listeners: Listener[] = []; for (const loadBalancer of loadBalancers.map((lb) => lb.LoadBalancerArn)) { - listeners.push(...(await this.client.describeAllListeners({ LoadBalancerArn: loadBalancer }))); + listeners.push(...(await this.client.paginateDescribeListeners({ LoadBalancerArn: loadBalancer }))); } return listeners; } diff --git a/packages/aws-cdk/test/api/util/cloudformation/stack-event-poller.test.ts b/packages/aws-cdk/test/api/util/cloudformation/stack-event-poller.test.ts new file mode 100644 index 0000000000000..96e11fbf7a367 --- /dev/null +++ b/packages/aws-cdk/test/api/util/cloudformation/stack-event-poller.test.ts @@ -0,0 +1,46 @@ +import { CloudFormationClient, DescribeStackEventsCommandInput, StackEvent } from '@aws-sdk/client-cloudformation'; +import { SDK } from '../../../../lib'; +import { StackEventPoller } from '../../../../lib/api/util/cloudformation/stack-event-poller'; + +beforeEach(() => { + jest.resetAllMocks(); +}); + +describe('poll', () => { + + test('does not poll unnecessary pages', async () => { + + const now = Date.now(); + + const pastEvent: StackEvent = { + Timestamp: new Date(now - 1000), + EventId: 'event-1', + StackId: 'stack-id', + StackName: 'stack', + }; + + const sdk = new SDK({ accessKeyId: 'accessKeyId', secretAccessKey: 'secretAccessKey' }, 'region', {}); + + jest.spyOn(CloudFormationClient.prototype, 'send').mockImplementation((command) => { + const input: DescribeStackEventsCommandInput = command.input as DescribeStackEventsCommandInput; + + // the first event we return should stop the polling. we therefore + // do not expect a second page to be polled. + expect(input.NextToken).toBe(undefined); + + return { + StackEvents: [pastEvent], + NextToken: input.NextToken === 'token' ? undefined : 'token', // simulate a two page event stream. + }; + }); + + const poller = new StackEventPoller(sdk.cloudFormation(), { + stackName: 'stack', + startTime: new Date().getTime(), + }); + + await poller.poll(); + + }); + +}); From e8776baa177b541b54d33077d6aed2cd49da8878 Mon Sep 17 00:00:00 2001 From: epolon Date: Wed, 20 Nov 2024 07:17:11 +0200 Subject: [PATCH 3/9] cleanup --- packages/aws-cdk/lib/api/aws-auth/sdk.ts | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/packages/aws-cdk/lib/api/aws-auth/sdk.ts b/packages/aws-cdk/lib/api/aws-auth/sdk.ts index 3af29db9b598a..9b38cf5ef8452 100644 --- a/packages/aws-cdk/lib/api/aws-auth/sdk.ts +++ b/packages/aws-cdk/lib/api/aws-auth/sdk.ts @@ -403,8 +403,8 @@ export interface ICloudFormationClient { updateTerminationProtection( input: UpdateTerminationProtectionCommandInput, ): Promise; - describeStackEventsPaginated(input: DescribeStackEventsCommandInput): Paginator; // Pagination functions + describeStackEventsPaginated(input: DescribeStackEventsCommandInput): Paginator; listStackResources(input: ListStackResourcesCommandInput): Promise; } @@ -455,15 +455,7 @@ export interface IECSClient { } export interface IElasticLoadBalancingV2Client { - /** - * Returns only the first page. Use `.NextMarker` on the result to query for the next page. - * To retrieve all results, use `describeAllListeners`. - */ describeListeners(input: DescribeListenersCommandInput): Promise; - /** - * Returns only the first page. Use `.NextMarker` on the result to query for the next page. - * To retrieve all results, use `describeAllLoadBalancers`. - */ describeLoadBalancers(input: DescribeLoadBalancersCommandInput): Promise; describeTags(input: DescribeTagsCommandInput): Promise; // Pagination From 2b75f65b0bd61cea1b16249b46c1998556f455d8 Mon Sep 17 00:00:00 2001 From: epolon Date: Wed, 20 Nov 2024 07:35:51 +0200 Subject: [PATCH 4/9] fix tests --- .../aws-cdk/lib/api/util/cloudformation/stack-event-poller.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/aws-cdk/lib/api/util/cloudformation/stack-event-poller.ts b/packages/aws-cdk/lib/api/util/cloudformation/stack-event-poller.ts index df7f108c31599..99fe847664cc4 100644 --- a/packages/aws-cdk/lib/api/util/cloudformation/stack-event-poller.ts +++ b/packages/aws-cdk/lib/api/util/cloudformation/stack-event-poller.ts @@ -92,7 +92,7 @@ export class StackEventPoller { StackName: this.props.stackName, }); for await (const page of paginator) { - for (const event of page.StackEvents ?? []) { + for (const event of page?.StackEvents ?? []) { // Event from before we were interested in 'em if (this.props.startTime !== undefined && event.Timestamp!.valueOf() < this.props.startTime) { return events; From ebf0b17da71275449b756c285d2003dcc5808af1 Mon Sep 17 00:00:00 2001 From: epolon Date: Wed, 20 Nov 2024 07:56:45 +0200 Subject: [PATCH 5/9] mock with sdk --- .../cloudformation/stack-event-poller.test.ts | 59 +++++++++++++++---- 1 file changed, 48 insertions(+), 11 deletions(-) diff --git a/packages/aws-cdk/test/api/util/cloudformation/stack-event-poller.test.ts b/packages/aws-cdk/test/api/util/cloudformation/stack-event-poller.test.ts index 96e11fbf7a367..1a93e3627c266 100644 --- a/packages/aws-cdk/test/api/util/cloudformation/stack-event-poller.test.ts +++ b/packages/aws-cdk/test/api/util/cloudformation/stack-event-poller.test.ts @@ -1,6 +1,6 @@ -import { CloudFormationClient, DescribeStackEventsCommandInput, StackEvent } from '@aws-sdk/client-cloudformation'; -import { SDK } from '../../../../lib'; +import { DescribeStackEventsCommand, DescribeStackEventsCommandInput, StackEvent } from '@aws-sdk/client-cloudformation'; import { StackEventPoller } from '../../../../lib/api/util/cloudformation/stack-event-poller'; +import { MockSdk, mockCloudFormationClient } from '../../../util/mock-sdk'; beforeEach(() => { jest.resetAllMocks(); @@ -8,30 +8,67 @@ beforeEach(() => { describe('poll', () => { - test('does not poll unnecessary pages', async () => { + test('polls all necessary pages', async () => { - const now = Date.now(); + const deployTime = Date.now(); - const pastEvent: StackEvent = { - Timestamp: new Date(now - 1000), + const postDeployEvent1: StackEvent = { + Timestamp: new Date(deployTime + 1000), EventId: 'event-1', StackId: 'stack-id', StackName: 'stack', }; - const sdk = new SDK({ accessKeyId: 'accessKeyId', secretAccessKey: 'secretAccessKey' }, 'region', {}); + const postDeployEvent2: StackEvent = { + Timestamp: new Date(deployTime + 2000), + EventId: 'event-2', + StackId: 'stack-id', + StackName: 'stack', + }; + + const sdk = new MockSdk(); + mockCloudFormationClient.on(DescribeStackEventsCommand).callsFake((input: DescribeStackEventsCommandInput) => { + const result = { + StackEvents: input.NextToken ? [postDeployEvent2] : [postDeployEvent1], + NextToken: input.NextToken ? undefined : 'token', // simulate a two page event stream. + }; + + return result; + }); + + const poller = new StackEventPoller(sdk.cloudFormation(), { + stackName: 'stack', + startTime: new Date().getTime(), + }); - jest.spyOn(CloudFormationClient.prototype, 'send').mockImplementation((command) => { - const input: DescribeStackEventsCommandInput = command.input as DescribeStackEventsCommandInput; + const events = await poller.poll(); + expect(events.length).toEqual(2); + + }); + + test('does not poll unnecessary pages', async () => { + + const deployTime = Date.now(); + + const preDeployTimeEvent: StackEvent = { + Timestamp: new Date(deployTime - 1000), + EventId: 'event-1', + StackId: 'stack-id', + StackName: 'stack', + }; + + const sdk = new MockSdk(); + mockCloudFormationClient.on(DescribeStackEventsCommand).callsFake((input: DescribeStackEventsCommandInput) => { // the first event we return should stop the polling. we therefore // do not expect a second page to be polled. expect(input.NextToken).toBe(undefined); return { - StackEvents: [pastEvent], - NextToken: input.NextToken === 'token' ? undefined : 'token', // simulate a two page event stream. + StackEvents: [preDeployTimeEvent], + NextToken: input.NextToken ? undefined : 'token', // simulate a two page event stream. }; + }); const poller = new StackEventPoller(sdk.cloudFormation(), { From f459d6ac86e133ad535ba56f699e6e491b154510 Mon Sep 17 00:00:00 2001 From: epolon Date: Wed, 20 Nov 2024 08:20:22 +0200 Subject: [PATCH 6/9] exclude coverage --- .../aws-cdk/lib/api/util/cloudformation/stack-event-poller.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/aws-cdk/lib/api/util/cloudformation/stack-event-poller.ts b/packages/aws-cdk/lib/api/util/cloudformation/stack-event-poller.ts index 99fe847664cc4..426bc79ce66d7 100644 --- a/packages/aws-cdk/lib/api/util/cloudformation/stack-event-poller.ts +++ b/packages/aws-cdk/lib/api/util/cloudformation/stack-event-poller.ts @@ -107,6 +107,7 @@ export class StackEventPoller { // The events for the stack itself are also included next to events about resources; we can test for them in this way. const isParentStackEvent = event.PhysicalResourceId === event.StackId; + /* istanbul ignore next */ if (isParentStackEvent && this.props.stackStatuses?.includes(event.ResourceStatus ?? '')) { return events; } @@ -128,6 +129,7 @@ export class StackEventPoller { this.trackNestedStack(event, [...(this.props.parentStackLogicalIds ?? []), event.LogicalResourceId ?? '']); } + /* istanbul ignore next */ if (isParentStackEvent && isStackTerminalState(event.ResourceStatus)) { this.complete = true; } From d9b6dbab34778619d1121e6eac48eacf4a3183a5 Mon Sep 17 00:00:00 2001 From: epolon Date: Wed, 20 Nov 2024 08:22:33 +0200 Subject: [PATCH 7/9] exclude coverage --- .../aws-cdk/lib/api/util/cloudformation/stack-event-poller.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/aws-cdk/lib/api/util/cloudformation/stack-event-poller.ts b/packages/aws-cdk/lib/api/util/cloudformation/stack-event-poller.ts index 426bc79ce66d7..702a9cd8e8edd 100644 --- a/packages/aws-cdk/lib/api/util/cloudformation/stack-event-poller.ts +++ b/packages/aws-cdk/lib/api/util/cloudformation/stack-event-poller.ts @@ -107,8 +107,8 @@ export class StackEventPoller { // The events for the stack itself are also included next to events about resources; we can test for them in this way. const isParentStackEvent = event.PhysicalResourceId === event.StackId; - /* istanbul ignore next */ if (isParentStackEvent && this.props.stackStatuses?.includes(event.ResourceStatus ?? '')) { + /* istanbul ignore next */ return events; } @@ -129,8 +129,8 @@ export class StackEventPoller { this.trackNestedStack(event, [...(this.props.parentStackLogicalIds ?? []), event.LogicalResourceId ?? '']); } - /* istanbul ignore next */ if (isParentStackEvent && isStackTerminalState(event.ResourceStatus)) { + /* istanbul ignore next */ this.complete = true; } } From 78fe63a4bed65af2d48dfae595935c4929b747e7 Mon Sep 17 00:00:00 2001 From: epolon Date: Wed, 20 Nov 2024 08:49:33 +0200 Subject: [PATCH 8/9] exclude coverage --- .../lib/api/util/cloudformation/stack-event-poller.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/aws-cdk/lib/api/util/cloudformation/stack-event-poller.ts b/packages/aws-cdk/lib/api/util/cloudformation/stack-event-poller.ts index 702a9cd8e8edd..8ff1a728d4cdf 100644 --- a/packages/aws-cdk/lib/api/util/cloudformation/stack-event-poller.ts +++ b/packages/aws-cdk/lib/api/util/cloudformation/stack-event-poller.ts @@ -107,8 +107,8 @@ export class StackEventPoller { // The events for the stack itself are also included next to events about resources; we can test for them in this way. const isParentStackEvent = event.PhysicalResourceId === event.StackId; + /* istanbul ignore next */ if (isParentStackEvent && this.props.stackStatuses?.includes(event.ResourceStatus ?? '')) { - /* istanbul ignore next */ return events; } @@ -125,17 +125,19 @@ export class StackEventPoller { event.ResourceType === 'AWS::CloudFormation::Stack' && isStackBeginOperationState(event.ResourceStatus) ) { + /* istanbul ignore next */ // If the event is not for `this` stack and has a physical resource Id, recursively call for events in the nested stack this.trackNestedStack(event, [...(this.props.parentStackLogicalIds ?? []), event.LogicalResourceId ?? '']); } + /* istanbul ignore next */ if (isParentStackEvent && isStackTerminalState(event.ResourceStatus)) { - /* istanbul ignore next */ this.complete = true; } } } } catch (e: any) { + /* istanbul ignore next */ if (!(e.name === 'ValidationError' && e.message === `Stack [${this.props.stackName}] does not exist`)) { throw e; } From 850b1505e9c12fe0452910e0a7e83bc0a9810548 Mon Sep 17 00:00:00 2001 From: epolon Date: Wed, 20 Nov 2024 12:59:02 +0200 Subject: [PATCH 9/9] mid work --- packages/aws-cdk/lib/api/aws-auth/sdk.ts | 10 ++++---- .../util/cloudformation/stack-event-poller.ts | 23 +++++++++++-------- .../cloudformation/stack-event-poller.test.ts | 6 ++--- 3 files changed, 21 insertions(+), 18 deletions(-) diff --git a/packages/aws-cdk/lib/api/aws-auth/sdk.ts b/packages/aws-cdk/lib/api/aws-auth/sdk.ts index 9b38cf5ef8452..5dc345226824f 100644 --- a/packages/aws-cdk/lib/api/aws-auth/sdk.ts +++ b/packages/aws-cdk/lib/api/aws-auth/sdk.ts @@ -51,6 +51,7 @@ import { DescribeResourceScanCommand, type DescribeResourceScanCommandInput, type DescribeResourceScanCommandOutput, + DescribeStackEventsCommand, type DescribeStackEventsCommandInput, DescribeStackEventsCommandOutput, DescribeStackResourcesCommand, @@ -87,7 +88,6 @@ import { ListStacksCommand, ListStacksCommandInput, ListStacksCommandOutput, - paginateDescribeStackEvents, paginateListStackResources, RollbackStackCommand, RollbackStackCommandInput, @@ -311,7 +311,7 @@ import { GetCallerIdentityCommand, STSClient } from '@aws-sdk/client-sts'; import { Upload } from '@aws-sdk/lib-storage'; import { getEndpointFromInstructions } from '@smithy/middleware-endpoint'; import type { NodeHttpHandlerOptions } from '@smithy/node-http-handler'; -import { AwsCredentialIdentity, Logger, Paginator } from '@smithy/types'; +import { AwsCredentialIdentity, Logger } from '@smithy/types'; import { ConfiguredRetryStrategy } from '@smithy/util-retry'; import { WaiterResult } from '@smithy/util-waiter'; import { AccountAccessKeyCache } from './account-cache'; @@ -404,7 +404,7 @@ export interface ICloudFormationClient { input: UpdateTerminationProtectionCommandInput, ): Promise; // Pagination functions - describeStackEventsPaginated(input: DescribeStackEventsCommandInput): Paginator; + describeStackEvents(input: DescribeStackEventsCommandInput): Promise; listStackResources(input: ListStackResourcesCommandInput): Promise; } @@ -664,8 +664,8 @@ export class SDK { input: UpdateTerminationProtectionCommandInput, ): Promise => client.send(new UpdateTerminationProtectionCommand(input)), - describeStackEventsPaginated: (input: DescribeStackEventsCommandInput): Paginator => { - return paginateDescribeStackEvents({ client }, input); + describeStackEvents: (input: DescribeStackEventsCommandInput): Promise => { + return client.send(new DescribeStackEventsCommand(input)); }, listStackResources: async (input: ListStackResourcesCommandInput): Promise => { const stackResources = Array(); diff --git a/packages/aws-cdk/lib/api/util/cloudformation/stack-event-poller.ts b/packages/aws-cdk/lib/api/util/cloudformation/stack-event-poller.ts index 8ff1a728d4cdf..efc66da8ef3b0 100644 --- a/packages/aws-cdk/lib/api/util/cloudformation/stack-event-poller.ts +++ b/packages/aws-cdk/lib/api/util/cloudformation/stack-event-poller.ts @@ -88,10 +88,11 @@ export class StackEventPoller { private async doPoll(): Promise { const events: ResourceEvent[] = []; try { - const paginator = this.cfn.describeStackEventsPaginated({ - StackName: this.props.stackName, - }); - for await (const page of paginator) { + let nextToken: string | undefined; + let finished = false; + + while (!finished) { + const page = await this.cfn.describeStackEvents({ StackName: this.props.stackName, NextToken: nextToken }); for (const event of page?.StackEvents ?? []) { // Event from before we were interested in 'em if (this.props.startTime !== undefined && event.Timestamp!.valueOf() < this.props.startTime) { @@ -107,7 +108,6 @@ export class StackEventPoller { // The events for the stack itself are also included next to events about resources; we can test for them in this way. const isParentStackEvent = event.PhysicalResourceId === event.StackId; - /* istanbul ignore next */ if (isParentStackEvent && this.props.stackStatuses?.includes(event.ResourceStatus ?? '')) { return events; } @@ -122,22 +122,25 @@ export class StackEventPoller { if ( !isParentStackEvent && - event.ResourceType === 'AWS::CloudFormation::Stack' && - isStackBeginOperationState(event.ResourceStatus) + event.ResourceType === 'AWS::CloudFormation::Stack' && + isStackBeginOperationState(event.ResourceStatus) ) { - /* istanbul ignore next */ // If the event is not for `this` stack and has a physical resource Id, recursively call for events in the nested stack this.trackNestedStack(event, [...(this.props.parentStackLogicalIds ?? []), event.LogicalResourceId ?? '']); } - /* istanbul ignore next */ if (isParentStackEvent && isStackTerminalState(event.ResourceStatus)) { this.complete = true; } } + + nextToken = page?.NextToken; + if (nextToken === undefined) { + finished = true; + } + } } catch (e: any) { - /* istanbul ignore next */ if (!(e.name === 'ValidationError' && e.message === `Stack [${this.props.stackName}] does not exist`)) { throw e; } diff --git a/packages/aws-cdk/test/api/util/cloudformation/stack-event-poller.test.ts b/packages/aws-cdk/test/api/util/cloudformation/stack-event-poller.test.ts index 1a93e3627c266..6ff65e4eba58f 100644 --- a/packages/aws-cdk/test/api/util/cloudformation/stack-event-poller.test.ts +++ b/packages/aws-cdk/test/api/util/cloudformation/stack-event-poller.test.ts @@ -29,8 +29,8 @@ describe('poll', () => { const sdk = new MockSdk(); mockCloudFormationClient.on(DescribeStackEventsCommand).callsFake((input: DescribeStackEventsCommandInput) => { const result = { - StackEvents: input.NextToken ? [postDeployEvent2] : [postDeployEvent1], - NextToken: input.NextToken ? undefined : 'token', // simulate a two page event stream. + StackEvents: input.NextToken === 'token' ? [postDeployEvent2] : [postDeployEvent1], + NextToken: input.NextToken === 'token' ? undefined : 'token', // simulate a two page event stream. }; return result; @@ -66,7 +66,7 @@ describe('poll', () => { return { StackEvents: [preDeployTimeEvent], - NextToken: input.NextToken ? undefined : 'token', // simulate a two page event stream. + NextToken: input.NextToken === 'token' ? undefined : 'token', // simulate a two page event stream. }; });