testcontainers 9.10.1 → 9.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. package/dist/package.json +3 -1
  2. package/dist/src/docker/client/docker-client-types.d.ts +21 -0
  3. package/dist/src/docker/client/docker-client-types.js +2 -0
  4. package/dist/src/docker/client/docker-client.d.ts +1 -17
  5. package/dist/src/docker/client/docker-client.js +50 -23
  6. package/dist/src/docker/client/strategy/configuration-strategy.d.ts +1 -1
  7. package/dist/src/docker/client/strategy/configuration-strategy.js +2 -4
  8. package/dist/src/docker/client/strategy/docker-client-strategy.d.ts +1 -1
  9. package/dist/src/docker/client/strategy/npipe-socket-strategy.d.ts +1 -1
  10. package/dist/src/docker/client/strategy/npipe-socket-strategy.js +1 -5
  11. package/dist/src/docker/client/strategy/rootless-unix-socket-strategy.d.ts +1 -1
  12. package/dist/src/docker/client/strategy/rootless-unix-socket-strategy.js +1 -2
  13. package/dist/src/docker/client/strategy/testcontainers-host-strategy.d.ts +1 -1
  14. package/dist/src/docker/client/strategy/testcontainers-host-strategy.js +2 -7
  15. package/dist/src/docker/client/strategy/unix-socket-strategy.d.ts +1 -1
  16. package/dist/src/docker/client/strategy/unix-socket-strategy.js +1 -5
  17. package/dist/src/docker/functions/container/create-container.d.ts +1 -1
  18. package/dist/src/docker/functions/container/create-container.js +2 -2
  19. package/dist/src/docker/functions/create-labels.d.ts +1 -2
  20. package/dist/src/docker/functions/create-labels.js +2 -3
  21. package/dist/src/docker/functions/image/build-image.d.ts +2 -1
  22. package/dist/src/docker/functions/image/build-image.js +2 -2
  23. package/dist/src/docker/functions/network/create-network.d.ts +1 -1
  24. package/dist/src/docker/functions/network/create-network.js +2 -2
  25. package/dist/src/docker/remote-docker-unix-socket-path.d.ts +2 -2
  26. package/dist/src/docker/types.d.ts +3 -0
  27. package/dist/src/docker-compose-environment/docker-compose-environment.js +2 -2
  28. package/dist/src/file-lock.d.ts +1 -0
  29. package/dist/src/file-lock.js +47 -0
  30. package/dist/src/generic-container/generic-container-builder.d.ts +4 -1
  31. package/dist/src/generic-container/generic-container-builder.js +4 -5
  32. package/dist/src/generic-container/generic-container.d.ts +8 -4
  33. package/dist/src/generic-container/generic-container.js +75 -72
  34. package/dist/src/index.d.ts +1 -1
  35. package/dist/src/modules/selenium/selenium-container.js +11 -8
  36. package/dist/src/network.js +3 -3
  37. package/dist/src/port-forwarder.js +2 -2
  38. package/dist/src/reaper.d.ts +5 -14
  39. package/dist/src/reaper.js +124 -114
  40. package/dist/src/test-container.d.ts +2 -1
  41. package/package.json +3 -1
  42. package/dist/src/docker/session-id.d.ts +0 -1
  43. package/dist/src/docker/session-id.js +0 -5
@@ -1,11 +1,10 @@
1
1
  /// <reference types="node" />
2
- import archiver from "archiver";
3
2
  import { BoundPorts } from "../bound-ports";
4
3
  import { PortWithOptionalBinding } from "../port";
5
4
  import { PullPolicy } from "../pull-policy";
6
5
  import { StartedTestContainer, TestContainer } from "../test-container";
7
6
  import { WaitStrategy } from "../wait-strategy/wait-strategy";
8
- import { BindMount, ContentToCopy, Environment, ExtraHost, FileToCopy, HealthCheck, Labels, ResourcesQuota, TmpFs, Ulimits } from "../docker/types";
7
+ import { BindMount, ContentToCopy, DirectoryToCopy, Environment, ExtraHost, FileToCopy, HealthCheck, Labels, ResourcesQuota, TmpFs, Ulimits } from "../docker/types";
9
8
  import { CreateContainerOptions } from "../docker/functions/container/create-container";
10
9
  import { InspectResult } from "../docker/functions/container/inspect-container";
11
10
  import { GenericContainerBuilder } from "./generic-container-builder";
