Skip to content

Commit d459e5d

Browse files
committed
fix: CCAPI provider does not do pagination
The CCAPI provider stops after the first page of results. This may cause for example EC2 Prefix Lists that exist to not be found, if they don't occur in the first page of results. Make the provider retrieve all pages of results. Also in this PR: - New SDK mocks had been added without them being added to all the places where they needed to be added to be reset properly. Instead, put them all into an object so we can do a reliable `for` loop that will never go out of date again.
1 parent 6735323 commit d459e5d

File tree

3 files changed

+118
-64
lines changed

3 files changed

+118
-64
lines changed

packages/@aws-cdk/toolkit-lib/lib/context-providers/cc-api-provider.ts

Lines changed: 28 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -101,24 +101,39 @@ export class CcApiContextProviderPlugin implements ContextProviderPlugin {
101101
expectedMatchCount?: CcApiContextQuery['expectedMatchCount'],
102102
): Promise<FoundResource[]> {
103103
try {
104-
const result = await cc.listResources({
105-
TypeName: typeName,
106-
});
107-
const found = (result.ResourceDescriptions ?? [])
108-
.map(foundResourceFromCcApi)
109-
.filter((r) => {
110-
return Object.entries(propertyMatch).every(([propPath, expected]) => {
111-
const actual = findJsonValue(r.properties, propPath);
112-
return propertyMatchesFilter(actual, expected);
113-
});
104+
const found: FoundResource[] = [];
105+
let nextToken: string | undefined = undefined;
106+
107+
do {
108+
const result = await cc.listResources({
109+
TypeName: typeName,
110+
...nextToken ? { NextToken: nextToken } : {},
114111
});
112+
// eslint-disable-next-line no-console
113+
console.log(result);
114+
115+
found.push(
116+
...(result.ResourceDescriptions ?? [])
117+
.map(foundResourceFromCcApi)
118+
.filter((r) => {
119+
return Object.entries(propertyMatch).every(([propPath, expected]) => {
120+
const actual = findJsonValue(r.properties, propPath);
121+
return propertyMatchesFilter(actual, expected);
122+
});
123+
}),
124+
);
125+
126+
nextToken = result.NextToken;
127+
128+
// This allows us to error out early, before we have consumed all pages.
129+
if ((expectedMatchCount === 'at-most-one' || expectedMatchCount === 'exactly-one') && found.length > 1) {
130+
throw new ContextProviderError(`Found ${found.length} resources matching ${JSON.stringify(propertyMatch)}; expected ${expectedMatchCountText(expectedMatchCount)}. Please narrow the search criteria`);
131+
}
132+
} while (nextToken);
115133

116134
if ((expectedMatchCount === 'at-least-one' || expectedMatchCount === 'exactly-one') && found.length === 0) {
117135
throw new NoResultsFoundError(`Could not find any resources matching ${JSON.stringify(propertyMatch)}; expected ${expectedMatchCountText(expectedMatchCount)}.`);
118136
}
119-
if ((expectedMatchCount === 'at-most-one' || expectedMatchCount === 'exactly-one') && found.length > 1) {
120-
throw new ContextProviderError(`Found ${found.length} resources matching ${JSON.stringify(propertyMatch)}; expected ${expectedMatchCountText(expectedMatchCount)}. Please narrow the search criteria`);
121-
}
122137

123138
return found;
124139
} catch (err: any) {

packages/@aws-cdk/toolkit-lib/test/_helpers/mock-sdk.ts

Lines changed: 46 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -36,24 +36,46 @@ export const FAKE_CREDENTIALS: SDKv3CompatibleCredentials = {
3636
export const FAKE_CREDENTIAL_CHAIN = createCredentialChain(() => Promise.resolve(FAKE_CREDENTIALS));
3737

3838
// Default implementations
39-
export const mockAppSyncClient = mockClient(AppSyncClient);
40-
export const mockCloudControlClient = mockClient(CloudControlClient);
41-
export const mockCloudFormationClient = mockClient(CloudFormationClient);
42-
export const mockCloudWatchClient = mockClient(CloudWatchLogsClient);
43-
export const mockCodeBuildClient = mockClient(CodeBuildClient);
44-
export const mockEC2Client = mockClient(EC2Client);
45-
export const mockECRClient = mockClient(ECRClient);
46-
export const mockECSClient = mockClient(ECSClient);
47-
export const mockElasticLoadBalancingV2Client = mockClient(ElasticLoadBalancingV2Client);
48-
export const mockIAMClient = mockClient(IAMClient);
49-
export const mockKMSClient = mockClient(KMSClient);
50-
export const mockLambdaClient = mockClient(LambdaClient);
51-
export const mockRoute53Client = mockClient(Route53Client);
52-
export const mockS3Client = mockClient(S3Client);
53-
export const mockSecretsManagerClient = mockClient(SecretsManagerClient);
54-
export const mockSSMClient = mockClient(SSMClient);
55-
export const mockStepFunctionsClient = mockClient(SFNClient);
56-
export const mockSTSClient = mockClient(STSClient);
39+
export const awsMock = {
40+
appSync: mockClient(AppSyncClient),
41+
cloudControl: mockClient(CloudControlClient),
42+
cloudFormation: mockClient(CloudFormationClient),
43+
cloudWatch: mockClient(CloudWatchLogsClient),
44+
codeBuild: mockClient(CodeBuildClient),
45+
ec2: mockClient(EC2Client),
46+
ecr: mockClient(ECRClient),
47+
ecs: mockClient(ECSClient),
48+
elasticLoadBalancingV2: mockClient(ElasticLoadBalancingV2Client),
49+
iAM: mockClient(IAMClient),
50+
kMS: mockClient(KMSClient),
51+
lambda: mockClient(LambdaClient),
52+
route53: mockClient(Route53Client),
53+
s3: mockClient(S3Client),
54+
sSM: mockClient(SSMClient),
55+
sTS: mockClient(STSClient),
56+
secretsManager: mockClient(SecretsManagerClient),
57+
stepFunctions: mockClient(SFNClient),
58+
};
59+
60+
// Global aliases for the mock clients for backwards compatibility
61+
export const mockAppSyncClient = awsMock.appSync;
62+
export const mockCloudControlClient = awsMock.cloudControl;
63+
export const mockCloudFormationClient = awsMock.cloudFormation;
64+
export const mockCloudWatchClient = awsMock.cloudWatch;
65+
export const mockCodeBuildClient = awsMock.codeBuild;
66+
export const mockEC2Client = awsMock.ec2;
67+
export const mockECRClient = awsMock.ecr;
68+
export const mockECSClient = awsMock.ecs;
69+
export const mockElasticLoadBalancingV2Client = awsMock.elasticLoadBalancingV2;
70+
export const mockIAMClient = awsMock.iAM;
71+
export const mockKMSClient = awsMock.kMS;
72+
export const mockLambdaClient = awsMock.lambda;
73+
export const mockRoute53Client = awsMock.route53;
74+
export const mockS3Client = awsMock.s3;
75+
export const mockSSMClient = awsMock.sSM;
76+
export const mockSTSClient = awsMock.sTS;
77+
export const mockSecretsManagerClient = awsMock.secretsManager;
78+
export const mockStepFunctionsClient = awsMock.stepFunctions;
5779

5880
/**
5981
* Resets clients back to defaults and resets the history
@@ -67,22 +89,9 @@ export const mockSTSClient = mockClient(STSClient);
6789
export const restoreSdkMocksToDefault = () => {
6890
applyToAllMocks('reset');
6991

70-
mockAppSyncClient.onAnyCommand().resolves({});
71-
mockCloudControlClient.onAnyCommand().resolves({});
72-
mockCloudFormationClient.onAnyCommand().resolves({});
73-
mockCloudWatchClient.onAnyCommand().resolves({});
74-
mockCodeBuildClient.onAnyCommand().resolves({});
75-
mockEC2Client.onAnyCommand().resolves({});
76-
mockECRClient.onAnyCommand().resolves({});
77-
mockECSClient.onAnyCommand().resolves({});
78-
mockElasticLoadBalancingV2Client.onAnyCommand().resolves({});
79-
mockIAMClient.onAnyCommand().resolves({});
80-
mockKMSClient.onAnyCommand().resolves({});
81-
mockLambdaClient.onAnyCommand().resolves({});
82-
mockRoute53Client.onAnyCommand().resolves({});
83-
mockS3Client.onAnyCommand().resolves({});
84-
mockSecretsManagerClient.onAnyCommand().resolves({});
85-
mockSSMClient.onAnyCommand().resolves({});
92+
for (const mock of Object.values(awsMock)) {
93+
(mock as any).onAnyCommand().resolves({});
94+
}
8695
};
8796

8897
/**
@@ -102,23 +111,9 @@ export function undoAllSdkMocks() {
102111
}
103112

104113
function applyToAllMocks(meth: 'reset' | 'restore') {
105-
mockAppSyncClient[meth]();
106-
mockCloudFormationClient[meth]();
107-
mockCloudWatchClient[meth]();
108-
mockCodeBuildClient[meth]();
109-
mockEC2Client[meth]();
110-
mockECRClient[meth]();
111-
mockECSClient[meth]();
112-
mockElasticLoadBalancingV2Client[meth]();
113-
mockIAMClient[meth]();
114-
mockKMSClient[meth]();
115-
mockLambdaClient[meth]();
116-
mockRoute53Client[meth]();
117-
mockS3Client[meth]();
118-
mockSecretsManagerClient[meth]();
119-
mockSSMClient[meth]();
120-
mockStepFunctionsClient[meth]();
121-
mockSTSClient[meth]();
114+
for (const mock of Object.values(awsMock)) {
115+
mock[meth]();
116+
}
122117
}
123118

124119
export const setDefaultSTSMocks = () => {

packages/@aws-cdk/toolkit-lib/test/context-providers/cc-api-provider.test.ts

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -290,6 +290,50 @@ test('error by specifying both exactIdentifier and propertyMatch', async () => {
290290
).rejects.toThrow('specify either exactIdentifier or propertyMatch, but not both'); // THEN
291291
});
292292

293+
294+
test('CCAPI provider paginates results of listResources', async () => {
295+
// GIVEN
296+
mockCloudControlClient.on(ListResourcesCommand)
297+
.callsFake((input) => {
298+
switch (input.NextToken) {
299+
case undefined:
300+
return {
301+
ResourceDescriptions: [
302+
{ Identifier: 'pl-xxxx', Properties: '{"PrefixListName":"name1","PrefixListId":"pl-xxxx","OwnerId":"123456789012"}' },
303+
{ Identifier: 'pl-yyyy', Properties: '{"PrefixListName":"name1","PrefixListId":"pl-yyyy","OwnerId":"234567890123"}' },
304+
],
305+
NextToken: 'next-token',
306+
};
307+
case 'next-token':
308+
return {
309+
ResourceDescriptions: [
310+
{ Identifier: 'pl-zzzz', Properties: '{"PrefixListName":"name2","PrefixListId":"pl-zzzz","OwnerId":"123456789012"}' },
311+
],
312+
};
313+
default:
314+
throw new Error('Unrecognized token');
315+
}
316+
});
317+
318+
// WHEN
319+
await expect(
320+
// WHEN
321+
provider.getValue({
322+
account: '123456789012',
323+
region: 'us-east-1',
324+
typeName: 'AWS::EC2::PrefixList',
325+
propertiesToReturn: ['PrefixListId'],
326+
propertyMatch: {},
327+
}),
328+
).resolves.toEqual([
329+
{ PrefixListId: 'pl-xxxx', Identifier: 'pl-xxxx' },
330+
{ PrefixListId: 'pl-yyyy', Identifier: 'pl-yyyy' },
331+
{ PrefixListId: 'pl-zzzz', Identifier: 'pl-zzzz' },
332+
]);
333+
334+
expect(mockCloudControlClient).toHaveReceivedCommandTimes(ListResourcesCommand, 2);
335+
});
336+
293337
test('error by specifying neither exactIdentifier or propertyMatch', async () => {
294338
// GIVEN
295339
mockCloudControlClient.on(GetResourceCommand).resolves({

0 commit comments

Comments
 (0)