testcontainers 8.4.0 → 8.5.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.
package/README.md CHANGED
@@ -129,6 +129,24 @@ const container = await new GenericContainer("alpine")
129
129
  .start();
130
130
  ```
131
131
 
132
+ Enabling container reuse, note that two containers are considered equal if their options (exposed ports, commands, mounts, etc) match:
133
+
134
+ ```javascript
135
+ const { GenericContainer } = require("testcontainers");
136
+
137
+ const container1 = await new GenericContainer("alpine")
138
+ .withCmd(["sleep", "infinity"])
139
+ .withReuse()
140
+ .start();
141
+
142
+ const container2 = await new GenericContainer("alpine")
143
+ .withCmd(["sleep", "infinity"])
144
+ .withReuse()
145
+ .start();
146
+
147
+ assert(container1.getId() === container2.getId());
148
+ ```
149
+
132
150
  Creating a container with a specified name:
133
151
 
134
152
  ```javascript
@@ -1,6 +1,7 @@
1
1
  import { DockerImageName } from "../../../docker-image-name";
2
2
  import Dockerode from "dockerode";
3
3
  import { PortWithOptionalBinding } from "../../../port";
4
+ import { Labels } from "../create-labels";
4
5
  import { BindMount, Command, ContainerName, Env, ExtraHost, HealthCheck, NetworkMode, TmpFs } from "../../types";
