Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,12 @@ import { shell, rimraf, addToShellPath } from '../shell';

export class ReleasePackageSourceSetup implements IPackageSourceSetup {
readonly name = 'release';
readonly description = `release @ ${this.version}`;
readonly description: string;

private tempDir?: string;

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

public async prepare(): Promise<void> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,10 @@ import { shell, addToShellPath } from '../shell';

export class RepoPackageSourceSetup implements IPackageSourceSetup {
readonly name = 'repo';
readonly description = `repo(${this.repoRoot})`;
readonly description: string;

constructor(private readonly repoRoot: string) {
this.description = `repo(${this.repoRoot})`;
}

public async prepare(): Promise<void> {
Expand Down
62 changes: 62 additions & 0 deletions packages/@aws-cdk-testing/cli-integ/lib/proxy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import { promises as fs } from 'fs';
import * as querystring from 'node:querystring';
import * as os from 'os';
import * as path from 'path';
import * as mockttp from 'mockttp';
import { CompletedRequest } from 'mockttp';

export async function startProxyServer(certDirRoot?: string): Promise<ProxyServer> {
const certDir = await fs.mkdtemp(path.join(certDirRoot ?? os.tmpdir(), 'cdk-'));
const certPath = path.join(certDir, 'cert.pem');
const keyPath = path.join(certDir, 'key.pem');

// Set up key and certificate
const { key, cert } = await mockttp.generateCACertificate();
await fs.writeFile(keyPath, key);
await fs.writeFile(certPath, cert);

const server = mockttp.getLocal({
https: { keyPath: keyPath, certPath: certPath },
});

// We don't need to modify any request, so the proxy
// passes through all requests to the target host.
const endpoint = await server
.forAnyRequest()
.thenPassThrough();

server.enableDebug();
await server.start();

return {
certPath,
keyPath,
server,
url: server.url,
port: server.port,
getSeenRequests: () => endpoint.getSeenRequests(),
async stop() {
await server.stop();
await fs.rm(certDir, { recursive: true, force: true });
},
};
}

export interface ProxyServer {
readonly certPath: string;
readonly keyPath: string;
readonly server: mockttp.Mockttp;
readonly url: string;
readonly port: number;

getSeenRequests(): Promise<CompletedRequest[]>;
stop(): Promise<void>;
}

export function awsActionsFromRequests(requests: CompletedRequest[]): string[] {
return [...new Set(requests
.map(req => req.body.buffer.toString('utf-8'))
.map(body => querystring.decode(body))
.map(x => x.Action as string)
.filter(action => action != null))];
}
32 changes: 24 additions & 8 deletions packages/@aws-cdk-testing/cli-integ/lib/with-cdk-app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -359,7 +359,7 @@ export interface CdkGarbageCollectionCommandOptions {
}

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

Expand All @@ -372,6 +372,7 @@ export class TestFixture extends ShellHelper {

super(integTestDir, output);

this.qualifier = this.randomString.slice(0, 10);
this.packages = packageSourceInSubprocess();
}

Expand All @@ -380,16 +381,22 @@ export class TestFixture extends ShellHelper {
}

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

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

return this.cdk(['deploy',
return [
'deploy',
...(neverRequireApproval ? ['--require-approval=never'] : []), // Default to no approval in an unattended test
...(options.options ?? []),
...(options.verbose ? ['-v'] : []),
// use events because bar renders bad in tests
'--progress', 'events',
...(skipStackRename ? stackNames : this.fullStackName(stackNames))], options);
...(skipStackRename ? stackNames : this.fullStackName(stackNames)),
];
}

public async cdkSynth(options: CdkCliOptions = {}) {
Expand Down Expand Up @@ -532,15 +539,24 @@ export class TestFixture extends ShellHelper {
return this.shell(['cdk', ...(verbose ? ['-v'] : []), ...args], {
...options,
modEnv: {
AWS_REGION: this.aws.region,
AWS_DEFAULT_REGION: this.aws.region,
STACK_NAME_PREFIX: this.stackNamePrefix,
PACKAGE_LAYOUT_VERSION: this.packages.majorVersion(),
...this.cdkShellEnv(),
...options.modEnv,
},
});
}

/**
* Return the environment variables with which to execute CDK
*/
public cdkShellEnv() {
return {
AWS_REGION: this.aws.region,
AWS_DEFAULT_REGION: this.aws.region,
STACK_NAME_PREFIX: this.stackNamePrefix,
PACKAGE_LAYOUT_VERSION: this.packages.majorVersion(),
};
}

public template(stackName: string): any {
const fullStackName = this.fullStackName(stackName);
const templatePath = path.join(this.integTestDir, 'cdk.out', `${fullStackName}.template.json`);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { existsSync, promises as fs } from 'fs';
import * as querystring from 'node:querystring';
import * as os from 'os';
import * as path from 'path';
import {
Expand All @@ -23,8 +22,6 @@ import { InvokeCommand } from '@aws-sdk/client-lambda';
import { PutObjectLockConfigurationCommand } from '@aws-sdk/client-s3';
import { CreateTopicCommand, DeleteTopicCommand } from '@aws-sdk/client-sns';
import { AssumeRoleCommand, GetCallerIdentityCommand } from '@aws-sdk/client-sts';
import * as mockttp from 'mockttp';
import { CompletedRequest } from 'mockttp';
import {
cloneDirectory,
integTest,
Expand All @@ -41,6 +38,7 @@ import {
withSamIntegrationFixture,
withSpecificFixture,
} from '../../lib';
import { awsActionsFromRequests, startProxyServer } from '../../lib/proxy';

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

Expand Down Expand Up @@ -2874,60 +2872,29 @@ integTest('cdk notices are displayed correctly', withDefaultFixture(async (fixtu

integTest('requests go through a proxy when configured',
withDefaultFixture(async (fixture) => {
// Set up key and certificate
const { key, cert } = await mockttp.generateCACertificate();
const certDir = await fs.mkdtemp(path.join(os.tmpdir(), 'cdk-'));
const certPath = path.join(certDir, 'cert.pem');
const keyPath = path.join(certDir, 'key.pem');
await fs.writeFile(keyPath, key);
await fs.writeFile(certPath, cert);

const proxyServer = mockttp.getLocal({
https: { keyPath, certPath },
});

// We don't need to modify any request, so the proxy
// passes through all requests to the target host.
const endpoint = await proxyServer
.forAnyRequest()
.thenPassThrough();

proxyServer.enableDebug();
await proxyServer.start();

// The proxy is now ready to intercept requests

const proxyServer = await startProxyServer();
try {
await fixture.cdkDeploy('test-2', {
captureStderr: true,
options: [
'--proxy', proxyServer.url,
'--ca-bundle-path', certPath,
'--ca-bundle-path', proxyServer.certPath,
],
modEnv: {
CDK_HOME: fixture.integTestDir,
},
});
} finally {
await fs.rm(certDir, { recursive: true, force: true });
await proxyServer.stop();
}

const requests = await endpoint.getSeenRequests();
const requests = await proxyServer.getSeenRequests();

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

const actionsUsed = actions(requests);
expect(actionsUsed).toContain('AssumeRole');
expect(actionsUsed).toContain('CreateChangeSet');
const actionsUsed = awsActionsFromRequests(requests);
expect(actionsUsed).toContain('AssumeRole');
expect(actionsUsed).toContain('CreateChangeSet');
} finally {
await proxyServer.stop();
}
}),
);

function actions(requests: CompletedRequest[]): string[] {
return [...new Set(requests
.map(req => req.body.buffer.toString('utf-8'))
.map(body => querystring.decode(body))
.map(x => x.Action as string)
.filter(action => action != null))];
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
import { promises as fs } from 'fs';
import * as path from 'path';
import { integTest } from '../../lib/integ-test';
import { startProxyServer } from '../../lib/proxy';
import { TestFixture, withDefaultFixture } from '../../lib/with-cdk-app';

const docker = process.env.CDK_DOCKER ?? 'docker';
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I suppose the answer is yes, but does this work in an ECS container? Do we need any special setup?


integTest(
'deploy in isolated container',
withDefaultFixture(async (fixture) => {
// Find the 'cdk' command and make sure it is mounted into the container
const cdkFullpath = (await fixture.shell(['which', 'cdk'])).trim();
const cdkTop = topLevelDirectory(cdkFullpath);

// Run a 'cdk deploy' inside the container
const commands = [
`env ${renderEnv(fixture.cdkShellEnv())} ${cdkFullpath} ${fixture.cdkDeployCommandLine('test-2', { verbose: true }).join(' ')}`,
];

await runInIsolatedContainer(fixture, [cdkTop], commands);
}),
);

async function runInIsolatedContainer(fixture: TestFixture, pathsToMount: string[], testCommands: string[]) {
pathsToMount.push(
`${process.env.HOME}`,
fixture.integTestDir,
);

const proxy = await startProxyServer(fixture.integTestDir);
try {
const proxyPort = proxy.port;

const setupCommands = [
'apt-get update -qq',
'apt-get install -qqy nodejs > /dev/null',
...isolatedDockerCommands(proxyPort, proxy.certPath),
];

const scriptName = path.join(fixture.integTestDir, 'script.sh');

// Write a script file
await fs.writeFile(scriptName, [
'#!/bin/bash',
'set -x',
'set -eu',
...setupCommands,
...testCommands,
].join('\n'), 'utf-8');

await fs.chmod(scriptName, 0o755);

// Run commands in a Docker shell
await fixture.shell([
docker, 'run', '--net=bridge', '--rm',
...pathsToMount.flatMap(p => ['-v', `${p}:${p}`]),
...['HOME', 'AWS_ACCESS_KEY_ID', 'AWS_SECRET_ACCESS_KEY', 'AWS_SESSION_TOKEN'].flatMap(e => ['-e', e]),
'-w', fixture.integTestDir,
'--cap-add=NET_ADMIN',
'ubuntu:latest',
`${scriptName}`,
], {
stdio: 'inherit',
});
} finally {
await proxy.stop();
}
}

function topLevelDirectory(dir: string) {
while (true) {
let parent = path.dirname(dir);
if (parent === '/') {
return dir;
}
dir = parent;
}
}

/**
* Return the commands necessary to isolate the inside of the container from the internet,
* except by going through the proxy
*/
function isolatedDockerCommands(proxyPort: number, caBundlePath: string) {
return [
'echo Working...',
'apt-get install -qqy curl net-tools iputils-ping dnsutils iptables > /dev/null',
'',
'gateway=$(dig +short host.docker.internal)',
'',
'# Some iptables manipulation; there might be unnecessary commands in here, not an expert',
'iptables -F',
'iptables -X',
'iptables -P INPUT DROP',
'iptables -P OUTPUT DROP',
'iptables -P FORWARD DROP',
'iptables -A INPUT -i lo -j ACCEPT',
'iptables -A OUTPUT -o lo -j ACCEPT',
'iptables -A OUTPUT -d $gateway -j ACCEPT',
'iptables -A INPUT -s $gateway -j ACCEPT',
'',
'',
`if [[ ! -f ${caBundlePath} ]]; then`,
` echo "Could not find ${caBundlePath}, this will probably not go well. Exiting." >&2`,
' exit 1',
'fi',
'',
'# Configure a bunch of tools to work with the proxy',
'echo "+-------------------------------------------------------------------------------------+"',
'echo "| Direct network traffic has been blocked, everything must go through the proxy. |"',
'echo "+-------------------------------------------------------------------------------------+"',
`export HTTP_PROXY=http://$gateway:${proxyPort}/`,
`export HTTPS_PROXY=http://$gateway:${proxyPort}/`,
`export NODE_EXTRA_CA_CERTS=${caBundlePath}`,
`export AWS_CA_BUNDLE=${caBundlePath}`,
`export SSL_CERT_FILE=${caBundlePath}`,
'echo "Acquire::http::proxy \"$HTTP_PROXY\";" >> /etc/apt/apt.conf.d/95proxies',
'echo "Acquire::https::proxy \"$HTTPS_PROXY\";" >> /etc/apt/apt.conf.d/95proxies',
];
}

function renderEnv(env: Record<string, string>) {
return Object.entries(env).map(([k, v]) => `${k}='${v}'`).join(' ');
}
2 changes: 1 addition & 1 deletion packages/@aws-cdk-testing/cli-integ/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"compilerOptions": {
"target": "ES2020",
"module": "commonjs",
"lib": ["es2019", "es2020", "dom"],
"lib": ["ES2020", "dom"],
"strict": true,
"alwaysStrict": true,
"declaration": true,
Expand Down
Loading