Skip to content

Commit 4d2b9d7

Browse files
authored
Merge branch 'main' into iankhou-rds-test-refactor
2 parents 9450eb8 + ac90399 commit 4d2b9d7

344 files changed

Lines changed: 580 additions & 828 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

packages/@aws-cdk-testing/cli-integ/lib/aws.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,6 @@ interface ClientConfig {
2929
}
3030

3131
export class AwsClients {
32-
3332
public static async forRegion(region: string, output: NodeJS.WritableStream) {
3433
return new AwsClients(region, output);
3534
}

packages/@aws-cdk-testing/cli-integ/lib/package-sources/release-source.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,12 @@ import { shell, rimraf, addToShellPath } from '../shell';
77

88
export class ReleasePackageSourceSetup implements IPackageSourceSetup {
99
readonly name = 'release';
10-
readonly description = `release @ ${this.version}`;
10+
readonly description: string;
1111

1212
private tempDir?: string;
1313

1414
constructor(private readonly version: string, private readonly frameworkVersion?: string) {
15+
this.description = `release @ ${this.version}`;
1516
}
1617

1718
public async prepare(): Promise<void> {

packages/@aws-cdk-testing/cli-integ/lib/package-sources/repo-source.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,10 @@ import { shell, addToShellPath } from '../shell';
77

88
export class RepoPackageSourceSetup implements IPackageSourceSetup {
99
readonly name = 'repo';
10-
readonly description = `repo(${this.repoRoot})`;
10+
readonly description: string;
1111

1212
constructor(private readonly repoRoot: string) {
13+
this.description = `repo(${this.repoRoot})`;
1314
}
1415

1516
public async prepare(): Promise<void> {
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
import { promises as fs } from 'fs';
2+
import * as querystring from 'node:querystring';
3+
import * as os from 'os';
4+
import * as path from 'path';
5+
import * as mockttp from 'mockttp';
6+
import { CompletedRequest } from 'mockttp';
7+
8+
export async function startProxyServer(certDirRoot?: string): Promise<ProxyServer> {
9+
const certDir = await fs.mkdtemp(path.join(certDirRoot ?? os.tmpdir(), 'cdk-'));
10+
const certPath = path.join(certDir, 'cert.pem');
11+
const keyPath = path.join(certDir, 'key.pem');
12+
13+
// Set up key and certificate
14+
const { key, cert } = await mockttp.generateCACertificate();
15+
await fs.writeFile(keyPath, key);
16+
await fs.writeFile(certPath, cert);
17+
18+
const server = mockttp.getLocal({
19+
https: { keyPath: keyPath, certPath: certPath },
20+
});
21+
22+
// We don't need to modify any request, so the proxy
23+
// passes through all requests to the target host.
24+
const endpoint = await server
25+
.forAnyRequest()
26+
.thenPassThrough();
27+
28+
server.enableDebug();
29+
await server.start();
30+
31+
return {
32+
certPath,
33+
keyPath,
34+
server,
35+
url: server.url,
36+
port: server.port,
37+
getSeenRequests: () => endpoint.getSeenRequests(),
38+
async stop() {
39+
await server.stop();
40+
await fs.rm(certDir, { recursive: true, force: true });
41+
},
42+
};
43+
}
44+
45+
export interface ProxyServer {
46+
readonly certPath: string;
47+
readonly keyPath: string;
48+
readonly server: mockttp.Mockttp;
49+
readonly url: string;
50+
readonly port: number;
51+
52+
getSeenRequests(): Promise<CompletedRequest[]>;
53+
stop(): Promise<void>;
54+
}
55+
56+
export function awsActionsFromRequests(requests: CompletedRequest[]): string[] {
57+
return [...new Set(requests
58+
.map(req => req.body.buffer.toString('utf-8'))
59+
.map(body => querystring.decode(body))
60+
.map(x => x.Action as string)
61+
.filter(action => action != null))];
62+
}

packages/@aws-cdk-testing/cli-integ/lib/with-cdk-app.ts

Lines changed: 24 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -317,7 +317,7 @@ export interface CdkGarbageCollectionCommandOptions {
317317
}
318318

319319
export class TestFixture extends ShellHelper {
320-
public readonly qualifier = this.randomString.slice(0, 10);
320+
public readonly qualifier: string;
321321
private readonly bucketsToDelete = new Array<string>();
322322
public readonly packages: IPackageSource;
323323

@@ -330,6 +330,7 @@ export class TestFixture extends ShellHelper {
330330

331331
super(integTestDir, output);
332332

333+
this.qualifier = this.randomString.slice(0, 10);
333334
this.packages = packageSourceInSubprocess();
334335
}
335336

@@ -338,16 +339,22 @@ export class TestFixture extends ShellHelper {
338339
}
339340

340341
public async cdkDeploy(stackNames: string | string[], options: CdkCliOptions = {}, skipStackRename?: boolean) {
341-
stackNames = typeof stackNames === 'string' ? [stackNames] : stackNames;
342+
return this.cdk(this.cdkDeployCommandLine(stackNames, options, skipStackRename));
343+
}
342344

345+
public cdkDeployCommandLine(stackNames: string | string[], options: CdkCliOptions = {}, skipStackRename?: boolean) {
346+
stackNames = typeof stackNames === 'string' ? [stackNames] : stackNames;
343347
const neverRequireApproval = options.neverRequireApproval ?? true;
344348

345-
return this.cdk(['deploy',
349+
return [
350+
'deploy',
346351
...(neverRequireApproval ? ['--require-approval=never'] : []), // Default to no approval in an unattended test
347352
...(options.options ?? []),
353+
...(options.verbose ? ['-v'] : []),
348354
// use events because bar renders bad in tests
349355
'--progress', 'events',
350-
...(skipStackRename ? stackNames : this.fullStackName(stackNames))], options);
356+
...(skipStackRename ? stackNames : this.fullStackName(stackNames)),
357+
];
351358
}
352359

353360
public async cdkSynth(options: CdkCliOptions = {}) {
@@ -490,15 +497,24 @@ export class TestFixture extends ShellHelper {
490497
return this.shell(['cdk', ...(verbose ? ['-v'] : []), ...args], {
491498
...options,
492499
modEnv: {
493-
AWS_REGION: this.aws.region,
494-
AWS_DEFAULT_REGION: this.aws.region,
495-
STACK_NAME_PREFIX: this.stackNamePrefix,
496-
PACKAGE_LAYOUT_VERSION: this.packages.majorVersion(),
500+
...this.cdkShellEnv(),
497501
...options.modEnv,
498502
},
499503
});
500504
}
501505

506+
/**
507+
* Return the environment variables with which to execute CDK
508+
*/
509+
public cdkShellEnv() {
510+
return {
511+
AWS_REGION: this.aws.region,
512+
AWS_DEFAULT_REGION: this.aws.region,
513+
STACK_NAME_PREFIX: this.stackNamePrefix,
514+
PACKAGE_LAYOUT_VERSION: this.packages.majorVersion(),
515+
};
516+
}
517+
502518
public template(stackName: string): any {
503519
const fullStackName = this.fullStackName(stackName);
504520
const templatePath = path.join(this.integTestDir, 'cdk.out', `${fullStackName}.template.json`);

packages/@aws-cdk-testing/cli-integ/lib/with-cli-lib.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,5 @@ __EOS__`], {
135135
},
136136
});
137137
}
138-
139138
}
140139

packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cli.integtest.ts

Lines changed: 12 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import { existsSync, promises as fs } from 'fs';
2-
import * as querystring from 'node:querystring';
32
import * as os from 'os';
43
import * as path from 'path';
54
import {
@@ -23,8 +22,6 @@ import { InvokeCommand } from '@aws-sdk/client-lambda';
2322
import { PutObjectLockConfigurationCommand } from '@aws-sdk/client-s3';
2423
import { CreateTopicCommand, DeleteTopicCommand } from '@aws-sdk/client-sns';
2524
import { AssumeRoleCommand, GetCallerIdentityCommand } from '@aws-sdk/client-sts';
26-
import * as mockttp from 'mockttp';
27-
import { CompletedRequest } from 'mockttp';
2825
import {
2926
cloneDirectory,
3027
integTest,
@@ -41,6 +38,7 @@ import {
4138
withSamIntegrationFixture,
4239
withSpecificFixture,
4340
} from '../../lib';
41+
import { awsActionsFromRequests, startProxyServer } from '../../lib/proxy';
4442

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

@@ -2876,60 +2874,29 @@ integTest('cdk notices are displayed correctly', withDefaultFixture(async (fixtu
28762874

28772875
integTest('requests go through a proxy when configured',
28782876
withDefaultFixture(async (fixture) => {
2879-
// Set up key and certificate
2880-
const { key, cert } = await mockttp.generateCACertificate();
2881-
const certDir = await fs.mkdtemp(path.join(os.tmpdir(), 'cdk-'));
2882-
const certPath = path.join(certDir, 'cert.pem');
2883-
const keyPath = path.join(certDir, 'key.pem');
2884-
await fs.writeFile(keyPath, key);
2885-
await fs.writeFile(certPath, cert);
2886-
2887-
const proxyServer = mockttp.getLocal({
2888-
https: { keyPath, certPath },
2889-
});
2890-
2891-
// We don't need to modify any request, so the proxy
2892-
// passes through all requests to the target host.
2893-
const endpoint = await proxyServer
2894-
.forAnyRequest()
2895-
.thenPassThrough();
2896-
2897-
proxyServer.enableDebug();
2898-
await proxyServer.start();
2899-
2900-
// The proxy is now ready to intercept requests
2901-
2877+
const proxyServer = await startProxyServer();
29022878
try {
29032879
await fixture.cdkDeploy('test-2', {
29042880
captureStderr: true,
29052881
options: [
29062882
'--proxy', proxyServer.url,
2907-
'--ca-bundle-path', certPath,
2883+
'--ca-bundle-path', proxyServer.certPath,
29082884
],
29092885
modEnv: {
29102886
CDK_HOME: fixture.integTestDir,
29112887
},
29122888
});
2913-
} finally {
2914-
await fs.rm(certDir, { recursive: true, force: true });
2915-
await proxyServer.stop();
2916-
}
29172889

2918-
const requests = await endpoint.getSeenRequests();
2890+
const requests = await proxyServer.getSeenRequests();
29192891

2920-
expect(requests.map(req => req.url))
2921-
.toContain('https://cli.cdk.dev-tools.aws.dev/notices.json');
2892+
expect(requests.map(req => req.url))
2893+
.toContain('https://cli.cdk.dev-tools.aws.dev/notices.json');
29222894

2923-
const actionsUsed = actions(requests);
2924-
expect(actionsUsed).toContain('AssumeRole');
2925-
expect(actionsUsed).toContain('CreateChangeSet');
2895+
const actionsUsed = awsActionsFromRequests(requests);
2896+
expect(actionsUsed).toContain('AssumeRole');
2897+
expect(actionsUsed).toContain('CreateChangeSet');
2898+
} finally {
2899+
await proxyServer.stop();
2900+
}
29262901
}),
29272902
);
2928-
2929-
function actions(requests: CompletedRequest[]): string[] {
2930-
return [...new Set(requests
2931-
.map(req => req.body.buffer.toString('utf-8'))
2932-
.map(body => querystring.decode(body))
2933-
.map(x => x.Action as string)
2934-
.filter(action => action != null))];
2935-
}
Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
import { promises as fs } from 'fs';
2+
import * as path from 'path';
3+
import { integTest } from '../../lib/integ-test';
4+
import { startProxyServer } from '../../lib/proxy';
5+
import { TestFixture, withDefaultFixture } from '../../lib/with-cdk-app';
6+
7+
const docker = process.env.CDK_DOCKER ?? 'docker';
8+
9+
integTest(
10+
'deploy in isolated container',
11+
withDefaultFixture(async (fixture) => {
12+
// Find the 'cdk' command and make sure it is mounted into the container
13+
const cdkFullpath = (await fixture.shell(['which', 'cdk'])).trim();
14+
const cdkTop = topLevelDirectory(cdkFullpath);
15+
16+
// Run a 'cdk deploy' inside the container
17+
const commands = [
18+
`env ${renderEnv(fixture.cdkShellEnv())} ${cdkFullpath} ${fixture.cdkDeployCommandLine('test-2', { verbose: true }).join(' ')}`,
19+
];
20+
21+
await runInIsolatedContainer(fixture, [cdkTop], commands);
22+
}),
23+
);
24+
25+
async function runInIsolatedContainer(fixture: TestFixture, pathsToMount: string[], testCommands: string[]) {
26+
pathsToMount.push(
27+
`${process.env.HOME}`,
28+
fixture.integTestDir,
29+
);
30+
31+
const proxy = await startProxyServer(fixture.integTestDir);
32+
try {
33+
const proxyPort = proxy.port;
34+
35+
const setupCommands = [
36+
'apt-get update -qq',
37+
'apt-get install -qqy nodejs > /dev/null',
38+
...isolatedDockerCommands(proxyPort, proxy.certPath),
39+
];
40+
41+
const scriptName = path.join(fixture.integTestDir, 'script.sh');
42+
43+
// Write a script file
44+
await fs.writeFile(scriptName, [
45+
'#!/bin/bash',
46+
'set -x',
47+
'set -eu',
48+
...setupCommands,
49+
...testCommands,
50+
].join('\n'), 'utf-8');
51+
52+
await fs.chmod(scriptName, 0o755);
53+
54+
// Run commands in a Docker shell
55+
await fixture.shell([
56+
docker, 'run', '--net=bridge', '--rm',
57+
...pathsToMount.flatMap(p => ['-v', `${p}:${p}`]),
58+
...['HOME', 'AWS_ACCESS_KEY_ID', 'AWS_SECRET_ACCESS_KEY', 'AWS_SESSION_TOKEN'].flatMap(e => ['-e', e]),
59+
'-w', fixture.integTestDir,
60+
'--cap-add=NET_ADMIN',
61+
'ubuntu:latest',
62+
`${scriptName}`,
63+
], {
64+
stdio: 'inherit',
65+
});
66+
} finally {
67+
await proxy.stop();
68+
}
69+
}
70+
71+
function topLevelDirectory(dir: string) {
72+
while (true) {
73+
let parent = path.dirname(dir);
74+
if (parent === '/') {
75+
return dir;
76+
}
77+
dir = parent;
78+
}
79+
}
80+
81+
/**
82+
* Return the commands necessary to isolate the inside of the container from the internet,
83+
* except by going through the proxy
84+
*/
85+
function isolatedDockerCommands(proxyPort: number, caBundlePath: string) {
86+
return [
87+
'echo Working...',
88+
'apt-get install -qqy curl net-tools iputils-ping dnsutils iptables > /dev/null',
89+
'',
90+
'gateway=$(dig +short host.docker.internal)',
91+
'',
92+
'# Some iptables manipulation; there might be unnecessary commands in here, not an expert',
93+
'iptables -F',
94+
'iptables -X',
95+
'iptables -P INPUT DROP',
96+
'iptables -P OUTPUT DROP',
97+
'iptables -P FORWARD DROP',
98+
'iptables -A INPUT -i lo -j ACCEPT',
99+
'iptables -A OUTPUT -o lo -j ACCEPT',
100+
'iptables -A OUTPUT -d $gateway -j ACCEPT',
101+
'iptables -A INPUT -s $gateway -j ACCEPT',
102+
'',
103+
'',
104+
`if [[ ! -f ${caBundlePath} ]]; then`,
105+
` echo "Could not find ${caBundlePath}, this will probably not go well. Exiting." >&2`,
106+
' exit 1',
107+
'fi',
108+
'',
109+
'# Configure a bunch of tools to work with the proxy',
110+
'echo "+-------------------------------------------------------------------------------------+"',
111+
'echo "| Direct network traffic has been blocked, everything must go through the proxy. |"',
112+
'echo "+-------------------------------------------------------------------------------------+"',
113+
`export HTTP_PROXY=http://$gateway:${proxyPort}/`,
114+
`export HTTPS_PROXY=http://$gateway:${proxyPort}/`,
115+
`export NODE_EXTRA_CA_CERTS=${caBundlePath}`,
116+
`export AWS_CA_BUNDLE=${caBundlePath}`,
117+
`export SSL_CERT_FILE=${caBundlePath}`,
118+
'echo "Acquire::http::proxy \"$HTTP_PROXY\";" >> /etc/apt/apt.conf.d/95proxies',
119+
'echo "Acquire::https::proxy \"$HTTPS_PROXY\";" >> /etc/apt/apt.conf.d/95proxies',
120+
];
121+
}
122+
123+
function renderEnv(env: Record<string, string>) {
124+
return Object.entries(env).map(([k, v]) => `${k}='${v}'`).join(' ');
125+
}

0 commit comments

Comments
 (0)