5
6
  export declare type CreateContainerOptions = {
6
7
  imageName: DockerImageName;
@@ -10,6 +11,8 @@ export declare type CreateContainerOptions = {
10
11
  tmpFs: TmpFs;
11
12
  exposedPorts: PortWithOptionalBinding[];
12
13
  name?: ContainerName;
14
+ reusable: boolean;
15
+ labels?: Labels;
13
16
  networkMode?: NetworkMode;
14
17
  healthCheck?: HealthCheck;
15
18
  useDefaultLogDriver: boolean;
@@ -25,7 +25,7 @@ const createContainer = (options) => __awaiter(void 0, void 0, void 0, function*
25
25
  Env: getEnv(options.env),
26
26
  ExposedPorts: getExposedPorts(options.exposedPorts),
27
27
  Cmd: options.cmd,
28
- Labels: create_labels_1.createLabels(options.imageName),
28
+ Labels: create_labels_1.createLabels(options.reusable, options.imageName, options.labels),
29
29
  // @ts-ignore
30
30
  Healthcheck: getHealthCheck(options.healthCheck),
31
31
  HostConfig: {
@@ -1,3 +1,4 @@
1
1
  import Dockerode from "dockerode";
2
2
  import { Id } from "../../types";
3
3
  export declare const getContainerById: (id: Id) => Promise<Dockerode.Container>;
4
+ export declare const getContainerByHash: (hash: string) => Promise<Dockerode.Container | undefined>;
@@ -9,10 +9,41 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
9
9
  });
10
10
  };
11
11
  Object.defineProperty(exports, "__esModule", { value: true });
12
- exports.getContainerById = void 0;
12
+ exports.getContainerByHash = exports.getContainerById = void 0;
13
13
  const docker_client_1 = require("../../docker-client");
14
+ const logger_1 = require("../../../logger");
15
+ const labels_1 = require("../../../labels");
14
16
  const getContainerById = (id) => __awaiter(void 0, void 0, void 0, function* () {
15
- const { dockerode } = yield docker_client_1.dockerClient;
16
- return dockerode.getContainer(id);
17
+ try {
18
+ const { dockerode } = yield docker_client_1.dockerClient;
19
+ return dockerode.getContainer(id);
20
+ }
21
+ catch (err) {
22
+ logger_1.log.error(`Failed to get container by ID: ${err}`);
23
+ throw err;
24
+ }
17
25
  });
18
26
  exports.getContainerById = getContainerById;
27
+ const getContainerByHash = (hash) => __awaiter(void 0, void 0, void 0, function* () {
28
+ try {
29
+ const { dockerode } = yield docker_client_1.dockerClient;
30
+ const containers = yield dockerode.listContainers({
31
+ limit: 1,
32
+ filters: {
33
+ status: ["running"],
34
+ label: [`${labels_1.LABEL_CONTAINER_HASH}=${hash}`],
35
+ },
36
+ });
37
+ if (containers.length === 0) {
38
+ return undefined;
39
+ }
40
+ else {
41
+ return yield exports.getContainerById(containers[0].Id);
42
+ }
43
+ }
44
+ catch (err) {
45
+ logger_1.log.error(`Failed to get container by hash (${hash}): ${err}`);
46
+ throw err;
47
+ }
48
+ });
49
+ exports.getContainerByHash = getContainerByHash;
@@ -1,4 +1,5 @@
1
1
  import { DockerImageName } from "../../docker-image-name";
2
- export declare const createLabels: (dockerImageName?: DockerImageName | undefined) => {
3
- [label: string]: string;
2
+ export declare type Labels = {
3
+ [key: string]: string;
4
4
  };
5
+ export declare const createLabels: (reusable: boolean, dockerImageName?: DockerImageName | undefined, extraLabels?: Labels) => Labels;
@@ -2,10 +2,15 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.createLabels = void 0;
4
4
  const session_id_1 = require("../session-id");
5
- const createLabels = (dockerImageName) => {
5
+ const labels_1 = require("../../labels");
6
+ const createLabels = (reusable, dockerImageName, extraLabels = {}) => {
7
+ const labels = Object.assign({}, extraLabels);
6
8
  if (dockerImageName && dockerImageName.isReaper()) {
7
- return {};
9
+ return labels;
8
10
  }
9
- return { "org.testcontainers.session-id": session_id_1.sessionId };
11
+ if (!reusable) {
12
+ labels[labels_1.LABEL_SESSION_ID] = session_id_1.sessionId;
13
+ }
14
+ return labels;
10
15
  };
11
16
  exports.createLabels = createLabels;
@@ -31,7 +31,7 @@ const buildImage = (options) => __awaiter(void 0, void 0, void 0, function* () {
31
31
  dockerfile: options.dockerfileName,
32
32
  buildargs: options.buildArgs,
33
33
  t: options.imageName.toString(),
34
- labels: create_labels_1.createLabels(options.imageName),
34
+ labels: create_labels_1.createLabels(false, options.imageName),
35
35
  registryconfig: options.registryConfig,
36
36
  pull: options.pullPolicy.shouldPull() ? "any" : undefined,
37
37
  })
@@ -26,7 +26,7 @@ const createNetwork = (options) => __awaiter(void 0, void 0, void 0, function* (
26
26
  Ingress: options.ingress,
27
27
  EnableIPv6: options.enableIPv6,
28
28
  Options: options.options,
29
- Labels: Object.assign(Object.assign({}, options.labels), create_labels_1.createLabels()),
29
+ Labels: Object.assign(Object.assign({}, options.labels), create_labels_1.createLabels(false)),
30
30
  });
31
31
  return network.id;
32
32
  }
@@ -29,11 +29,14 @@ export declare class GenericContainer implements TestContainer {
29
29
  protected ipcMode?: string;
30
30
  protected user?: string;
31
31
  protected pullPolicy: PullPolicy;
32
+ protected reuse: boolean;
32
33
  protected tarToCopy?: archiver.Archiver;
33
34
  private extraHosts;
34
35
  constructor(image: string);
35
36
  protected preStart?(): Promise<void>;
36
37
  start(): Promise<StartedTestContainer>;
38
+ private reuseContainer;
39
+ private startContainer;
37
40
  protected postStart?(container: StartedTestContainer, inspectResult: InspectResult, boundPorts: BoundPorts): Promise<void>;
38
41
  withCmd(cmd: Command[]): this;
39
42
  withName(name: ContainerName): this;
@@ -51,6 +54,7 @@ export declare class GenericContainer implements TestContainer {
51
54
  withDefaultLogDriver(): this;
52
55
  withPrivilegedMode(): this;
53
56
  withUser(user: string): this;
57
+ withReuse(): this;
54
58
  withPullPolicy(pullPolicy: PullPolicy): this;
55
59
  withIpcMode(ipcMode: string): this;
56
60
  withCopyFileToContainer(sourcePath: string, containerPath: string): this;
@@ -35,6 +35,9 @@ const remove_container_1 = require("../docker/functions/container/remove-contain
35
35
  const put_container_archive_1 = require("../docker/functions/container/put-container-archive");
36
36
  const generic_container_builder_1 = require("./generic-container-builder");
37
37
  const started_generic_container_1 = require("./started-generic-container");
38
+ const hash_1 = require("../hash");
39
+ const get_container_1 = require("../docker/functions/container/get-container");
40
+ const labels_1 = require("../labels");
38
41
  class GenericContainer {
39
42
  constructor(image) {
40
43
  this.image = image;
@@ -48,6 +51,7 @@ class GenericContainer {
48
51
  this.useDefaultLogDriver = false;
49
52
  this.privilegedMode = false;
50
53
  this.pullPolicy = new pull_policy_1.DefaultPullPolicy();
54
+ this.reuse = false;
51
55
  this.extraHosts = [];
52
56
  this.imageName = docker_image_name_1.DockerImageName.fromString(image);
53
57
  }
@@ -71,7 +75,7 @@ class GenericContainer {
71
75
  const portForwarder = yield port_forwarder_1.PortForwarderInstance.getInstance();
72
76
  this.extraHosts.push({ host: "host.testcontainers.internal", ipAddress: portForwarder.getIpAddress() });
73
77
  }
74
- const container = yield create_container_1.createContainer({
78
+ const createContainerOptions = {
75
79
  imageName: this.imageName,
76
80
  env: this.env,
77
81
  cmd: this.cmd,
@@ -79,6 +83,7 @@ class GenericContainer {
79
83
  tmpFs: this.tmpFs,
80
84
  exposedPorts: this.ports,
81
85
  name: this.name,
86
+ reusable: this.reuse,
82
87
  networkMode: this.networkAliases.length > 0 ? undefined : this.networkMode,
83
88
  healthCheck: this.healthCheck,
84
89
  useDefaultLogDriver: this.useDefaultLogDriver,
@@ -87,7 +92,37 @@ class GenericContainer {
87
92
  extraHosts: this.extraHosts,
88
93
  ipcMode: this.ipcMode,
89
94
  user: this.user,
90
- });
95
+ };
96
+ if (this.reuse) {
97
+ const containerHash = hash_1.hash(JSON.stringify(createContainerOptions));
98
+ createContainerOptions.labels = { [labels_1.LABEL_CONTAINER_HASH]: containerHash };
99
+ logger_1.log.debug(`Container reuse has been enabled, hash: ${containerHash}`);
100
+ const container = yield get_container_1.getContainerByHash(containerHash);
101
+ if (container !== undefined) {
102
+ logger_1.log.debug(`Found container to reuse with hash: ${containerHash}`);
103
+ return this.reuseContainer(container);
104
+ }
105
+ else {
106
+ logger_1.log.debug("No container found to reuse");
107
+ return this.startContainer(createContainerOptions);
108
+ }
109
+ }
110
+ else {
111
+ return this.startContainer(createContainerOptions);
112
+ }
113
+ });
114
+ }
115
+ reuseContainer(startedContainer) {
116
+ return __awaiter(this, void 0, void 0, function* () {
117
+ const inspectResult = yield inspect_container_1.inspectContainer(startedContainer);
118
+ const boundPorts = bound_ports_1.BoundPorts.fromInspectResult(inspectResult).filter(this.ports);
119
+ yield this.waitForContainer(startedContainer, boundPorts);
120
+ return new started_generic_container_1.StartedGenericContainer(startedContainer, (yield docker_client_1.dockerClient).host, inspectResult, boundPorts, inspectResult.name);
121
+ });
122
+ }
123
+ startContainer(createContainerOptions) {
124
+ return __awaiter(this, void 0, void 0, function* () {
125
+ const container = yield create_container_1.createContainer(createContainerOptions);
91
126
  if (!this.imageName.isHelperContainer() && port_forwarder_1.PortForwarderInstance.isRunning()) {
92
127
  const portForwarder = yield port_forwarder_1.PortForwarderInstance.getInstance();
93
128
  const portForwarderNetworkId = portForwarder.getNetworkId();
@@ -190,6 +225,10 @@ class GenericContainer {
190
225
  this.user = user;
191
226
  return this;
192
227
  }
228
+ withReuse() {
229
+ this.reuse = true;
230
+ return this;
231
+ }
193
232
  withPullPolicy(pullPolicy) {
194
233
  this.pullPolicy = pullPolicy;
195
234
  return this;
package/dist/hash.d.ts ADDED
@@ -0,0 +1 @@
1
+ export declare const hash: (str: string) => string;
package/dist/hash.js ADDED
@@ -0,0 +1,11 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.hash = void 0;
7
+ const crypto_1 = __importDefault(require("crypto"));
8
+ const hash = (str) => {
9
+ return crypto_1.default.createHash("md5").update(str).digest("hex");
10
+ };
11
+ exports.hash = hash;
@@ -0,0 +1,2 @@
1
+ export declare const LABEL_SESSION_ID = "org.testcontainers.session-id";
2
+ export declare const LABEL_CONTAINER_HASH = "org.testcontainers.container-hash";
package/dist/labels.js ADDED
@@ -0,0 +1,5 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.LABEL_CONTAINER_HASH = exports.LABEL_SESSION_ID = void 0;
4
+ exports.LABEL_SESSION_ID = "org.testcontainers.session-id";
5
+ exports.LABEL_CONTAINER_HASH = "org.testcontainers.container-hash";
package/dist/reaper.js CHANGED
@@ -17,6 +17,7 @@ const session_id_1 = require("./docker/session-id");
17
17
  const docker_client_1 = require("./docker/docker-client");
18
18
  const images_1 = require("./images");
19
19
  const port_1 = require("./port");
20
+ const labels_1 = require("./labels");
20
21
  class RealReaper {
21
22
  constructor(container, socket) {
22
23
  this.container = container;
@@ -109,7 +110,7 @@ class ReaperInstance {
109
110
  return new Promise((resolve) => {
110
111
  socket.connect(port_1.getContainerPort(port), host, () => {
111
112
  logger_1.log.debug(`Connected to Reaper ${containerId}`);
112
- socket.write(`label=org.testcontainers.session-id=${session_id_1.sessionId}\r\n`);
113
+ socket.write(`label=${labels_1.LABEL_SESSION_ID}=${session_id_1.sessionId}\r\n`);
113
114
  const reaper = new RealReaper(startedContainer, socket);
114
115
  resolve(reaper);
115
116
  });
@@ -19,6 +19,7 @@ export interface TestContainer {
19
19
  withPrivilegedMode(): this;
20
20
  withUser(user: string): this;
21
21
  withPullPolicy(pullPolicy: PullPolicy): this;
22
+ withReuse(): this;
22
23
  withCopyFileToContainer(sourcePath: string, containerPath: string): this;
23
24
  withCopyContentToContainer(content: string | Buffer | Readable, containerPath: string): this;
24
25
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "testcontainers",
3
3
  "author": "Cristian Greco",
4
- "version": "8.4.0",
4
+ "version": "8.5.0",
5
5
  "main": "dist/index",
6
6
  "types": "dist/index",
7
7
  "keywords": [