|
| 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