@@ -17,11 +16,13 @@ export declare class GenericContainer implements TestContainer {
17
16
  protected opts: CreateContainerOptions;
18
17
  protected startupTimeout?: number;
19
18
  protected waitStrategy: WaitStrategy;
20
- protected tarToCopy?: archiver.Archiver;
21
19
  protected networkMode?: string;
22
20
  protected networkAliases: string[];
23
21
  protected pullPolicy: PullPolicy;
24
22
  protected logConsumer?: (stream: Readable) => unknown;
23
+ protected filesToCopy: FileToCopy[];
24
+ protected directoriesToCopy: DirectoryToCopy[];
25
+ protected contentsToCopy: ContentToCopy[];
25
26
  constructor(image: string);
26
27
  /**
27
28
  * @deprecated Since version 9.4.0. Will be removed in version 10.0.0. Use `beforeContainerStarted` instead.
@@ -31,8 +32,11 @@ export declare class GenericContainer implements TestContainer {
31
32
  protected containerCreated?(containerId: string): Promise<void>;
32
33
  protected containerStarting?(inspectResult: InspectResult, reused: boolean): Promise<void>;
33
34
  start(): Promise<StartedTestContainer>;
35
+ private reuseOrStartContainer;
34
36
  private reuseContainer;
35
37
  private startContainer;
38
+ private connectContainerToPortForwarder;
39
+ private createArchiveToCopyToContainer;
36
40
  /**
37
41
  * @deprecated Since version 9.4.0. Will be removed in version 10.0.0. Use `containerStarted` instead.
38
42
  */
@@ -64,8 +68,8 @@ export declare class GenericContainer implements TestContainer {
64
68
  withPullPolicy(pullPolicy: PullPolicy): this;
65
69
  withIpcMode(ipcMode: string): this;
66
70
  withCopyFilesToContainer(filesToCopy: FileToCopy[]): this;
71
+ withCopyDirectoriesToContainer(directoriesToCopy: DirectoryToCopy[]): this;
67
72
  withCopyContentToContainer(contentsToCopy: ContentToCopy[]): this;
68
- protected getTarToCopy(): archiver.Archiver;
69
73
  withWorkingDir(workingDir: string): this;
70
74
  withResourcesQuota({ memory, cpu }: ResourcesQuota): this;
71
75
  withSharedMemorySize(bytes: number): this;
@@ -18,7 +18,6 @@ const async_lock_1 = __importDefault(require("async-lock"));
18
18
  const bound_ports_1 = require("../bound-ports");
19
19
  const logger_1 = require("../logger");
20
20
  const pull_policy_1 = require("../pull-policy");
21
- const reaper_1 = require("../reaper");
22
21
  const docker_image_name_1 = require("../docker-image-name");
23
22
  const port_forwarder_1 = require("../port-forwarder");
24
23
  const pull_image_1 = require("../docker/functions/image/pull-image");
@@ -47,19 +46,19 @@ class GenericContainer {
47
46
  this.waitStrategy = wait_1.Wait.forListeningPorts();
48
47
  this.networkAliases = [];
49
48
  this.pullPolicy = new pull_policy_1.DefaultPullPolicy();
49
+ this.filesToCopy = [];
50
+ this.directoriesToCopy = [];
51
+ this.contentsToCopy = [];
50
52
  const imageName = docker_image_name_1.DockerImageName.fromString(image);
51
53
  this.opts = (0, create_container_options_1.initCreateContainerOptions)(imageName, imageName.isReaper());
52
54
  }
53
55
  start() {
54
56
  return __awaiter(this, void 0, void 0, function* () {
55
- const { dockerode, info } = yield (0, docker_client_1.getDockerClient)();
56
- yield (0, pull_image_1.pullImage)(dockerode, info.dockerInfo.indexServerAddress, {
57
+ const dockerClient = yield (0, docker_client_1.getDockerClient)();
58
+ yield (0, pull_image_1.pullImage)(dockerClient.dockerode, dockerClient.info.dockerInfo.indexServerAddress, {
57
59
  imageName: this.opts.imageName,
58
60
  force: this.pullPolicy.shouldPull(),
59
61
  });
60
- if (!this.opts.reusable && !this.opts.imageName.isReaper()) {
61
- yield reaper_1.ReaperInstance.getInstance();
62
- }
63
62
  if (this.beforeContainerStarted) {
64
63
  yield this.beforeContainerStarted();
65
64
  }
@@ -70,41 +69,33 @@ class GenericContainer {
70
69
  const portForwarder = yield port_forwarder_1.PortForwarderInstance.getInstance();
71
70
  this.opts.extraHosts.push({ host: "host.testcontainers.internal", ipAddress: portForwarder.getIpAddress() });
72
71
  }
73
- if (this.networkAliases.length > 0) {
74
- this.opts.networkMode = undefined;
75
- }
76
- else {
77
- this.opts.networkMode = this.networkMode;
78
- }
72
+ this.opts.networkMode = this.networkAliases.length > 0 ? undefined : this.networkMode;
79
73
  if (this.opts.reusable) {
80
- const containerHash = (0, hash_1.hash)(JSON.stringify(this.opts));
81
- this.opts.labels = Object.assign(Object.assign({}, this.opts.labels), { [labels_1.LABEL_TESTCONTAINERS_CONTAINER_HASH]: containerHash });
82
- logger_1.log.debug(`Container reuse has been enabled with hash "${containerHash}"`);
83
- // We might have several async processes try to create a reusable container
84
- // at once, to avoid possibly creating too many of these, use a lock
85
- // on the containerHash, this ensures that only single reusable instance is created
86
- return reusableContainerCreationLock.acquire(containerHash, () => __awaiter(this, void 0, void 0, function* () {
87
- const container = yield (0, get_container_1.getContainerByHash)(containerHash);
88
- if (container !== undefined) {
89
- logger_1.log.debug(`Found container to reuse with hash "${containerHash}"`, { containerId: container.id });
90
- return this.reuseContainer(container);
91
- }
92
- else {
93
- logger_1.log.debug("No container found to reuse");
94
- return this.startContainer(this.opts);
95
- }
96
- }));
97
- }
98
- else {
99
- return this.startContainer(this.opts);
74
+ return this.reuseOrStartContainer(dockerClient);
100
75
  }
76
+ return this.startContainer(dockerClient, this.opts);
101
77
  });
102
78
  }
103
- reuseContainer(container) {
79
+ reuseOrStartContainer(dockerClient) {
80
+ return __awaiter(this, void 0, void 0, function* () {
81
+ const containerHash = (0, hash_1.hash)(JSON.stringify(this.opts));
82
+ this.opts.labels = Object.assign(Object.assign({}, this.opts.labels), { [labels_1.LABEL_TESTCONTAINERS_CONTAINER_HASH]: containerHash });
83
+ logger_1.log.debug(`Container reuse has been enabled with hash "${containerHash}"`);
84
+ return reusableContainerCreationLock.acquire(containerHash, () => __awaiter(this, void 0, void 0, function* () {
85
+ const container = yield (0, get_container_1.getContainerByHash)(containerHash);
86
+ if (container !== undefined) {
87
+ logger_1.log.debug(`Found container to reuse with hash "${containerHash}"`, { containerId: container.id });
88
+ return this.reuseContainer(dockerClient, container);
89
+ }
90
+ logger_1.log.debug("No container found to reuse");
91
+ return this.startContainer(dockerClient, this.opts);
92
+ }));
93
+ });
94
+ }
95
+ reuseContainer(dockerClient, container) {
104
96
  return __awaiter(this, void 0, void 0, function* () {
105
- const { host, hostIps } = yield (0, docker_client_1.getDockerClient)();
106
97
  const inspectResult = yield (0, inspect_container_1.inspectContainer)(container);
107
- const boundPorts = bound_ports_1.BoundPorts.fromInspectResult(hostIps, inspectResult).filter(this.opts.exposedPorts);
98
+ const boundPorts = bound_ports_1.BoundPorts.fromInspectResult(dockerClient.hostIps, inspectResult).filter(this.opts.exposedPorts);
108
99
  if (this.startupTimeout !== undefined) {
109
100
  this.waitStrategy.withStartupTimeout(this.startupTimeout);
110
101
  }
@@ -112,7 +103,7 @@ class GenericContainer {
112
103
  yield this.containerStarting(inspectResult, true);
113
104
  }
114
105
  yield (0, wait_for_container_1.waitForContainer)(container, this.waitStrategy, boundPorts);
115
- const startedContainer = new started_generic_container_1.StartedGenericContainer(container, host, inspectResult, boundPorts, inspectResult.name, this.waitStrategy);
106
+ const startedContainer = new started_generic_container_1.StartedGenericContainer(container, dockerClient.host, inspectResult, boundPorts, inspectResult.name, this.waitStrategy);
116
107
  if (this.containerStarted) {
117
108
  yield this.containerStarted(startedContainer, inspectResult, true);
118
109
  }
@@ -122,20 +113,11 @@ class GenericContainer {
122
113
  return startedContainer;
123
114
  });
124
115
  }
125
- startContainer(createContainerOptions) {
116
+ startContainer(dockerClient, createContainerOptions) {
126
117
  return __awaiter(this, void 0, void 0, function* () {
127
- const container = yield (0, create_container_1.createContainer)(createContainerOptions);
118
+ const container = yield (0, create_container_1.createContainer)(dockerClient.sessionId, createContainerOptions);
128
119
  if (!this.opts.imageName.isHelperContainer() && port_forwarder_1.PortForwarderInstance.isRunning()) {
129
- const portForwarder = yield port_forwarder_1.PortForwarderInstance.getInstance();
130
- const portForwarderNetworkId = portForwarder.getNetworkId();
131
- const excludedNetworks = [portForwarderNetworkId, "none", "host"];
132
- if (!this.networkMode || !excludedNetworks.includes(this.networkMode)) {
133
- yield (0, connect_network_1.connectNetwork)({
134
- containerId: container.id,
135
- networkId: portForwarderNetworkId,
136
- networkAliases: [],
137
- });
138
- }
120
+ yield this.connectContainerToPortForwarder(container);
139
121
  }
140
122
  if (this.networkMode && this.networkAliases.length > 0) {
141
123
  yield (0, connect_network_1.connectNetwork)({
@@ -144,9 +126,10 @@ class GenericContainer {
144
126
  networkAliases: this.networkAliases,
145
127
  });
146
128
  }
147
- if (this.tarToCopy) {
148
- this.tarToCopy.finalize();
149
- yield (0, put_container_archive_1.putContainerArchive)({ container, stream: this.tarToCopy, containerPath: "/" });
129
+ if (this.filesToCopy.length > 0 || this.directoriesToCopy.length > 0 || this.contentsToCopy.length > 0) {
130
+ const archive = this.createArchiveToCopyToContainer();
131
+ archive.finalize();
132
+ yield (0, put_container_archive_1.putContainerArchive)({ container, stream: archive, containerPath: "/" });
150
133
  }
151
134
  logger_1.log.info(`Starting container for image "${this.opts.imageName}"...`, { containerId: container.id });
152
135
  if (this.containerCreated) {
@@ -154,19 +137,17 @@ class GenericContainer {
154
137
  }
155
138
  yield (0, start_container_1.startContainer)(container);
156
139
  logger_1.log.info(`Started container for image "${this.opts.imageName}"`, { containerId: container.id });
157
- const { host, hostIps } = yield (0, docker_client_1.getDockerClient)();
158
140
  const inspectResult = yield (0, inspect_container_1.inspectContainer)(container);
159
- const boundPorts = bound_ports_1.BoundPorts.fromInspectResult(hostIps, inspectResult).filter(this.opts.exposedPorts);
141
+ const boundPorts = bound_ports_1.BoundPorts.fromInspectResult(dockerClient.hostIps, inspectResult).filter(this.opts.exposedPorts);
160
142
  if (this.startupTimeout !== undefined) {
161
143
  this.waitStrategy.withStartupTimeout(this.startupTimeout);
162
144
  }
163
145
  if (logger_1.containerLog.enabled() || this.logConsumer !== undefined) {
164
- const logStream = yield (0, container_logs_1.containerLogs)(container);
165
146
  if (this.logConsumer !== undefined) {
166
- this.logConsumer(logStream);
147
+ this.logConsumer(yield (0, container_logs_1.containerLogs)(container));
167
148
  }
168
149
  if (logger_1.containerLog.enabled()) {
169
- logStream
150
+ (yield (0, container_logs_1.containerLogs)(container))
170
151
  .on("data", (data) => logger_1.containerLog.trace(data.trim(), { containerId: container.id }))
171
152
  .on("err", (data) => logger_1.containerLog.error(data.trim(), { containerId: container.id }));
172
153
  }
@@ -175,7 +156,7 @@ class GenericContainer {
175
156
  yield this.containerStarting(inspectResult, false);
176
157
  }
177
158
  yield (0, wait_for_container_1.waitForContainer)(container, this.waitStrategy, boundPorts);
178
- const startedContainer = new started_generic_container_1.StartedGenericContainer(container, host, inspectResult, boundPorts, inspectResult.name, this.waitStrategy);
159
+ const startedContainer = new started_generic_container_1.StartedGenericContainer(container, dockerClient.host, inspectResult, boundPorts, inspectResult.name, this.waitStrategy);
179
160
  if (this.containerStarted) {
180
161
  yield this.containerStarted(startedContainer, inspectResult, false);
181
162
  }
@@ -185,6 +166,33 @@ class GenericContainer {
185
166
  return startedContainer;
186
167
  });
187
168
  }
169
+ connectContainerToPortForwarder(container) {
170
+ return __awaiter(this, void 0, void 0, function* () {
171
+ const portForwarder = yield port_forwarder_1.PortForwarderInstance.getInstance();
172
+ const portForwarderNetworkId = portForwarder.getNetworkId();
173
+ const excludedNetworks = [portForwarderNetworkId, "none", "host"];
174
+ if (!this.networkMode || !excludedNetworks.includes(this.networkMode)) {
175
+ yield (0, connect_network_1.connectNetwork)({
176
+ containerId: container.id,
177
+ networkId: portForwarderNetworkId,
178
+ networkAliases: [],
179
+ });
180
+ }
181
+ });
182
+ }
183
+ createArchiveToCopyToContainer() {
184
+ const tar = (0, archiver_1.default)("tar");
185
+ for (const { source, target, mode } of this.filesToCopy) {
186
+ tar.file(source, { name: target, mode });
187
+ }
188
+ for (const { source, target, mode } of this.directoriesToCopy) {
189
+ tar.directory(source, target, { mode });
190
+ }
191
+ for (const { content, target, mode } of this.contentsToCopy) {
192
+ tar.append(content, { name: target, mode });
193
+ }
194
+ return tar;
195
+ }
188
196
  get hasExposedPorts() {
189
197
  return this.opts.exposedPorts.length !== 0;
190
198
  }
@@ -287,31 +295,26 @@ class GenericContainer {
287
295
  return this;
288
296
  }
289
297
  withCopyFilesToContainer(filesToCopy) {
290
- const tar = this.getTarToCopy();
291
- filesToCopy.forEach(({ source, target }) => tar.file(source, { name: target }));
298
+ this.filesToCopy = [...this.filesToCopy, ...filesToCopy];
292
299
  return this;
293
300
  }
294
- withCopyContentToContainer(contentsToCopy) {
295
- const tar = this.getTarToCopy();
296
- contentsToCopy.forEach(({ content, target }) => tar.append(content, { name: target }));
301
+ withCopyDirectoriesToContainer(directoriesToCopy) {
302
+ this.directoriesToCopy = [...this.directoriesToCopy, ...directoriesToCopy];
297
303
  return this;
298
304
  }
299
- getTarToCopy() {
300
- if (!this.tarToCopy) {
301
- this.tarToCopy = (0, archiver_1.default)("tar");
302
- }
303
- return this.tarToCopy;
305
+ withCopyContentToContainer(contentsToCopy) {
306
+ this.contentsToCopy = [...this.contentsToCopy, ...contentsToCopy];
307
+ return this;
304
308
  }
305
309
  withWorkingDir(workingDir) {
306
310
  this.opts.workingDir = workingDir;
307
311
  return this;
308
312
  }
309
313
  withResourcesQuota({ memory, cpu }) {
310
- // Memory and CPU units from here: https://docs.docker.com/engine/api/v1.42/#tag/Container/operation/ContainerCreate
311
- // see Memory, NanoCpus parameters
312
- const ram = memory !== undefined ? memory * Math.pow(1024, 3) : undefined;
313
- const cpuQuota = cpu !== undefined ? cpu * Math.pow(10, 9) : undefined;
314
- this.opts.resourcesQuota = { memory: ram, cpu: cpuQuota };
314
+ this.opts.resourcesQuota = {
315
+ memory: memory !== undefined ? memory * Math.pow(1024, 3) : undefined,
316
+ cpu: cpu !== undefined ? cpu * Math.pow(10, 9) : undefined,
317
+ };
315
318
  return this;
316
319
  }
317
320
  withSharedMemorySize(bytes) {
@@ -1,6 +1,6 @@
1
1
  export { TestContainer, StartedTestContainer, StoppedTestContainer } from "./test-container";
2
2
  export { GenericContainer } from "./generic-container/generic-container";
3
- export { GenericContainerBuilder } from "./generic-container/generic-container-builder";
3
+ export { GenericContainerBuilder, BuildOptions } from "./generic-container/generic-container-builder";
4
4
  export { TestContainers } from "./test-containers";
5
5
  export { DockerComposeEnvironment } from "./docker-compose-environment/docker-compose-environment";
6
6
  export { StartedDockerComposeEnvironment } from "./docker-compose-environment/started-docker-compose-environment";
@@ -35,14 +35,17 @@ class SeleniumContainer extends generic_container_1.GenericContainer {
35
35
  return __awaiter(this, void 0, void 0, function* () {
36
36
  this.withExposedPorts(SELENIUM_PORT, VNC_PORT)
37
37
  .withSharedMemorySize(2 * 1024 * 1024 * 1024)
38
- .withWaitStrategy(wait_1.Wait.forHttp("/wd/hub/status", SELENIUM_PORT).forResponsePredicate((response) => {
39
- try {
40
- return JSON.parse(response).value.ready;
41
- }
42
- catch (_a) {
43
- return false;
44
- }
45
- }));
38
+ .withWaitStrategy(wait_1.Wait.forAll([
39
+ wait_1.Wait.forListeningPorts(),
40
+ wait_1.Wait.forHttp("/wd/hub/status", SELENIUM_PORT).forResponsePredicate((response) => {
41
+ try {
42
+ return JSON.parse(response).value.ready;
43
+ }
44
+ catch (_a) {
45
+ return false;
46
+ }
47
+ }),
48
+ ]));
46
49
  });
47
50
  }
48
51
  withRecording() {
@@ -12,9 +12,9 @@ Object.defineProperty(exports, "__esModule", { value: true });
12
12
  exports.StoppedNetwork = exports.StartedNetwork = exports.Network = void 0;
13
13
  const uuid_1 = require("./uuid");
14
14
  const logger_1 = require("./logger");
15
- const reaper_1 = require("./reaper");
16
15
  const create_network_1 = require("./docker/functions/network/create-network");
17
16
  const remove_network_1 = require("./docker/functions/network/remove-network");
17
+ const docker_client_1 = require("./docker/client/docker-client");
18
18
  class Network {
19
19
  constructor(createNetworkOptions = {}, uuid = new uuid_1.RandomUuid()) {
20
20
  this.createNetworkOptions = createNetworkOptions;
@@ -25,9 +25,9 @@ class Network {
25
25
  return __awaiter(this, void 0, void 0, function* () {
26
26
  const name = this.uuid.nextUuid();
27
27
  const options = Object.assign({ name, driver: "bridge", checkDuplicate: true, internal: false, attachable: false, ingress: false, enableIPv6: false }, this.createNetworkOptions);
28
- yield reaper_1.ReaperInstance.getInstance();
29
28
  logger_1.log.info(`Starting network "${name}"...`);
30
- const id = yield (0, create_network_1.createNetwork)(options);
29
+ const { sessionId } = yield (0, docker_client_1.getDockerClient)();
30
+ const id = yield (0, create_network_1.createNetwork)(sessionId, options);
31
31
  logger_1.log.info(`Started network "${name}" with ID "${id}"`);
32
32
  return new StartedNetwork(id, options);
33
33
  });
@@ -14,7 +14,6 @@ const ssh_remote_port_forward_1 = require("ssh-remote-port-forward");
14
14
  const logger_1 = require("./logger");
15
15
  const generic_container_1 = require("./generic-container/generic-container");
16
16
  const uuid_1 = require("./uuid");
17
- const session_id_1 = require("./docker/session-id");
18
17
  const docker_client_1 = require("./docker/client/docker-client");
19
18
  const images_1 = require("./images");
20
19
  class PortForwarder {
@@ -60,8 +59,9 @@ class PortForwarderInstance {
60
59
  const containerPort = process.env["TESTCONTAINERS_SSHD_PORT"]
61
60
  ? { container: 22, host: Number(process.env["TESTCONTAINERS_SSHD_PORT"]) }
62
61
  : 22;
62
+ const { sessionId } = yield (0, docker_client_1.getDockerClient)();
63
63
  const container = yield new generic_container_1.GenericContainer(images_1.SSHD_IMAGE)
64
- .withName(`testcontainers-port-forwarder-${session_id_1.sessionId}`)
64
+ .withName(`testcontainers-port-forwarder-${sessionId}`)
65
65
  .withExposedPorts(containerPort)
66
66
  .withEnvironment({ PASSWORD: password })
67
67
  .withCommand([
@@ -1,14 +1,5 @@
1
- export interface Reaper {
2
- addProject(projectName: string): void;
3
- getContainerId(): string;
4
- stop(): void;
5
- }
6
- export declare class ReaperInstance {
7
- private static instance?;
8
- static getInstance(): Promise<Reaper>;
9
- static stopInstance(): Promise<void>;
10
- private static isEnabled;
11
- private static isPrivileged;
12
- private static createDisabledInstance;
13
- private static createRealInstance;
14
- }
1
+ import { PartialDockerClient } from "./docker/client/docker-client-types";
2
+ import Dockerode from "dockerode";
3
+ export declare function registerSessionIdForCleanup(sessionId: string): Promise<void>;
4
+ export declare function registerComposeProjectForCleanup(project: string): Promise<void>;
5
+ export declare function startReaper(dockerClient: PartialDockerClient, sessionId: string, existingReaperContainer?: Dockerode.ContainerInfo): Promise<void>;
@@ -9,139 +9,149 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
9
9
  });
10
10
  };
11
11
  Object.defineProperty(exports, "__esModule", { value: true });
12
- exports.ReaperInstance = void 0;
12
+ exports.startReaper = exports.registerComposeProjectForCleanup = exports.registerSessionIdForCleanup = void 0;
13
13
  const net_1 = require("net");
14
14
  const logger_1 = require("./logger");
15
15
  const generic_container_1 = require("./generic-container/generic-container");
16
- const session_id_1 = require("./docker/session-id");
17
16
  const images_1 = require("./images");
18
- const port_1 = require("./port");
19
17
  const labels_1 = require("./labels");
20
18
  const wait_1 = require("./wait-strategy/wait");
21
19
  const retry_strategy_1 = require("./retry-strategy");
22
20
  const remote_docker_unix_socket_path_1 = require("./docker/remote-docker-unix-socket-path");
23
21
  const docker_client_1 = require("./docker/client/docker-client");
22
+ const isReaperEnabled = process.env.TESTCONTAINERS_RYUK_DISABLED !== "true";
23
+ let reaper;
24
+ function registerSessionIdForCleanup(sessionId) {
25
+ return __awaiter(this, void 0, void 0, function* () {
26
+ if (!reaper) {
27
+ yield (0, docker_client_1.getDockerClient)();
28
+ }
29
+ (yield reaper).addSession(sessionId);
30
+ });
31
+ }
32
+ exports.registerSessionIdForCleanup = registerSessionIdForCleanup;
33
+ function registerComposeProjectForCleanup(project) {
34
+ return __awaiter(this, void 0, void 0, function* () {
35
+ if (!reaper) {
36
+ yield (0, docker_client_1.getDockerClient)();
37
+ }
38
+ (yield reaper).addProject(project);
39
+ });
40
+ }
41
+ exports.registerComposeProjectForCleanup = registerComposeProjectForCleanup;
42
+ function startReaper(dockerClient, sessionId, existingReaperContainer) {
43
+ return __awaiter(this, void 0, void 0, function* () {
44
+ reaper = !isReaperEnabled
45
+ ? yield createDisabledReaper()
46
+ : existingReaperContainer
47
+ ? yield useExistingReaper(dockerClient, sessionId, existingReaperContainer)
48
+ : yield createNewReaper(dockerClient, sessionId);
49
+ });
50
+ }
51
+ exports.startReaper = startReaper;
52
+ function createDisabledReaper() {
53
+ return __awaiter(this, void 0, void 0, function* () {
54
+ return new DisabledReaper();
55
+ });
56
+ }
57
+ function useExistingReaper(dockerClient, sessionId, reaperContainer) {
58
+ return __awaiter(this, void 0, void 0, function* () {
59
+ logger_1.log.debug(`Reusing existing Reaper for session "${sessionId}"...`);
60
+ const reaperPort = getReaperPublicPort(reaperContainer);
61
+ const socket = yield connectToReaperSocket(dockerClient.host, reaperPort, sessionId, reaperContainer.Id);
62
+ return new RealReaper(sessionId, socket);
63
+ });
64
+ }
65
+ function getReaperPublicPort(reaperContainer) {
66
+ var _a;
67
+ const port = (_a = reaperContainer.Ports.find((port) => port.PrivatePort == 8080)) === null || _a === void 0 ? void 0 : _a.PublicPort;
68
+ if (!port) {
69
+ throw new Error("Expected Reaper to map exposed port 8080");
70
+ }
71
+ return port;
72
+ }
73
+ function createNewReaper(dockerClient, sessionId) {
74
+ return __awaiter(this, void 0, void 0, function* () {
75
+ const remoteDockerUnixSocketPath = (0, remote_docker_unix_socket_path_1.getRemoteDockerUnixSocketPath)(dockerClient);
76
+ logger_1.log.debug(`Creating new Reaper for session "${sessionId}" with socket path "${remoteDockerUnixSocketPath}"...`);
77
+ const startedContainer = yield createAndStartReaperContainer(sessionId, remoteDockerUnixSocketPath);
78
+ const containerId = startedContainer.getId();
79
+ const host = startedContainer.getHost();
80
+ const port = startedContainer.getMappedPort(8080);
81
+ const socket = yield connectToReaperSocket(host, port, sessionId, containerId);
82
+ return new RealReaper(sessionId, socket);
83
+ });
84
+ }
85
+ function createAndStartReaperContainer(sessionId, socketPath) {
86
+ return __awaiter(this, void 0, void 0, function* () {
87
+ const exposedPort = process.env["TESTCONTAINERS_RYUK_PORT"]
88
+ ? { container: 8080, host: Number(process.env["TESTCONTAINERS_RYUK_PORT"]) }
89
+ : 8080;
90
+ const container = new generic_container_1.GenericContainer(images_1.REAPER_IMAGE)
91
+ .withName(`testcontainers-ryuk-${sessionId}`)
92
+ .withExposedPorts(exposedPort)
93
+ .withBindMounts([{ source: socketPath, target: "/var/run/docker.sock" }])
94
+ .withWaitStrategy(wait_1.Wait.forLogMessage(/.+ Started!/));
95
+ if (process.env.TESTCONTAINERS_RYUK_PRIVILEGED === "true") {
96
+ container.withPrivilegedMode();
97
+ }
98
+ return yield container.start();
99
+ });
100
+ }
101
+ function connectToReaperSocket(host, port, sessionId, containerId) {
102
+ return __awaiter(this, void 0, void 0, function* () {
103
+ const retryResult = yield new retry_strategy_1.IntervalRetryStrategy(1000).retryUntil((attempt) => {
104
+ return new Promise((resolve) => {
105
+ logger_1.log.debug(`Connecting to Reaper (attempt ${attempt + 1}) on "${host}:${port}"...`, { containerId });
106
+ const socket = new net_1.Socket();
107
+ socket
108
+ .unref()
109
+ .on("timeout", () => logger_1.log.error(`Reaper ${containerId} socket timed out`))
110
+ .on("error", (err) => logger_1.log.error(`Reaper ${containerId} socket error: ${err}`))
111
+ .on("close", (hadError) => {
112
+ if (hadError) {
113
+ logger_1.log.error(`Connection to Reaper closed with error`, { containerId });
114
+ }
115
+ else {
116
+ logger_1.log.warn(`Connection to Reaper closed`, { containerId });
117
+ }
118
+ resolve(undefined);
119
+ })
120
+ .connect(port, host, () => {
121
+ logger_1.log.debug(`Connected to Reaper`, { containerId });
122
+ resolve(socket);
123
+ });
124
+ });
125
+ }, (result) => result !== undefined, () => {
126
+ const message = `Failed to connect to Reaper`;
127
+ logger_1.log.error(message, { containerId });
128
+ return new Error(message);
129
+ }, 4000);
130
+ if (retryResult instanceof net_1.Socket) {
131
+ return retryResult;
132
+ }
133
+ else {
134
+ throw retryResult;
135
+ }
136
+ });
137
+ }
24
138
  class RealReaper {
25
- constructor(container, socket) {
26
- this.container = container;
139
+ constructor(sessionId, socket) {
140
+ this.sessionId = sessionId;
27
141
  this.socket = socket;
28
142
  }
143
+ addSession(sessionId) {
144
+ this.socket.write(`label=${labels_1.LABEL_TESTCONTAINERS_SESSION_ID}=${sessionId}\r\n`);
145
+ }
29
146
  addProject(projectName) {
30
147
  this.socket.write(`label=com.docker.compose.project=${projectName}\r\n`);
31
148
  }
32
- getContainerId() {
33
- return this.container.getId();
34
- }
35
- stop() {
36
- this.socket.end();
37
- }
38
149
  }
39
150
  class DisabledReaper {
40
- addProject() {
151
+ addSession() {
41
152
  // noop
42
153
  }
43
- stop() {
154
+ addProject() {
44
155
  // noop
45
156
  }
46
- getContainerId() {
47
- return "";
48
- }
49
- }
50
- class ReaperInstance {
51
- static getInstance() {
52
- return __awaiter(this, void 0, void 0, function* () {
53
- if (!this.instance) {
54
- if (this.isEnabled()) {
55
- this.instance = this.createRealInstance();
56
- }
57
- else {
58
- this.instance = this.createDisabledInstance();
59
- }
60
- }
61
- return this.instance;
62
- });
63
- }
64
- static stopInstance() {
65
- return __awaiter(this, void 0, void 0, function* () {
66
- if (this.instance) {
67
- const reaper = yield this.instance;
68
- reaper.stop();
69
- this.instance = undefined;
70
- }
71
- });
72
- }
73
- static isEnabled() {
74
- return process.env.TESTCONTAINERS_RYUK_DISABLED !== "true";
75
- }
76
- static isPrivileged() {
77
- return process.env.TESTCONTAINERS_RYUK_PRIVILEGED === "true";
78
- }
79
- static createDisabledInstance() {
80
- logger_1.log.debug(`Not creating new Reaper for session "${session_id_1.sessionId}"`);
81
- return Promise.resolve(new DisabledReaper());
82
- }
83
- static createRealInstance() {
84
- return __awaiter(this, void 0, void 0, function* () {
85
- const containerPort = process.env["TESTCONTAINERS_RYUK_PORT"]
86
- ? { container: 8080, host: Number(process.env["TESTCONTAINERS_RYUK_PORT"]) }
87
- : 8080;
88
- const dockerClient = yield (0, docker_client_1.getDockerClient)();
89
- const remoteDockerUnixSocketPath = (0, remote_docker_unix_socket_path_1.getRemoteDockerUnixSocketPath)(dockerClient);
90
- logger_1.log.debug(`Creating new Reaper for session "${session_id_1.sessionId}" with socket path "${remoteDockerUnixSocketPath}"...`);
91
- const container = new generic_container_1.GenericContainer(images_1.REAPER_IMAGE)
92
- .withName(`testcontainers-ryuk-${session_id_1.sessionId}`)
93
- .withExposedPorts(containerPort)
94
- .withBindMounts([
95
- {
96
- source: remoteDockerUnixSocketPath,
97
- target: "/var/run/docker.sock",
98
- },
99
- ])
100
- .withWaitStrategy(wait_1.Wait.forLogMessage(/.+ Started!/));
101
- if (this.isPrivileged()) {
102
- container.withPrivilegedMode();
103
- }
104
- const startedContainer = yield container.start();
105
- const containerId = startedContainer.getId();
106
- const host = startedContainer.getHost();
107
- const port = startedContainer.getMappedPort(8080);
108
- const retryStrategy = new retry_strategy_1.IntervalRetryStrategy(1000);
109
- const retryResult = yield retryStrategy.retryUntil((attempt) => {
110
- return new Promise((resolve) => {
111
- logger_1.log.debug(`Connecting to Reaper (attempt ${attempt + 1}) on "${host}:${port}"...`, { containerId });
112
- const socket = new net_1.Socket();
113
- socket
114
- .unref()
115
- .on("timeout", () => logger_1.log.error(`Reaper ${containerId} socket timed out`))
116
- .on("error", (err) => logger_1.log.error(`Reaper ${containerId} socket error: ${err}`))
117
- .on("close", (hadError) => {
118
- if (hadError) {
119
- logger_1.log.error(`Connection to Reaper closed with error`, { containerId });
120
- }
121
- else {
122
- logger_1.log.warn(`Connection to Reaper closed`, { containerId });
123
- }
124
- resolve(undefined);
125
- })
126
- .connect((0, port_1.getContainerPort)(port), host, () => {
127
- logger_1.log.debug(`Connected to Reaper`, { containerId });
128
- socket.write(`label=${labels_1.LABEL_TESTCONTAINERS_SESSION_ID}=${session_id_1.sessionId}\r\n`);
129
- const reaper = new RealReaper(startedContainer, socket);
130
- resolve(reaper);
131
- });
132
- });
133
- }, (result) => result !== undefined, () => {
134
- const message = `Failed to connect to Reaper`;
135
- logger_1.log.error(message, { containerId });
136
- return new Error(message);
137
- }, 4000);
138
- if (retryResult instanceof RealReaper) {
139
- return retryResult;
140
- }
141
- else {
142
- throw retryResult;
143
- }
144
- });
145
- }
146
157
  }
147
- exports.ReaperInstance = ReaperInstance;