testcontainers 8.3.0 → 8.5.1

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
@@ -77,7 +77,12 @@ describe("GenericContainer", () => {
77
77
  Using TypeScript:
78
78
 
79
79
  ```typescript
80
- import { TestContainer, StartedTestContainer, StoppedTestContainer, GenericContainer } from "testcontainers";
80
+ import {
81
+ TestContainer,
82
+ StartedTestContainer,
83
+ StoppedTestContainer,
84
+ GenericContainer
85
+ } from "testcontainers";
81
86
 
82
87
  const container: TestContainer = new GenericContainer("alpine");
83
88
  const startedContainer: StartedTestContainer = await container.start();
@@ -129,6 +134,24 @@ const container = await new GenericContainer("alpine")
129
134
  .start();
130
135
  ```
131
136
 
137
+ Enabling container reuse, note that two containers are considered equal if their options (exposed ports, commands, mounts, etc) match. This also works across multiple processes:
138
+
139
+ ```javascript
140
+ const { GenericContainer } = require("testcontainers");
141
+
142
+ const container1 = await new GenericContainer("alpine")
143
+ .withCmd(["sleep", "infinity"])
144
+ .withReuse()
145
+ .start();
146
+
147
+ const container2 = await new GenericContainer("alpine")
148
+ .withCmd(["sleep", "infinity"])
149
+ .withReuse()
150
+ .start();
151
+
152
+ assert(container1.getId() === container2.getId());
153
+ ```
154
+
132
155
  Creating a container with a specified name:
133
156
 
134
157
  ```javascript
@@ -301,10 +324,10 @@ expect((await container.exec(["nslookup", "foo"])).exitCode).toBe(0);
301
324
  expect((await container.exec(["nslookup", "bar"])).exitCode).toBe(0);
302
325
  ```
303
326
 
304
- Specifying a pull policy. Note that if omitted will use the `DefaultPullPolicy` which will use a locally cached image
305
- if one already exists, this is usually the preferred option. In cases where there is a local image for a given tag
306
- but the remote image with the same tag may have changed (for example when using the `latest` tag), you can tell
307
- testcontainers to pull the image again by specifying an `AlwaysPullPolicy`:
327
+ Specifying a pull policy. Note that if omitted will use the `DefaultPullPolicy` which will use a locally cached image
328
+ if one already exists, this is usually the preferred option. In cases where there is a local image for a given tag
329
+ but the remote image with the same tag may have changed (for example when using the `latest` tag), you can tell
330
+ testcontainers to pull the image again by specifying an `AlwaysPullPolicy`:
308
331
 
309
332
  ```javascript
310
333
  const { GenericContainer, AlwaysPullPolicy } = require("testcontainers");
@@ -326,7 +349,7 @@ const container = await GenericContainer.fromDockerfile(buildContext)
326
349
 
327
350
  Specifying a default log driver. You can override the logging driver used by Docker to be the default one (json-file).
328
351
  This might be necessary when the driver of your docker host does not support reading logs
329
- and you want to use the `Wait.forLogMessage` wait strategy. This is the same as
352
+ and you want to use the `Wait.forLogMessage` wait strategy. This is the same as
330
353
  [--log-driver json-file on docker run](https://docs.docker.com/config/containers/logging/configure/#configure-the-logging-driver-for-a-container).
331
354
 
332
355
  ```javascript
@@ -376,8 +399,8 @@ const container = await new GenericContainer("postgres")
376
399
  .withExposedPorts(5432)
377
400
  .start();
378
401
 
379
- await container.stop({
380
- timeout: 10000
402
+ await container.stop({
403
+ timeout: 10000
381
404
  });
382
405
  ```
383
406
 
@@ -391,7 +414,7 @@ const container = await new GenericContainer("postgres")
391
414
  .withExposedPorts(5432)
392
415
  .start();
393
416
 
394
- await container.stop({
417
+ await container.stop({
395
418
  removeVolumes: false
396
419
  });
397
420
  ```
@@ -433,6 +456,19 @@ const { output } = await container.exec(["curl", `http://host.testcontainers.int
433
456
  assert(output === "hello world");
434
457
  ```
435
458
 
459
+ Specifying an exact host port to bind to (not recommended, Testcontainers will automatically find an available port):
460
+
461
+ ```javascript
462
+ const { GenericContainer } = require("testcontainers");
463
+
464
+ const container = await new GenericContainer("alpine")
465
+ .withExposedPorts({
466
+ container: 8000,
467
+ host: 8080
468
+ })
469
+ .start();
470
+ ```
471
+
436
472
  ## Docker Compose
437
473
 
438
474
  Testcontainers supports docker-compose. For example for the following `docker-compose.yml`:
@@ -568,7 +604,7 @@ const environment = await new DockerComposeEnvironment(composeFilePath, composeF
568
604
  .up();
569
605
  ```
570
606
 
571
- Specify not to re-create containers that are already running:
607
+ Specify not to re-create containers that are already running:
572
608
 
573
609
  ```javascript
574
610
  const { DockerComposeEnvironment } = require("testcontainers");
@@ -598,7 +634,7 @@ const environment = await new DockerComposeEnvironment(composeFilePath, composeF
598
634
 
599
635
  ## Wait Strategies
600
636
 
601
- Ordinarily Testcontainers will wait for up to 60 seconds for the container's mapped network ports to start listening.
637
+ Ordinarily Testcontainers will wait for up to 60 seconds for the container's mapped network ports to start listening.
602
638
  If the default 60s timeout is not sufficient, it can be altered with the `withStartupTimeout()` method:
603
639
 
604
640
  ```javascript
@@ -652,7 +688,7 @@ credential helpers, credential stores, or raw auth as necessary and in that orde
652
688
 
653
689
  ## Auxiliary Containers
654
690
 
655
- Testcontainers may need to create auxiliary containers to provide its functionality.
691
+ Testcontainers may need to create auxiliary containers to provide its functionality.
656
692
 
657
693
  To avoid Docker pull limits, you can host your own images and use them by setting the appropriate environment variables:
658
694
 
@@ -663,21 +699,21 @@ To avoid Docker pull limits, you can host your own images and use them by settin
663
699
 
664
700
  ### ryuk
665
701
 
666
- Testcontainers will start ryuk whenever a container, docker-compose environment or network is started.
702
+ Testcontainers will start ryuk whenever a container, docker-compose environment or network is started.
667
703
 
668
- Once started, this container keeps track of containers/images/networks/volumes created by testcontainers and will
669
- automatically clean them up 10s after connectivity with testcontainers is lost. This is useful for example if a test
704
+ Once started, this container keeps track of containers/images/networks/volumes created by testcontainers and will
705
+ automatically clean them up 10s after connectivity with testcontainers is lost. This is useful for example if a test
670
706
  starts a container and then terminates unexpectedly, as it will be automatically removed.
671
707
 
672
- ryuk by default does not run privileged, if necessary this can be overridden by setting the environment variable
673
- `TESTCONTAINERS_RYUK_PRIVILEGED` to `true`. If necessary, ryuk can be disabled enirely by setting the environment
708
+ ryuk by default does not run privileged, if necessary this can be overridden by setting the environment variable
709
+ `TESTCONTAINERS_RYUK_PRIVILEGED` to `true`. If necessary, ryuk can be disabled enirely by setting the environment
674
710
  variable `TESTCONTAINERS_RYUK_DISABLED` to `true`.
675
711
 
676
712
  ### SSHd
677
713
 
678
- Testcontainers will start SSHd when using the expose host port functionality.
714
+ Testcontainers will start SSHd when using the expose host port functionality.
679
715
 
680
- Once started, any container that is created will have a host mapping of `host.testcontainers.internal` that points to
716
+ Once started, any container that is created will have a host mapping of `host.testcontainers.internal` that points to
681
717
  the SSHd container, as well as being connected to its network.
682
718
 
683
719
  When we then expose a host port, we remote port forward our local port to the SSHd container, which the other
@@ -687,18 +723,17 @@ containers will then be able to access at `host.testcontainers.internal:<exposed
687
723
 
688
724
  1. **Insufficient Docker memory**
689
725
 
690
- By default, Docker sets CPU and memory limits, with a default memory limit
691
- of 2GB. If exceeded, you will be unable to pull/run Docker images.
726
+ By default, Docker sets CPU and memory limits, with a default memory limit
727
+ of 2GB. If exceeded, you will be unable to pull/run Docker images.
692
728
  To see how much memory Docker has used, you can run ```docker system info```
693
729
 
694
730
  - To remove existing containers and images to clear some space you can run ```docker system prune```
695
731
  - Alternatively you can increase the memory limit via Docker's settings under the Advanced pane.
696
732
 
697
- 2. **Insufficient test timeouts**
733
+ 2. **Insufficient test timeouts**
698
734
 
699
- It can take a few seconds up to a few minutes to pull and run certain Docker images,
735
+ It can take a few seconds up to a few minutes to pull and run certain Docker images,
700
736
  depending on file sizes and network constraints. It's unlikely that the default
701
737
  timeouts set by test frameworks are sufficient.
702
738
 
703
739
  - Increase the test timeout via the methods provided by the testing framework.
704
-
@@ -1,10 +1,10 @@
1
- import { Port } from "./port";
1
+ import { Port, PortWithOptionalBinding } from "./port";
2
2
  import { InspectResult } from "./docker/functions/container/inspect-container";
3
3
  export declare class BoundPorts {
4
4
  private readonly ports;
5
5
  getBinding(port: Port): Port;
6
6
  setBinding(key: Port, value: Port): void;
7
7
  iterator(): Iterable<[Port, Port]>;
8
- filter(ports: Port[]): BoundPorts;
8
+ filter(ports: PortWithOptionalBinding[]): BoundPorts;
9
9
  static fromInspectResult(inspectResult: InspectResult): BoundPorts;
10
10
  }
@@ -1,6 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.BoundPorts = void 0;
4
+ const port_1 = require("./port");
4
5
  class BoundPorts {
5
6
  constructor() {
6
7
  this.ports = new Map();
@@ -20,8 +21,9 @@ class BoundPorts {
20
21
  }
21
22
  filter(ports) {
22
23
  const boundPorts = new BoundPorts();
24
+ const containerPorts = ports.map((port) => port_1.getContainerPort(port));
23
25
  for (const [internalPort, hostPort] of this.iterator()) {
24
- if (ports.includes(internalPort)) {
26
+ if (containerPorts.includes(internalPort)) {
25
27
  boundPorts.setBinding(internalPort, hostPort);
26
28
  }
27
29
  }
@@ -1,6 +1,7 @@
1
1
  import { DockerImageName } from "../../../docker-image-name";
2
2
  import Dockerode from "dockerode";
3
- import { Port } from "../../../port";
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;
@@ -8,8 +9,10 @@ export declare type CreateContainerOptions = {
8
9
  cmd: Command[];
9
10
  bindMounts: BindMount[];
10
11
  tmpFs: TmpFs;
11
- exposedPorts: Port[];
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;
@@ -12,6 +12,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
12
12
  exports.createContainer = void 0;
13
13
  const logger_1 = require("../../../logger");
14
14
  const docker_client_1 = require("../../docker-client");
15
+ const port_1 = require("../../../port");
15
16
  const create_labels_1 = require("../create-labels");
16
17
  const createContainer = (options) => __awaiter(void 0, void 0, void 0, function* () {
17
18
  try {
@@ -24,7 +25,7 @@ const createContainer = (options) => __awaiter(void 0, void 0, void 0, function*
24
25
  Env: getEnv(options.env),
25
26
  ExposedPorts: getExposedPorts(options.exposedPorts),
26
27
  Cmd: options.cmd,
27
- Labels: create_labels_1.createLabels(options.imageName),
28
+ Labels: create_labels_1.createLabels(options.reusable, options.imageName, options.labels),
28
29
  // @ts-ignore
29
30
  Healthcheck: getHealthCheck(options.healthCheck),
30
31
  HostConfig: {
@@ -50,7 +51,7 @@ const getEnv = (env) => Object.entries(env).reduce((dockerodeEnvironment, [key,
50
51
  const getExposedPorts = (exposedPorts) => {
51
52
  const dockerodeExposedPorts = {};
52
53
  for (const exposedPort of exposedPorts) {
53
- dockerodeExposedPorts[exposedPort.toString()] = {};
54
+ dockerodeExposedPorts[port_1.getContainerPort(exposedPort).toString()] = {};
54
55
  }
55
56
  return dockerodeExposedPorts;
56
57
  };
@@ -60,7 +61,12 @@ const getExtraHosts = (extraHosts) => {
60
61
  const getPortBindings = (exposedPorts) => {
61
62
  const dockerodePortBindings = {};
62
63
  for (const exposedPort of exposedPorts) {
63
- dockerodePortBindings[exposedPort] = [{ HostPort: "0" }];
64
+ if (port_1.hasHostBinding(exposedPort)) {
65
+ dockerodePortBindings[exposedPort.container] = [{ HostPort: exposedPort.host.toString() }];
66
+ }
67
+ else {
68
+ dockerodePortBindings[exposedPort] = [{ HostPort: "0" }];
69
+ }
64
70
  }
65
71
  return dockerodePortBindings;
66
72
  };
@@ -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
  })
@@ -18,10 +18,12 @@ const listImages = () => __awaiter(void 0, void 0, void 0, function* () {
18
18
  const { dockerode } = yield docker_client_1.dockerClient;
19
19
  const images = yield dockerode.listImages();
20
20
  return images.reduce((dockerImageNames, image) => {
21
+ var _a;
21
22
  if (isDanglingImage(image)) {
22
23
  return dockerImageNames;
23
24
  }
24
- const dockerImageNamesForImage = image.RepoTags.map((imageRepoTag) => docker_image_name_1.DockerImageName.fromString(imageRepoTag));
25
+ const repoTags = (_a = image.RepoTags) !== null && _a !== void 0 ? _a : [];
26
+ const dockerImageNamesForImage = repoTags.map((imageRepoTag) => docker_image_name_1.DockerImageName.fromString(imageRepoTag));
25
27
  return [...dockerImageNames, ...dockerImageNamesForImage];
26
28
  }, []);
27
29
  }
@@ -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
  }
@@ -1,7 +1,7 @@
1
1
  /// <reference types="node" />
2
2
  import archiver from "archiver";
3
3
  import { BoundPorts } from "../bound-ports";
4
- import { Port } from "../port";
4
+ import { PortWithOptionalBinding } from "../port";
5
5
  import { PullPolicy } from "../pull-policy";
6
6
  import { StartedTestContainer, TestContainer } from "../test-container";
7
7
  import { WaitStrategy } from "../wait-strategy";
@@ -16,7 +16,7 @@ export declare class GenericContainer implements TestContainer {
16
16
  protected env: Env;
17
17
  protected networkMode?: NetworkMode;
18
18
  protected networkAliases: string[];
19
- protected ports: Port[];
19
+ protected ports: PortWithOptionalBinding[];
20
20
  protected cmd: Command[];
21
21
  protected bindMounts: BindMount[];
22
22
  protected name?: ContainerName;
@@ -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;
@@ -42,8 +45,8 @@ export declare class GenericContainer implements TestContainer {
42
45
  withNetworkMode(networkMode: NetworkMode): this;
43
46
  withNetworkAliases(...networkAliases: string[]): this;
44
47
  withExtraHosts(...extraHosts: ExtraHost[]): this;
45
- withExposedPorts(...ports: Port[]): this;
46
- protected addExposedPorts(...ports: Port[]): this;
48
+ withExposedPorts(...ports: PortWithOptionalBinding[]): this;
49
+ protected addExposedPorts(...ports: PortWithOptionalBinding[]): this;
47
50
  withBindMount(source: Dir, target: Dir, bindMode?: BindMode): this;
48
51
  withHealthCheck(healthCheck: HealthCheck): this;
49
52
  withStartupTimeout(startupTimeout: number): 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/port.d.ts CHANGED
@@ -1,2 +1,9 @@
1
1
  export declare type Port = number;
2
2
  export declare type PortString = string;
3
+ export declare type PortWithBinding = {
4
+ container: Port;
5
+ host: Port;
6
+ };
7
+ export declare type PortWithOptionalBinding = Port | PortWithBinding;
8
+ export declare const getContainerPort: (port: PortWithOptionalBinding) => number;
9
+ export declare const hasHostBinding: (port: PortWithOptionalBinding) => port is PortWithBinding;
package/dist/port.js CHANGED
@@ -1,2 +1,9 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.hasHostBinding = exports.getContainerPort = void 0;
4
+ const getContainerPort = (port) => typeof port === "number" ? port : port.container;
5
+ exports.getContainerPort = getContainerPort;
6
+ const hasHostBinding = (port) => {
7
+ return typeof port === "object" && port.host !== undefined;
8
+ };
9
+ exports.hasHostBinding = hasHostBinding;
package/dist/reaper.js CHANGED
@@ -16,6 +16,8 @@ const generic_container_1 = require("./generic-container/generic-container");
16
16
  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
+ const port_1 = require("./port");
20
+ const labels_1 = require("./labels");
19
21
  class RealReaper {
20
22
  constructor(container, socket) {
21
23
  this.container = container;
@@ -106,9 +108,9 @@ class ReaperInstance {
106
108
  }
107
109
  });
108
110
  return new Promise((resolve) => {
109
- socket.connect(port, host, () => {
111
+ socket.connect(port_1.getContainerPort(port), host, () => {
110
112
  logger_1.log.debug(`Connected to Reaper ${containerId}`);
111
- 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`);
112
114
  const reaper = new RealReaper(startedContainer, socket);
113
115
  resolve(reaper);
114
116
  });
@@ -1,5 +1,5 @@
1
1
  /// <reference types="node" />
2
- import { Port } from "./port";
2
+ import { Port, PortWithOptionalBinding } from "./port";
3
3
  import { PullPolicy } from "./pull-policy";
4
4
  import { WaitStrategy } from "./wait-strategy";
5
5
  import { Readable } from "stream";
@@ -9,7 +9,7 @@ export interface TestContainer {
9
9
  withEnv(key: EnvKey, value: EnvValue): this;
10
10
  withCmd(cmd: Command[]): this;
11
11
  withTmpFs(tmpFs: TmpFs): this;
12
- withExposedPorts(...ports: Port[]): this;
12
+ withExposedPorts(...ports: PortWithOptionalBinding[]): this;
13
13
  withBindMount(source: Dir, target: Dir, bindMode: BindMode): this;
14
14
  withWaitStrategy(waitStrategy: WaitStrategy): this;
15
15
  withStartupTimeout(startupTimeout: number): this;
@@ -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.3.0",
4
+ "version": "8.5.1",
5
5
  "main": "dist/index",
6
6
  "types": "dist/index",
7
7
  "keywords": [
@@ -33,11 +33,11 @@
33
33
  },
34
34
  "dependencies": {
35
35
  "@types/archiver": "^5.1.1",
36
- "@types/dockerode": "^3.2.5",
36
+ "@types/dockerode": "^3.2.7",
37
37
  "archiver": "^5.3.0",
38
38
  "byline": "^5.0.0",
39
- "debug": "^4.3.2",
40
- "docker-compose": "^0.23.13",
39
+ "debug": "^4.3.4",
40
+ "docker-compose": "^0.23.17",
41
41
  "dockerode": "^3.3.1",
42
42
  "get-port": "^5.1.1",
43
43
  "glob": "^7.2.0",
@@ -51,10 +51,10 @@
51
51
  "@types/byline": "^4.2.33",
52
52
  "@types/debug": "4.1.7",
53
53
  "@types/glob": "^7.1.4",
54
- "@types/jest": "^27.0.2",
54
+ "@types/jest": "^27.0.3",
55
55
  "@types/node": "^16.9.6",
56
- "@types/node-fetch": "^2.5.11",
57
- "@types/pg": "^8.6.1",
56
+ "@types/node-fetch": "^2.5.12",
57
+ "@types/pg": "^8.6.5",
58
58
  "@types/properties-reader": "^2.1.1",
59
59
  "@types/tar-fs": "^2.0.1",
60
60
  "@typescript-eslint/eslint-plugin": "^4.31.2",
@@ -63,17 +63,17 @@
63
63
  "cross-env": "^7.0.3",
64
64
  "eslint": "^7.32.0",
65
65
  "eslint-config-prettier": "^8.3.0",
66
- "husky": "^7.0.2",
67
- "jest": "^27.2.1",
66
+ "husky": "^7.0.4",
67
+ "jest": "^27.2.5",
68
68
  "kafkajs": "^1.15.0",
69
- "lint-staged": "^11.1.2",
70
- "mysql2": "^2.3.0",
71
- "neo4j-driver": "^4.3.3",
72
- "node-fetch": "^2.6.1",
73
- "pg": "^8.7.1",
69
+ "lint-staged": "^11.1.4",
70
+ "mysql2": "^2.3.3",
71
+ "neo4j-driver": "^4.3.6",
72
+ "node-fetch": "^2.6.7",
73
+ "pg": "^8.7.3",
74
74
  "prettier": "^2.4.1",
75
75
  "rimraf": "^3.0.2",
76
- "ts-jest": "^27.0.5",
76
+ "ts-jest": "^27.0.7",
77
77
  "ts-node": "^10.2.1",
78
78
  "typescript": "^4.3.5",
79
79
  "wait-for-expect": "^3.0.2"