UNPKG

@aws-cdk-testing/cli-integ

Version:

Integration tests for the AWS CDK CLI

167 lines (147 loc) 5.72 kB
import { promises as fs } from 'fs'; import * as os from 'os'; 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( 'all calls from isolated container go through proxy', withDefaultFixture(async (fixture) => { // Find the 'cdk' command and make sure it is mounted into the container const cdkFullpath = (await fixture.shell(['which', 'cdk'])).trim(); let cdkTop = findMountableParent(cdkFullpath); // Run a 'cdk deploy' inside the container const commands = [ `env ${renderEnv(fixture.cdkShellEnv())} ${cdkFullpath} ${fixture.cdkDeployCommandLine('test-2').join(' ')} -v`, ]; await runInIsolatedContainer(fixture, [cdkTop], commands); }), ); async function runInIsolatedContainer(fixture: TestFixture, pathsToMount: string[], testCommands: string[]) { pathsToMount.push( `${process.env.HOME}`, fixture.integTestDir, ); // Resolve credential provider to an access key that we can pass into the container const credentials = await fixture.aws.credentials(); 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', 'public.ecr.aws/ubuntu/ubuntu:24.04_stable', `${scriptName}`, ], { modEnv: { AWS_ACCESS_KEY_ID: credentials.accessKeyId, AWS_SECRET_ACCESS_KEY: credentials.secretAccessKey, AWS_SESSION_TOKEN: credentials.sessionToken, }, }); } finally { await proxy.stop(); } } /** * 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) { let defaultBridgeIp = '172.17.0.1'; // The host's default IP is different on CodeBuild than it is on other Docker systems. if (process.env.CODEBUILD_BUILD_ARN) { defaultBridgeIp = '172.18.0.1'; } return [ 'echo Working...', 'apt-get install -qqy curl net-tools iputils-ping dnsutils iptables > /dev/null', '', // Looking up `host.docker.internal` is necessary on MacOS Finch and Docker Desktop // on Mac. The magic IP address is necessary on CodeBuild and GitHub Actions. // // I tried `--add-host=host.docker.internal:host-gateway` on the Linuxes but // it doesn't change anything on either GHA or CodeBuild, so we're left with this // backup solution. 'gateway=$(dig +short host.docker.internal)', 'if [[ -z "$gateway" ]]; then', ` gateway=${defaultBridgeIp}`, 'fi', '', '# 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 | undefined>) { return Object.entries(env).filter(([_, v]) => v).map(([k, v]) => `${k}='${v}'`).join(' '); } /** * Find a broadly mountable parent of the given directory * * We don't want to just mount the top-level directory, because * it could end up being `/var` (top-level dir of tmpdir on Mac). */ function findMountableParent(dir: string): string { const candidates = [ os.tmpdir(), process.env.TMPDIR, process.env.HOME, ]; for (const cand of candidates) { if (cand === undefined) { continue; } if (dir.startsWith(cand)) { const suffix = dir.substring(cand.length); const firstChildDir = suffix.split('/')[0]; return path.join(cand, firstChildDir); } } return dir; }