diff --git a/packages/@aws-cdk-testing/cli-integ/lib/package-sources/release-source.ts b/packages/@aws-cdk-testing/cli-integ/lib/package-sources/release-source.ts index ad39f813020df..10fb9d302c7f8 100644 --- a/packages/@aws-cdk-testing/cli-integ/lib/package-sources/release-source.ts +++ b/packages/@aws-cdk-testing/cli-integ/lib/package-sources/release-source.ts @@ -7,12 +7,11 @@ import { shell, rimraf, addToShellPath } from '../shell'; export class ReleasePackageSourceSetup implements IPackageSourceSetup { readonly name = 'release'; - readonly description: string; + readonly description = `release @ ${this.version}`; private tempDir?: string; constructor(private readonly version: string, private readonly frameworkVersion?: string) { - this.description = `release @ ${this.version}`; } public async prepare(): Promise { diff --git a/packages/@aws-cdk-testing/cli-integ/lib/package-sources/repo-source.ts b/packages/@aws-cdk-testing/cli-integ/lib/package-sources/repo-source.ts index 28e7c02239927..e6d3714aa4b64 100644 --- a/packages/@aws-cdk-testing/cli-integ/lib/package-sources/repo-source.ts +++ b/packages/@aws-cdk-testing/cli-integ/lib/package-sources/repo-source.ts @@ -7,10 +7,9 @@ import { shell, addToShellPath } from '../shell'; export class RepoPackageSourceSetup implements IPackageSourceSetup { readonly name = 'repo'; - readonly description: string; + readonly description = `repo(${this.repoRoot})`; constructor(private readonly repoRoot: string) { - this.description = `repo(${this.repoRoot})`; } public async prepare(): Promise { diff --git a/packages/@aws-cdk-testing/cli-integ/lib/proxy.ts b/packages/@aws-cdk-testing/cli-integ/lib/proxy.ts deleted file mode 100644 index 0addbd8cc3231..0000000000000 --- a/packages/@aws-cdk-testing/cli-integ/lib/proxy.ts +++ /dev/null @@ -1,62 +0,0 @@ -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 { - 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; - stop(): Promise; -} - -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))]; -} diff --git a/packages/@aws-cdk-testing/cli-integ/lib/with-cdk-app.ts b/packages/@aws-cdk-testing/cli-integ/lib/with-cdk-app.ts index a8a366a2c91c8..a997f86365068 100644 --- a/packages/@aws-cdk-testing/cli-integ/lib/with-cdk-app.ts +++ b/packages/@aws-cdk-testing/cli-integ/lib/with-cdk-app.ts @@ -321,7 +321,7 @@ export interface CdkGarbageCollectionCommandOptions { } export class TestFixture extends ShellHelper { - public readonly qualifier: string; + public readonly qualifier = this.randomString.slice(0, 10); private readonly bucketsToDelete = new Array(); public readonly packages: IPackageSource; @@ -334,7 +334,6 @@ export class TestFixture extends ShellHelper { super(integTestDir, output); - this.qualifier = this.randomString.slice(0, 10); this.packages = packageSourceInSubprocess(); } @@ -343,22 +342,16 @@ export class TestFixture extends ShellHelper { } public async cdkDeploy(stackNames: string | string[], options: CdkCliOptions = {}, skipStackRename?: boolean) { - 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 [ - 'deploy', + return this.cdk(['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)), - ]; + ...(skipStackRename ? stackNames : this.fullStackName(stackNames))], options); } public async cdkSynth(options: CdkCliOptions = {}) { @@ -508,24 +501,15 @@ export class TestFixture extends ShellHelper { return this.shell(['cdk', ...(verbose ? ['-v'] : []), ...args], { ...options, modEnv: { - ...this.cdkShellEnv(), + AWS_REGION: this.aws.region, + AWS_DEFAULT_REGION: this.aws.region, + STACK_NAME_PREFIX: this.stackNamePrefix, + PACKAGE_LAYOUT_VERSION: this.packages.majorVersion(), ...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`); diff --git a/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cli.integtest.ts b/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cli.integtest.ts index 3f22dda39e4f4..d167f3288263b 100644 --- a/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cli.integtest.ts +++ b/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cli.integtest.ts @@ -1,4 +1,5 @@ import { existsSync, promises as fs } from 'fs'; +import * as querystring from 'node:querystring'; import * as os from 'os'; import * as path from 'path'; import { @@ -22,6 +23,8 @@ 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, @@ -38,7 +41,6 @@ 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 @@ -2875,29 +2877,60 @@ integTest('cdk notices are displayed correctly', withDefaultFixture(async (fixtu integTest('requests go through a proxy when configured', withDefaultFixture(async (fixture) => { - const proxyServer = await startProxyServer(); + // 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 + try { await fixture.cdkDeploy('test-2', { captureStderr: true, options: [ '--proxy', proxyServer.url, - '--ca-bundle-path', proxyServer.certPath, + '--ca-bundle-path', certPath, ], modEnv: { CDK_HOME: fixture.integTestDir, }, }); - - const requests = await proxyServer.getSeenRequests(); - - expect(requests.map(req => req.url)) - .toContain('https://cli.cdk.dev-tools.aws.dev/notices.json'); - - const actionsUsed = awsActionsFromRequests(requests); - expect(actionsUsed).toContain('AssumeRole'); - expect(actionsUsed).toContain('CreateChangeSet'); } finally { + await fs.rm(certDir, { recursive: true, force: true }); await proxyServer.stop(); } + + const requests = await endpoint.getSeenRequests(); + + 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'); }), ); + +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))]; +} diff --git a/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/proxy.integtest.ts b/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/proxy.integtest.ts deleted file mode 100644 index 1b27f6f236456..0000000000000 --- a/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/proxy.integtest.ts +++ /dev/null @@ -1,125 +0,0 @@ -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'; - -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) { - return Object.entries(env).map(([k, v]) => `${k}='${v}'`).join(' '); -} diff --git a/packages/@aws-cdk-testing/cli-integ/tsconfig.json b/packages/@aws-cdk-testing/cli-integ/tsconfig.json index 254f5c9c630d9..f03765e562835 100644 --- a/packages/@aws-cdk-testing/cli-integ/tsconfig.json +++ b/packages/@aws-cdk-testing/cli-integ/tsconfig.json @@ -2,7 +2,7 @@ "compilerOptions": { "target": "ES2020", "module": "commonjs", - "lib": ["ES2020", "dom"], + "lib": ["es2019", "es2020", "dom"], "strict": true, "alwaysStrict": true, "declaration": true,