Skip to content

Commit

Permalink
chore: update cli integ tests to use sdk v3 (#31226)
Browse files Browse the repository at this point in the history
### Checklist
- [ ] My code adheres to the [CONTRIBUTING GUIDE](https://github.com/aws/aws-cdk/blob/main/CONTRIBUTING.md) and [DESIGN GUIDELINES](https://github.com/aws/aws-cdk/blob/main/docs/DESIGN_GUIDELINES.md)

----

*By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license*
  • Loading branch information
TheRealAmazonKendra authored Aug 29, 2024
1 parent 39492e9 commit 2a27711
Show file tree
Hide file tree
Showing 8 changed files with 1,965 additions and 522 deletions.
307 changes: 135 additions & 172 deletions packages/@aws-cdk-testing/cli-integ/lib/aws.ts

Large diffs are not rendered by default.

277 changes: 191 additions & 86 deletions packages/@aws-cdk-testing/cli-integ/lib/staging/codeartifact.ts

Large diffs are not rendered by default.

27 changes: 20 additions & 7 deletions packages/@aws-cdk-testing/cli-integ/lib/with-cdk-app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
import * as fs from 'fs';
import * as os from 'os';
import * as path from 'path';
import { DescribeStacksCommand, Stack } from '@aws-sdk/client-cloudformation';
import { outputFromStack, AwsClients } from './aws';
import { TestContext } from './integ-test';
import { findYarnPackages } from './package-sources/repo-source';
Expand Down Expand Up @@ -511,7 +512,14 @@ export class TestFixture extends ShellHelper {
const imageRepositoryNames = stacksToDelete.map(stack => outputFromStack('ImageRepositoryName', stack)).filter(defined);
await Promise.all(imageRepositoryNames.map(r => this.aws.deleteImageRepository(r)));

await this.aws.deleteStacks(...stacksToDelete.map(s => s.StackName));
await this.aws.deleteStacks(
...stacksToDelete.map((s) => {
if (!s.StackName) {
throw new Error('Stack name is required to delete a stack.');
}
return s.StackName;
}),
);

// We might have leaked some buckets by upgrading the bootstrap stack. Be
// sure to clean everything.
Expand All @@ -529,7 +537,7 @@ export class TestFixture extends ShellHelper {
/**
* Return the stacks starting with our testing prefix that should be deleted
*/
private async deleteableStacks(prefix: string): Promise<AWS.CloudFormation.Stack[]> {
private async deleteableStacks(prefix: string): Promise<Stack[]> {
const statusFilter = [
'CREATE_IN_PROGRESS', 'CREATE_FAILED', 'CREATE_COMPLETE',
'ROLLBACK_IN_PROGRESS', 'ROLLBACK_FAILED', 'ROLLBACK_COMPLETE',
Expand All @@ -544,16 +552,21 @@ export class TestFixture extends ShellHelper {
'IMPORT_ROLLBACK_COMPLETE',
];

const response = await this.aws.cloudFormation('describeStacks', {});
const response = await this.aws.cloudFormation.send(new DescribeStacksCommand({}));

return (response.Stacks ?? [])
.filter(s => s.StackName.startsWith(prefix))
.filter(s => statusFilter.includes(s.StackStatus))
.filter(s => s.RootId === undefined); // Only delete parent stacks. Nested stacks are deleted in the process
.filter((s) => s.StackName && s.StackName.startsWith(prefix))
.filter((s) => s.StackStatus && statusFilter.includes(s.StackStatus))
.filter((s) => s.RootId === undefined); // Only delete parent stacks. Nested stacks are deleted in the process
}

private sortBootstrapStacksToTheEnd(stacks: AWS.CloudFormation.Stack[]) {
private sortBootstrapStacksToTheEnd(stacks: Stack[]) {
stacks.sort((a, b) => {

if (!a.StackName || !b.StackName) {
throw new Error('Stack names do not exists. These are required for sorting the bootstrap stacks.');
}

const aBs = a.StackName.startsWith(this.bootstrapStackName);
const bBs = b.StackName.startsWith(this.bootstrapStackName);

Expand Down
14 changes: 13 additions & 1 deletion packages/@aws-cdk-testing/cli-integ/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,19 @@
},
"dependencies": {
"@octokit/rest": "^18.12.0",
"aws-sdk": "^2.1653.0",
"@aws-sdk/client-codeartifact": "3.637.0",
"@aws-sdk/client-cloudformation": "3.637.0",
"@aws-sdk/client-ecr": "3.637.0",
"@aws-sdk/client-ecs": "3.637.0",
"@aws-sdk/client-iam": "3.637.0",
"@aws-sdk/client-lambda": "3.637.0",
"@aws-sdk/client-s3": "3.637.0",
"@aws-sdk/client-sns": "3.637.0",
"@aws-sdk/client-sso": "3.637.0",
"@aws-sdk/client-sts": "3.637.0",
"@aws-sdk/credential-providers": "3.637.0",
"@smithy/util-retry": "3.0.3",
"@smithy/types": "3.3.0",
"axios": "^1.7.2",
"chalk": "^4",
"fs-extra": "^9.1.0",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
/* eslint-disable @aws-cdk/no-literal-partition */
import * as fs from 'fs';
import * as path from 'path';
import { DescribeStackResourcesCommand, DescribeStacksCommand } from '@aws-sdk/client-cloudformation';
import { DescribeRepositoriesCommand } from '@aws-sdk/client-ecr';
import { CreatePolicyCommand, DeletePolicyCommand, GetRoleCommand } from '@aws-sdk/client-iam';
import * as yaml from 'yaml';
import { integTest, randomString, withoutBootstrap } from '../../lib';
import eventually from '../../lib/eventually';
Expand All @@ -15,9 +18,11 @@ integTest('can bootstrap without execution', withoutBootstrap(async (fixture) =>
noExecute: true,
});

const resp = await fixture.aws.cloudFormation('describeStacks', {
StackName: bootstrapStackName,
});
const resp = await fixture.aws.cloudFormation.send(
new DescribeStacksCommand({
StackName: bootstrapStackName,
}),
);

expect(resp.Stacks?.[0].StackStatus).toEqual('REVIEW_IN_PROGRESS');
}));
Expand Down Expand Up @@ -145,7 +150,7 @@ integTest('can create a legacy bootstrap stack with --public-access-block-config
tags: 'Foo=Bar',
});

const response = await fixture.aws.cloudFormation('describeStacks', { StackName: bootstrapStackName });
const response = await fixture.aws.cloudFormation.send(new DescribeStacksCommand({ StackName: bootstrapStackName }));
expect(response.Stacks?.[0].Tags).toEqual([
{ Key: 'Foo', Value: 'Bar' },
]);
Expand All @@ -167,7 +172,7 @@ integTest('can create multiple legacy bootstrap stacks', withoutBootstrap(async
toolkitStackName: bootstrapStackName2,
});

const response = await fixture.aws.cloudFormation('describeStacks', { StackName: bootstrapStackName1 });
const response = await fixture.aws.cloudFormation.send(new DescribeStacksCommand({ StackName: bootstrapStackName1 }));
expect(response.Stacks?.[0].Tags).toEqual([
{ Key: 'Foo', Value: 'Bar' },
]);
Expand Down Expand Up @@ -272,17 +277,19 @@ integTest('can remove customPermissionsBoundary', withoutBootstrap(async (fixtur
const policyName = `${bootstrapStackName}-pb`;
let policyArn;
try {
const policy = await fixture.aws.iam('createPolicy', {
PolicyName: policyName,
PolicyDocument: JSON.stringify({
Version: '2012-10-17',
Statement: {
Action: ['*'],
Resource: ['*'],
Effect: 'Allow',
},
const policy = await fixture.aws.iam.send(
new CreatePolicyCommand({
PolicyName: policyName,
PolicyDocument: JSON.stringify({
Version: '2012-10-17',
Statement: {
Action: ['*'],
Resource: ['*'],
Effect: 'Allow',
},
}),
}),
});
);
policyArn = policy.Policy?.Arn;

// Policy creation and consistency across regions is "almost immediate"
Expand All @@ -295,7 +302,9 @@ integTest('can remove customPermissionsBoundary', withoutBootstrap(async (fixtur
customPermissionsBoundary: policyName,
});

const response = await fixture.aws.cloudFormation('describeStacks', { StackName: bootstrapStackName });
const response = await fixture.aws.cloudFormation.send(
new DescribeStacksCommand({ StackName: bootstrapStackName }),
);
expect(
response.Stacks?.[0].Parameters?.some(
param => (param.ParameterKey === 'InputPermissionsBoundary' && param.ParameterValue === policyName),
Expand All @@ -309,20 +318,27 @@ integTest('can remove customPermissionsBoundary', withoutBootstrap(async (fixtur
toolkitStackName: bootstrapStackName,
usePreviousParameters: false,
});
const response2 = await fixture.aws.cloudFormation('describeStacks', { StackName: bootstrapStackName });
const response2 = await fixture.aws.cloudFormation.send(
new DescribeStacksCommand({ StackName: bootstrapStackName }),
);
expect(
response2.Stacks?.[0].Parameters?.some(
param => (param.ParameterKey === 'InputPermissionsBoundary' && !param.ParameterValue),
)).toEqual(true);

const region = fixture.aws.region;
const account = await fixture.aws.account();
const role = await fixture.aws.iam('getRole', { RoleName: `cdk-${fixture.qualifier}-cfn-exec-role-${account}-${region}` });
const role = await fixture.aws.iam.send(
new GetRoleCommand({ RoleName: `cdk-${fixture.qualifier}-cfn-exec-role-${account}-${region}` }),
);
if (!role.Role) {
throw new Error('Role not found');
}
expect(role.Role.PermissionsBoundary).toBeUndefined();

} finally {
if (policyArn) {
await fixture.aws.iam('deletePolicy', { PolicyArn: policyArn });
await fixture.aws.iam.send(new DeletePolicyCommand({ PolicyArn: policyArn }));
}
}
}));
Expand All @@ -342,7 +358,7 @@ integTest('switch on termination protection, switch is left alone on re-bootstra
force: true,
});

const response = await fixture.aws.cloudFormation('describeStacks', { StackName: bootstrapStackName });
const response = await fixture.aws.cloudFormation.send(new DescribeStacksCommand({ StackName: bootstrapStackName }));
expect(response.Stacks?.[0].EnableTerminationProtection).toEqual(true);
}));

Expand All @@ -361,7 +377,7 @@ integTest('add tags, left alone on re-bootstrap', withoutBootstrap(async (fixtur
force: true,
});

const response = await fixture.aws.cloudFormation('describeStacks', { StackName: bootstrapStackName });
const response = await fixture.aws.cloudFormation.send(new DescribeStacksCommand({ StackName: bootstrapStackName }));
expect(response.Stacks?.[0].Tags).toEqual([
{ Key: 'Foo', Value: 'Bar' },
]);
Expand All @@ -384,7 +400,7 @@ integTest('can add tags then update tags during re-bootstrap', withoutBootstrap(
force: true,
});

const response = await fixture.aws.cloudFormation('describeStacks', { StackName: bootstrapStackName });
const response = await fixture.aws.cloudFormation.send(new DescribeStacksCommand({ StackName: bootstrapStackName }));
expect(response.Stacks?.[0].Tags).toEqual([
{ Key: 'Foo', Value: 'BarBaz' },
]);
Expand Down Expand Up @@ -418,18 +434,22 @@ integTest('create ECR with tag IMMUTABILITY to set on', withoutBootstrap(async (
toolkitStackName: bootstrapStackName,
});

const response = await fixture.aws.cloudFormation('describeStackResources', {
StackName: bootstrapStackName,
});
const response = await fixture.aws.cloudFormation.send(
new DescribeStackResourcesCommand({
StackName: bootstrapStackName,
}),
);
const ecrResource = response.StackResources?.find(resource => resource.LogicalResourceId === 'ContainerAssetsRepository');
expect(ecrResource).toBeDefined();

const ecrResponse = await fixture.aws.ecr('describeRepositories', {
repositoryNames: [
// This is set, as otherwise we don't end up here
ecrResource?.PhysicalResourceId ?? '',
],
});
const ecrResponse = await fixture.aws.ecr.send(
new DescribeRepositoriesCommand({
repositoryNames: [
// This is set, as otherwise we don't end up here
ecrResource?.PhysicalResourceId ?? '',
],
}),
);

expect(ecrResponse.repositories?.[0].imageTagMutability).toEqual('IMMUTABLE');
}));
Expand Down
Original file line number Diff line number Diff line change
@@ -1,65 +1,90 @@
import { DescribeStackResourcesCommand, DescribeStacksCommand } from '@aws-sdk/client-cloudformation';
import { integTest, withCliLibFixture } from '../../lib';

jest.setTimeout(2 * 60 * 60_000); // Includes the time to acquire locks, worst-case single-threaded runtime

integTest('cli-lib synth', withCliLibFixture(async (fixture) => {
await fixture.cdk(['synth', fixture.fullStackName('simple-1')]);
expect(fixture.template('simple-1')).toEqual(expect.objectContaining({
// Checking for a small subset is enough as proof that synth worked
Resources: expect.objectContaining({
queue276F7297: expect.objectContaining({
Type: 'AWS::SQS::Queue',
Properties: {
VisibilityTimeout: 300,
},
Metadata: {
'aws:cdk:path': `${fixture.stackNamePrefix}-simple-1/queue/Resource`,
},
integTest(
'cli-lib synth',
withCliLibFixture(async (fixture) => {
await fixture.cdk(['synth', fixture.fullStackName('simple-1')]);
expect(fixture.template('simple-1')).toEqual(
expect.objectContaining({
// Checking for a small subset is enough as proof that synth worked
Resources: expect.objectContaining({
queue276F7297: expect.objectContaining({
Type: 'AWS::SQS::Queue',
Properties: {
VisibilityTimeout: 300,
},
Metadata: {
'aws:cdk:path': `${fixture.stackNamePrefix}-simple-1/queue/Resource`,
},
}),
}),
}),
}),
}));
}));
);
}),
);

integTest('cli-lib list', withCliLibFixture(async (fixture) => {
const listing = await fixture.cdk(['list'], { captureStderr: false });
expect(listing).toContain(fixture.fullStackName('simple-1'));
}));
integTest(
'cli-lib list',
withCliLibFixture(async (fixture) => {
const listing = await fixture.cdk(['list'], { captureStderr: false });
expect(listing).toContain(fixture.fullStackName('simple-1'));
}),
);

integTest('cli-lib deploy', withCliLibFixture(async (fixture) => {
const stackName = fixture.fullStackName('simple-1');
integTest(
'cli-lib deploy',
withCliLibFixture(async (fixture) => {
const stackName = fixture.fullStackName('simple-1');

try {
// deploy the stack
await fixture.cdk(['deploy', stackName], {
neverRequireApproval: true,
});
try {
// deploy the stack
await fixture.cdk(['deploy', stackName], {
neverRequireApproval: true,
});

// verify the number of resources in the stack
const expectedStack = await fixture.aws.cloudFormation('describeStackResources', {
StackName: stackName,
});
expect(expectedStack.StackResources?.length).toEqual(3);
} finally {
// delete the stack
await fixture.cdk(['destroy', stackName], {
captureStderr: false,
});
}
}));
// verify the number of resources in the stack
const expectedStack = await fixture.aws.cloudFormation.send(
new DescribeStackResourcesCommand({
StackName: stackName,
}),
);
expect(expectedStack.StackResources?.length).toEqual(3);
} finally {
// delete the stack
await fixture.cdk(['destroy', stackName], {
captureStderr: false,
});
}
}),
);

integTest('security related changes without a CLI are expected to fail when approval is required', withCliLibFixture(async (fixture) => {
const stdErr = await fixture.cdk(['deploy', fixture.fullStackName('simple-1')], {
onlyStderr: true,
captureStderr: true,
allowErrExit: true,
neverRequireApproval: false,
});
integTest(
'security related changes without a CLI are expected to fail when approval is required',
withCliLibFixture(async (fixture) => {
const stdErr = await fixture.cdk(['deploy', fixture.fullStackName('simple-1')], {
onlyStderr: true,
captureStderr: true,
allowErrExit: true,
neverRequireApproval: false,
});

expect(stdErr).toContain('This deployment will make potentially sensitive changes according to your current security approval level');
expect(stdErr).toContain('Deployment failed: Error: \"--require-approval\" is enabled and stack includes security-sensitive updates');
expect(stdErr).toContain(
'This deployment will make potentially sensitive changes according to your current security approval level',
);
expect(stdErr).toContain(
'Deployment failed: Error: "--require-approval" is enabled and stack includes security-sensitive updates',
);

// Ensure stack was not deployed
await expect(fixture.aws.cloudFormation('describeStacks', {
StackName: fixture.fullStackName('simple-1'),
})).rejects.toThrow('does not exist');
}));
// Ensure stack was not deployed
await expect(
fixture.aws.cloudFormation.send(
new DescribeStacksCommand({
StackName: fixture.fullStackName('simple-1'),
}),
),
).rejects.toThrow('does not exist');
}),
);
Loading

0 comments on commit 2a27711

Please sign in to comment.