Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

chore: update cli integ tests to use sdk v3 #31226

Merged
merged 7 commits into from
Aug 29, 2024
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
Loading