Skip to content

Commit b0afc87

Browse files
committed
Add Docker tests for various non-default network configurations
1 parent 120bb3b commit b0afc87

File tree

6 files changed

+177
-32
lines changed

6 files changed

+177
-32
lines changed

src/dns-server.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ class DnsServer extends dns2.UDPServer {
6161
const answers = this.getHostAddresses(question.name);
6262

6363
if (answers.size > 1) {
64-
console.log(`Multiple hosts in internal DNS for hostname ${question.name}: ${answers}`);
64+
console.log(`Multiple hosts in internal DNS for hostname ${question.name}:`, answers);
6565
}
6666

6767
if (answers) {

test/fixtures/docker/compose/Dockerfile

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,5 +5,4 @@ WORKDIR /usr/src/app
55

66
COPY . .
77

8-
EXPOSE 8000
98
CMD node /usr/src/app/index.js
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
version: '3.3'
2+
3+
# This compose file tests every network configuration I can think of, starting the same HTTP
4+
# app and checking self, remote & neighbour connectivity & interception. This is in addition
5+
# to the non-network compose file in this folder, which tests default docker-compose networks.
6+
7+
services:
8+
host:
9+
build: .
10+
network_mode: 'host'
11+
12+
none:
13+
build: .
14+
network_mode: 'none'
15+
environment:
16+
SKIP_REQUESTS: 'true'
17+
18+
default-service-a:
19+
build: .
20+
network_mode: 'bridge' # Docker's default bridge network
21+
22+
default-linked-service-b:
23+
build: .
24+
network_mode: 'bridge' # Docker's default bridge network
25+
environment:
26+
EXTRA_TARGET: 'http://a:9876'
27+
# Links are deprecated, but should still work:
28+
links:
29+
- "default-service-a:a"
30+
31+
multi-network-a:
32+
build: .
33+
networks:
34+
- custom_net_1
35+
- custom_net_2
36+
environment:
37+
EXTRA_TARGET: 'http://host.docker.internal:9876' # The host container
38+
39+
multi-network-b:
40+
build: .
41+
networks:
42+
- custom_net_2
43+
- custom_net_3
44+
environment:
45+
EXTRA_TARGET: 'http://multi-network-a:9876' # Container on some shared networks
46+
47+
networks:
48+
custom_net_1:
49+
custom_net_2:
50+
custom_net_3:

test/fixtures/docker/compose/docker-compose.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,8 @@ services:
44
service-a:
55
build: .
66
environment:
7-
- TARGET=service-b
7+
- EXTRA_TARGET=http://service-b:9876
88
service-b:
99
build: .
1010
environment:
11-
- TARGET=service-a
11+
- EXTRA_TARGET=http://service-a:9876

test/fixtures/docker/compose/index.js

Lines changed: 57 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,17 @@
11
const http = require('http');
22
const https = require('https');
33

4-
const makeRequest = async (method, url) => {
4+
// The 'none' network container can just shutdown immediately - we only want
5+
// to check it starts OK, but we'll clearly never be able to proxy anything.
6+
if (process.env.SKIP_REQUESTS) {
7+
console.log('Skipping');
8+
process.exit(0);
9+
}
10+
11+
const makeRequest = async (url) => {
512
const sendRequest = url.startsWith('https') ? https.request : http.request;
613

7-
const request = sendRequest(url, { method });
14+
const request = sendRequest(url);
815
request.end();
916

1017
return new Promise((resolve, reject) => {
@@ -13,46 +20,74 @@ const makeRequest = async (method, url) => {
1320
});
1421
};
1522

16-
const SERVER_PORT = 8000;
23+
const getBody = (response) => new Promise((resolve, reject) => {
24+
let body = "";
25+
response.on('data', (d) => {
26+
body = body + d;
27+
});
28+
response.on('end', () => resolve(body));
29+
response.on('error', reject);
30+
});
31+
32+
const SERVER_PORT = 9876;
33+
const OUR_HOSTNAME = process.env.HOSTNAME;
1734

18-
const DOCKER_TARGET_URL = `http://${process.env.TARGET}:${SERVER_PORT}/`;
19-
const SELF_LOCALHOST_TARGET_URL = `http://localhost:${SERVER_PORT}/`;
20-
const SELF_HOSTNAME_TARGET_URL = `http://${process.env.HOSTNAME}:${SERVER_PORT}/`;
21-
const EXTERNAL_TARGET_URL = `https://example.test/`; // Never normally reachable
35+
const isOurHostname = (v) => v === OUR_HOSTNAME;
36+
const isNotOurHostname = (v) => v !== OUR_HOSTNAME;
37+
const is = (x) => (v) => v === x;
38+
39+
const TARGETS = [
40+
// Can we remotely resolve our own loopback address?
41+
[`http://localhost:${SERVER_PORT}/`, isOurHostname],
42+
[`http://127.0.0.1:${SERVER_PORT}/`, isOurHostname],
43+
// We can remote resolve our Docker hostname?
44+
[`http://${OUR_HOSTNAME}:${SERVER_PORT}/`, isOurHostname],
45+
// Can we resolve a mocked-only URL? (This will always fail normally)
46+
[`https://example.test/`, is('Mock response')],
47+
];
48+
49+
if (process.env.EXTRA_TARGET) {
50+
TARGETS.push([process.env.EXTRA_TARGET, isNotOurHostname]);
51+
}
2252

2353
const server = http.createServer((req, res) => {
24-
res.writeHead(200).end();
54+
res.writeHead(200).end(OUR_HOSTNAME);
2555
});
2656

2757
server.listen(SERVER_PORT, () => {
2858
console.log('Server started');
2959
});
3060

3161
const pollInterval = setInterval(async () => {
32-
console.log("Sending requests to ",
33-
DOCKER_TARGET_URL,
34-
SELF_LOCALHOST_TARGET_URL,
35-
SELF_HOSTNAME_TARGET_URL,
36-
EXTERNAL_TARGET_URL
37-
);
38-
const responses = await Promise.all([
39-
makeRequest("POST", DOCKER_TARGET_URL).catch(e => e), // Will return 200
40-
makeRequest("POST", SELF_LOCALHOST_TARGET_URL).catch(e => e), // Will return 200
41-
makeRequest("POST", SELF_HOSTNAME_TARGET_URL).catch(e => e), // Will return 200
42-
makeRequest("GET", EXTERNAL_TARGET_URL).catch(e => e) // Will not resolve
43-
]);
62+
console.log("Sending requests to ", TARGETS);
63+
64+
const responses = await Promise.all(TARGETS.map(([target]) =>
65+
makeRequest(target).catch(e => e)
66+
));
4467

4568
// ^ This will always fail normally, because the external request fails. Will only pass if it's
4669
// intercepted such that both external & all internal requests are successful at the same time.
4770

48-
if (responses.every(r => !(r instanceof Error) && r.statusCode === 200)) {
71+
if (responses.every((response) =>
72+
!(response instanceof Error) &&
73+
response.statusCode === 200
74+
)) {
75+
// Check the bodies, fail hard if any have the wrong content (i.e. went to the wrong host)
76+
const responseBodies = await Promise.all(responses.map(r => getBody(r)));
77+
responseBodies.forEach((body, i) => {
78+
const validateBody = TARGETS[i][1];
79+
if (!validateBody(body)) throw new Error(
80+
`Request ${i} to ${TARGETS[i][0]} unexpectedly returned ${body}`
81+
);
82+
});
83+
4984
console.log("All requests ok");
5085
clearInterval(pollInterval);
5186

5287
// Exit OK, but after a delay, so the other container can still make requests to us.
5388
setTimeout(() => {
5489
process.exit(0);
55-
}, 500);
90+
}, 1000);
5691
} else {
5792
console.log("Requests failed with", responses.map(r => r.message || r.statusCode));
5893
}

test/interceptors/docker-terminal-interception.spec.ts

Lines changed: 67 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -184,12 +184,20 @@ Successfully built <hash>
184184
this.timeout(30000);
185185

186186
const { server, httpsConfig, getPassThroughOptions } = await testSetup;
187-
const externalRule = await server.get("https://example.test").thenReply(200, "Mock response");
188-
const internalRule = await server.post().thenPassThrough(await getPassThroughOptions());
187+
const externalRule = await server.anyRequest()
188+
.forHost("example.test")
189+
.thenReply(200, "Mock response");
190+
const internalRule = await server
191+
.unmatchedRequest()
192+
.thenPassThrough(await getPassThroughOptions());
189193

190194
// Create non-intercepted docker-compose containers, like normal use:
191195
const composeRoot = path.join(__dirname, '..', 'fixtures', 'docker', 'compose');
192-
await spawnToResult('docker-compose', ['create', '--force-recreate', '--build'], { cwd: composeRoot }, true);
196+
await spawnToResult(
197+
'docker-compose', ['create', '--force-recreate', '--build'],
198+
{ cwd: composeRoot },
199+
true
200+
);
193201

194202
const terminalEnvOverrides = getTerminalEnvVars(server.port, httpsConfig, process.env, {}, {
195203
dockerEnabled: true
@@ -214,9 +222,62 @@ Successfully built <hash>
214222

215223
const seenInternalRequests = await internalRule.getSeenRequests();
216224
const seenInternalUrls = seenInternalRequests.map(r => r.url);
217-
expect(seenInternalUrls).to.include("http://service-a:8000/");
218-
expect(seenInternalUrls).to.include("http://service-b:8000/");
219-
expect(seenInternalUrls).to.include("http://localhost:8000/");
225+
expect(seenInternalUrls).to.include("http://service-a:9876/");
226+
expect(seenInternalUrls).to.include("http://service-b:9876/");
227+
expect(seenInternalUrls).to.include("http://localhost:9876/");
228+
expect(seenInternalUrls).to.include("http://127.0.0.1:9876/");
229+
});
230+
231+
it("should intercept all network modes", async function () {
232+
this.timeout(30000);
233+
234+
const { server, httpsConfig, getPassThroughOptions } = await testSetup;
235+
const externalRule = await server.anyRequest()
236+
.forHost("example.test")
237+
.thenReply(200, "Mock response");
238+
const internalRule = await server
239+
.unmatchedRequest()
240+
.thenPassThrough(await getPassThroughOptions());
241+
242+
// Create non-intercepted docker-compose containers, like normal use:
243+
const composeRoot = path.join(__dirname, '..', 'fixtures', 'docker', 'compose');
244+
await spawnToResult(
245+
'docker-compose', ['-f', 'docker-compose.networks.yml', 'create', '--force-recreate', '--build'],
246+
{ cwd: composeRoot },
247+
true
248+
);
249+
250+
const terminalEnvOverrides = getTerminalEnvVars(server.port, httpsConfig, process.env, {}, {
251+
dockerEnabled: true
252+
});
253+
254+
// "DC Up" the same project, but in an intercepted env. Should ignore the existing containers,
255+
// create new intercepted containers, and then up those as normal.
256+
const { exitCode, stdout } = await spawnToResult(
257+
'docker-compose', ['-f', 'docker-compose.networks.yml', 'up'],
258+
{
259+
env: { ...process.env, ...terminalEnvOverrides },
260+
cwd: composeRoot
261+
},
262+
true
263+
);
264+
265+
expect(exitCode).to.equal(0);
266+
267+
const dcOutput = stdout
268+
.replace(/\u001b\[\d+m/g, '') // Strip terminal colour commands
269+
.replace(/\s+\|/g, ' |'); // Strip container name variable whitespace
270+
271+
[
272+
`compose_host_1_HTK${server.port}`,
273+
`compose_default-service-a_1_HTK${server.port}`,
274+
`compose_default-linked-service-b_1_HTK${server.port}`,
275+
`compose_multi-network-a_1_HTK${server.port}`,
276+
`compose_multi-network-b_1_HTK${server.port}`
277+
].forEach((container) => {
278+
expect(dcOutput).to.include(`${container} | All requests ok`);
279+
});
280+
expect(dcOutput).to.include(`compose_none_1_HTK${server.port} | Skipping`);
220281
});
221282

222283
it("should clean up containers after shutdown", async () => {

0 commit comments

Comments
 (0)