Skip to content

Commit 3b5f337

Browse files
committed
Batch Docker volume checks to avoid conflicts & dedupe setup work
1 parent d73066d commit 3b5f337

File tree

1 file changed

+94
-87
lines changed

1 file changed

+94
-87
lines changed

src/interceptors/docker/docker-data-injection.ts

Lines changed: 94 additions & 87 deletions
Original file line numberDiff line numberDiff line change
@@ -89,96 +89,103 @@ async function createBlankImage(docker: Docker) {
8989
return DOCKER_BLANK_TAG;
9090
}
9191

92-
export async function ensureDockerInjectionVolumeExists(
93-
certContent: string
94-
) {
95-
if (!await isDockerAvailable()) return;
96-
97-
const docker = new Docker();
98-
99-
const existingVolume = await docker.getVolume(DOCKER_DATA_VOLUME_NAME).inspect()
100-
.catch<false>(() => false);
101-
const isCertOutdated = existingVolume &&
102-
existingVolume.Labels[DOCKER_VOLUME_CERT_LABEL] !== certContent;
103-
104-
if (existingVolume && !isCertOutdated) return; // We're all good!
105-
106-
try {
107-
const startTime = Date.now();
108-
109-
// Clean up any leftover setup components that are hanging around (since they might
110-
// conflict with these next steps):
111-
await cleanupDataInjectionTools(docker);
112-
113-
// If cert is outdated, we just recreate the volume from scratch - cleaner to reset
114-
// then try and update, and it only takes a couple of seconds anyway:
115-
if (isCertOutdated) await docker.getVolume(DOCKER_DATA_VOLUME_NAME).remove({ force: true });
116-
117-
// No volume. We need to create a Docker volume that contains our override files,
118-
// and the CA certificate, which can be mounted into containers for interception.
119-
// We can't directly write to volumes, so we work around this by mounting a new volume
120-
// inside a stopped empty container, and writing to the container.
121-
122-
// First, we need an empty volume, and build blank image for our container:
123-
await Promise.all([
124-
docker.createVolume({
125-
Name: DOCKER_DATA_VOLUME_NAME,
126-
Labels: {
127-
[DOCKER_VOLUME_LABEL]: SERVER_VERSION,
128-
[DOCKER_VOLUME_CERT_LABEL]: certContent // Used to detect when we need to recreate
92+
// Parallel processing of a single Docker volume and the other assorted containers is asking for trouble,
93+
// and inefficient, so we collapse parallel attempts into one:
94+
let volumeSetupPromise: Promise<void> | undefined;
95+
96+
export function ensureDockerInjectionVolumeExists(certContent: string) {
97+
if (volumeSetupPromise) return volumeSetupPromise;
98+
return volumeSetupPromise = (async () => { // Run as an async IIFE
99+
if (!await isDockerAvailable()) return;
100+
const docker = new Docker();
101+
102+
const existingVolume = await docker.getVolume(DOCKER_DATA_VOLUME_NAME).inspect()
103+
.catch<false>(() => false);
104+
const isCertOutdated = existingVolume &&
105+
existingVolume.Labels[DOCKER_VOLUME_CERT_LABEL] !== certContent;
106+
107+
if (existingVolume && !isCertOutdated) return; // We're all good!
108+
109+
try {
110+
const startTime = Date.now();
111+
112+
// Clean up any leftover setup components that are hanging around (since they might
113+
// conflict with these next steps):
114+
await cleanupDataInjectionTools(docker);
115+
116+
// If cert is outdated, we just recreate the volume from scratch - cleaner to reset
117+
// then try and update, and it only takes a couple of seconds anyway:
118+
if (isCertOutdated) await docker.getVolume(DOCKER_DATA_VOLUME_NAME).remove({ force: true });
119+
120+
// No volume. We need to create a Docker volume that contains our override files,
121+
// and the CA certificate, which can be mounted into containers for interception.
122+
// We can't directly write to volumes, so we work around this by mounting a new volume
123+
// inside a stopped empty container, and writing to the container.
124+
125+
// First, we need an empty volume, and build blank image for our container:
126+
await Promise.all([
127+
docker.createVolume({
128+
Name: DOCKER_DATA_VOLUME_NAME,
129+
Labels: {
130+
[DOCKER_VOLUME_LABEL]: SERVER_VERSION,
131+
[DOCKER_VOLUME_CERT_LABEL]: certContent // Used to detect when we need to recreate
132+
}
133+
}),
134+
createBlankImage(docker)
135+
]);
136+
137+
// Then we create a blank container from the blank image with the volume mounted:
138+
const blankContainer = await docker.createContainer({
139+
Image: DOCKER_BLANK_TAG,
140+
Labels: { [DOCKER_BLANK_TAG]: '' },
141+
HostConfig: {
142+
Binds: [`${DOCKER_DATA_VOLUME_NAME}:/data-volume`]
129143
}
130-
}),
131-
createBlankImage(docker)
132-
]);
133-
134-
// Then we create a blank container from the blank image with the volume mounted:
135-
const blankContainer = await docker.createContainer({
136-
Image: DOCKER_BLANK_TAG,
137-
Labels: { [DOCKER_BLANK_TAG]: '' },
138-
HostConfig: {
139-
Binds: [`${DOCKER_DATA_VOLUME_NAME}:/data-volume`]
140-
}
141-
});
142-
143-
const blankContainerSetupTime = Date.now() - startTime;
144-
let overrideStreamTime: number | undefined;
145-
146-
// Then we use the container to write to the volume, without ever starting it:
147-
const volumeStream = TarStream.pack();
148-
// We write the CA cert, read-only:
149-
volumeStream.entry({ name: 'ca.pem', mode: parseInt('444', 8) }, certContent);
150-
// And all the override filesL
151-
const packOverridesPromise = packOverrideFiles(volumeStream, '/overrides')
152-
.then(() => {
153-
volumeStream.finalize();
154-
overrideStreamTime = Date.now() - startTime;
155144
});
156145

157-
const writeVolumePromise = blankContainer.putArchive(
158-
volumeStream,
159-
{ path: '/data-volume/' }
160-
);
161-
162-
await Promise.all([packOverridesPromise, writeVolumePromise]);
163-
const volumeCompleteTime = Date.now() - startTime;
164-
console.log(`Created Docker injection volume (took ${
165-
volumeCompleteTime
166-
}ms: ${blankContainerSetupTime}ms for setup & ${overrideStreamTime!}ms to pack)`);
167-
168-
// After success, we cleanup the tools (blank image & containers etc) and all old HTTP
169-
// Toolkit volumes, i.e. all matching volumes _except_ the current one.
170-
await cleanupDataInjectionTools(docker);
171-
await cleanupDataInjectionVolumes(docker, { keepCurrent: true }).catch(console.log);
172-
} catch (e) {
173-
console.warn('Docker injection volume setup error, cleaning up...');
174-
175-
// In a failure case, we delete the setup components and the volume too, so that at
176-
// least we have a clean slate next time:
177-
await cleanupDataInjectionTools(docker).catch(console.log);
178-
await cleanupDataInjectionVolumes(docker, { keepCurrent: false }).catch(console.log);
179-
180-
throw e;
181-
}
146+
const blankContainerSetupTime = Date.now() - startTime;
147+
let overrideStreamTime: number | undefined;
148+
149+
// Then we use the container to write to the volume, without ever starting it:
150+
const volumeStream = TarStream.pack();
151+
// We write the CA cert, read-only:
152+
volumeStream.entry({ name: 'ca.pem', mode: parseInt('444', 8) }, certContent);
153+
// And all the override filesL
154+
const packOverridesPromise = packOverrideFiles(volumeStream, '/overrides')
155+
.then(() => {
156+
volumeStream.finalize();
157+
overrideStreamTime = Date.now() - startTime;
158+
});
159+
160+
const writeVolumePromise = blankContainer.putArchive(
161+
volumeStream,
162+
{ path: '/data-volume/' }
163+
);
164+
165+
await Promise.all([packOverridesPromise, writeVolumePromise]);
166+
const volumeCompleteTime = Date.now() - startTime;
167+
console.log(`Created Docker injection volume (took ${
168+
volumeCompleteTime
169+
}ms: ${blankContainerSetupTime}ms for setup & ${overrideStreamTime!}ms to pack)`);
170+
171+
// After success, we cleanup the tools (blank image & containers etc) and all old HTTP
172+
// Toolkit volumes, i.e. all matching volumes _except_ the current one.
173+
await cleanupDataInjectionTools(docker);
174+
await cleanupDataInjectionVolumes(docker, { keepCurrent: true }).catch(console.log);
175+
} catch (e) {
176+
console.warn('Docker injection volume setup error, cleaning up...');
177+
178+
// In a failure case, we delete the setup components and the volume too, so that at
179+
// least we have a clean slate next time:
180+
await cleanupDataInjectionTools(docker).catch(console.log);
181+
await cleanupDataInjectionVolumes(docker, { keepCurrent: false }).catch(console.log);
182+
183+
throw e;
184+
}
185+
})().then(() => {
186+
// Reset the promise, so we can try again
187+
volumeSetupPromise = undefined;
188+
});
182189
}
183190

184191
async function cleanupDataInjectionVolumes(docker: Docker, options: { keepCurrent: boolean }) {

0 commit comments

Comments
 (0)