testcontainers 8.2.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 +54 -23
- package/dist/bound-ports.d.ts +2 -2
- package/dist/bound-ports.js +3 -1
- package/dist/docker/functions/container/create-container.d.ts +5 -2
- package/dist/docker/functions/container/create-container.js +9 -3
- package/dist/docker/functions/container/get-container.d.ts +1 -0
- package/dist/docker/functions/container/get-container.js +34 -3
- package/dist/docker/functions/create-labels.d.ts +3 -2
- package/dist/docker/functions/create-labels.js +8 -3
- package/dist/docker/functions/image/build-image.js +1 -1
- package/dist/docker/functions/image/list-images.js +3 -1
- package/dist/docker/functions/network/create-network.js +1 -1
- package/dist/generic-container/generic-container.d.ts +8 -4
- package/dist/generic-container/generic-container.js +41 -2
- package/dist/hash.d.ts +1 -0
- package/dist/hash.js +11 -0
- package/dist/labels.d.ts +2 -0
- package/dist/labels.js +5 -0
- package/dist/port.d.ts +7 -0
- package/dist/port.js +7 -0
- package/dist/reaper.js +4 -2
- package/dist/registry-auth-locator/index.js +2 -1
- package/dist/test-container.d.ts +3 -2
- package/package.json +15 -15
package/README.md
CHANGED
|
@@ -24,6 +24,7 @@ The following environment variables are supported:
|
|
|
24
24
|
- `DOCKER_HOST=tcp://docker:2375` Sets the URL of the docker daemon
|
|
25
25
|
- `DOCKER_TLS_VERIFY=1` When set to `1`, enables TLS communication with the docker
|
|
26
26
|
daemon
|
|
27
|
+
- `DOCKER_CONFIG=/some/path` Configures the path to the `config.json`. Defaults to `~/.docker`
|
|
27
28
|
- `DOCKER_CERT_PATH=/some/path` Configures the path to the `ca.pem`, `cert.pem`, and `key.pem` files used for TLS
|
|
28
29
|
verification. Defaults to `~/.docker`
|
|
29
30
|
- `TESTCONTAINERS_HOST_OVERRIDE=docker.svc.local` Docker's host on which ports are exposed
|
|
@@ -128,6 +129,24 @@ const container = await new GenericContainer("alpine")
|
|
|
128
129
|
.start();
|
|
129
130
|
```
|
|
130
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
|
+
|
|
131
150
|
Creating a container with a specified name:
|
|
132
151
|
|
|
133
152
|
```javascript
|
|
@@ -300,10 +319,10 @@ expect((await container.exec(["nslookup", "foo"])).exitCode).toBe(0);
|
|
|
300
319
|
expect((await container.exec(["nslookup", "bar"])).exitCode).toBe(0);
|
|
301
320
|
```
|
|
302
321
|
|
|
303
|
-
Specifying a pull policy. Note that if omitted will use the `DefaultPullPolicy` which will use a locally cached image
|
|
304
|
-
if one already exists, this is usually the preferred option. In cases where there is a local image for a given tag
|
|
305
|
-
but the remote image with the same tag may have changed (for example when using the `latest` tag), you can tell
|
|
306
|
-
testcontainers to pull the image again by specifying an `AlwaysPullPolicy`:
|
|
322
|
+
Specifying a pull policy. Note that if omitted will use the `DefaultPullPolicy` which will use a locally cached image
|
|
323
|
+
if one already exists, this is usually the preferred option. In cases where there is a local image for a given tag
|
|
324
|
+
but the remote image with the same tag may have changed (for example when using the `latest` tag), you can tell
|
|
325
|
+
testcontainers to pull the image again by specifying an `AlwaysPullPolicy`:
|
|
307
326
|
|
|
308
327
|
```javascript
|
|
309
328
|
const { GenericContainer, AlwaysPullPolicy } = require("testcontainers");
|
|
@@ -325,7 +344,7 @@ const container = await GenericContainer.fromDockerfile(buildContext)
|
|
|
325
344
|
|
|
326
345
|
Specifying a default log driver. You can override the logging driver used by Docker to be the default one (json-file).
|
|
327
346
|
This might be necessary when the driver of your docker host does not support reading logs
|
|
328
|
-
and you want to use the `Wait.forLogMessage` wait strategy. This is the same as
|
|
347
|
+
and you want to use the `Wait.forLogMessage` wait strategy. This is the same as
|
|
329
348
|
[--log-driver json-file on docker run](https://docs.docker.com/config/containers/logging/configure/#configure-the-logging-driver-for-a-container).
|
|
330
349
|
|
|
331
350
|
```javascript
|
|
@@ -375,8 +394,8 @@ const container = await new GenericContainer("postgres")
|
|
|
375
394
|
.withExposedPorts(5432)
|
|
376
395
|
.start();
|
|
377
396
|
|
|
378
|
-
await container.stop({
|
|
379
|
-
timeout: 10000
|
|
397
|
+
await container.stop({
|
|
398
|
+
timeout: 10000
|
|
380
399
|
});
|
|
381
400
|
```
|
|
382
401
|
|
|
@@ -390,7 +409,7 @@ const container = await new GenericContainer("postgres")
|
|
|
390
409
|
.withExposedPorts(5432)
|
|
391
410
|
.start();
|
|
392
411
|
|
|
393
|
-
await container.stop({
|
|
412
|
+
await container.stop({
|
|
394
413
|
removeVolumes: false
|
|
395
414
|
});
|
|
396
415
|
```
|
|
@@ -432,6 +451,19 @@ const { output } = await container.exec(["curl", `http://host.testcontainers.int
|
|
|
432
451
|
assert(output === "hello world");
|
|
433
452
|
```
|
|
434
453
|
|
|
454
|
+
Specifying an exact host port to bind to (not recommended, Testcontainers will automatically find an available port):
|
|
455
|
+
|
|
456
|
+
```javascript
|
|
457
|
+
const { GenericContainer } = require("testcontainers");
|
|
458
|
+
|
|
459
|
+
const container = await new GenericContainer("alpine")
|
|
460
|
+
.withExposedPorts({
|
|
461
|
+
container: 8000,
|
|
462
|
+
host: 8080
|
|
463
|
+
})
|
|
464
|
+
.start();
|
|
465
|
+
```
|
|
466
|
+
|
|
435
467
|
## Docker Compose
|
|
436
468
|
|
|
437
469
|
Testcontainers supports docker-compose. For example for the following `docker-compose.yml`:
|
|
@@ -567,7 +599,7 @@ const environment = await new DockerComposeEnvironment(composeFilePath, composeF
|
|
|
567
599
|
.up();
|
|
568
600
|
```
|
|
569
601
|
|
|
570
|
-
Specify not to re-create containers that are already running:
|
|
602
|
+
Specify not to re-create containers that are already running:
|
|
571
603
|
|
|
572
604
|
```javascript
|
|
573
605
|
const { DockerComposeEnvironment } = require("testcontainers");
|
|
@@ -597,7 +629,7 @@ const environment = await new DockerComposeEnvironment(composeFilePath, composeF
|
|
|
597
629
|
|
|
598
630
|
## Wait Strategies
|
|
599
631
|
|
|
600
|
-
Ordinarily Testcontainers will wait for up to 60 seconds for the container's mapped network ports to start listening.
|
|
632
|
+
Ordinarily Testcontainers will wait for up to 60 seconds for the container's mapped network ports to start listening.
|
|
601
633
|
If the default 60s timeout is not sufficient, it can be altered with the `withStartupTimeout()` method:
|
|
602
634
|
|
|
603
635
|
```javascript
|
|
@@ -651,7 +683,7 @@ credential helpers, credential stores, or raw auth as necessary and in that orde
|
|
|
651
683
|
|
|
652
684
|
## Auxiliary Containers
|
|
653
685
|
|
|
654
|
-
Testcontainers may need to create auxiliary containers to provide its functionality.
|
|
686
|
+
Testcontainers may need to create auxiliary containers to provide its functionality.
|
|
655
687
|
|
|
656
688
|
To avoid Docker pull limits, you can host your own images and use them by setting the appropriate environment variables:
|
|
657
689
|
|
|
@@ -662,21 +694,21 @@ To avoid Docker pull limits, you can host your own images and use them by settin
|
|
|
662
694
|
|
|
663
695
|
### ryuk
|
|
664
696
|
|
|
665
|
-
Testcontainers will start ryuk whenever a container, docker-compose environment or network is started.
|
|
697
|
+
Testcontainers will start ryuk whenever a container, docker-compose environment or network is started.
|
|
666
698
|
|
|
667
|
-
Once started, this container keeps track of containers/images/networks/volumes created by testcontainers and will
|
|
668
|
-
automatically clean them up 10s after connectivity with testcontainers is lost. This is useful for example if a test
|
|
699
|
+
Once started, this container keeps track of containers/images/networks/volumes created by testcontainers and will
|
|
700
|
+
automatically clean them up 10s after connectivity with testcontainers is lost. This is useful for example if a test
|
|
669
701
|
starts a container and then terminates unexpectedly, as it will be automatically removed.
|
|
670
702
|
|
|
671
|
-
ryuk by default does not run privileged, if necessary this can be overridden by setting the environment variable
|
|
672
|
-
`TESTCONTAINERS_RYUK_PRIVILEGED` to `true`. If necessary, ryuk can be disabled enirely by setting the environment
|
|
703
|
+
ryuk by default does not run privileged, if necessary this can be overridden by setting the environment variable
|
|
704
|
+
`TESTCONTAINERS_RYUK_PRIVILEGED` to `true`. If necessary, ryuk can be disabled enirely by setting the environment
|
|
673
705
|
variable `TESTCONTAINERS_RYUK_DISABLED` to `true`.
|
|
674
706
|
|
|
675
707
|
### SSHd
|
|
676
708
|
|
|
677
|
-
Testcontainers will start SSHd when using the expose host port functionality.
|
|
709
|
+
Testcontainers will start SSHd when using the expose host port functionality.
|
|
678
710
|
|
|
679
|
-
Once started, any container that is created will have a host mapping of `host.testcontainers.internal` that points to
|
|
711
|
+
Once started, any container that is created will have a host mapping of `host.testcontainers.internal` that points to
|
|
680
712
|
the SSHd container, as well as being connected to its network.
|
|
681
713
|
|
|
682
714
|
When we then expose a host port, we remote port forward our local port to the SSHd container, which the other
|
|
@@ -686,18 +718,17 @@ containers will then be able to access at `host.testcontainers.internal:<exposed
|
|
|
686
718
|
|
|
687
719
|
1. **Insufficient Docker memory**
|
|
688
720
|
|
|
689
|
-
By default, Docker sets CPU and memory limits, with a default memory limit
|
|
690
|
-
of 2GB. If exceeded, you will be unable to pull/run Docker images.
|
|
721
|
+
By default, Docker sets CPU and memory limits, with a default memory limit
|
|
722
|
+
of 2GB. If exceeded, you will be unable to pull/run Docker images.
|
|
691
723
|
To see how much memory Docker has used, you can run ```docker system info```
|
|
692
724
|
|
|
693
725
|
- To remove existing containers and images to clear some space you can run ```docker system prune```
|
|
694
726
|
- Alternatively you can increase the memory limit via Docker's settings under the Advanced pane.
|
|
695
727
|
|
|
696
|
-
2. **Insufficient test timeouts**
|
|
728
|
+
2. **Insufficient test timeouts**
|
|
697
729
|
|
|
698
|
-
It can take a few seconds up to a few minutes to pull and run certain Docker images,
|
|
730
|
+
It can take a few seconds up to a few minutes to pull and run certain Docker images,
|
|
699
731
|
depending on file sizes and network constraints. It's unlikely that the default
|
|
700
732
|
timeouts set by test frameworks are sufficient.
|
|
701
733
|
|
|
702
734
|
- Increase the test timeout via the methods provided by the testing framework.
|
|
703
|
-
|
package/dist/bound-ports.d.ts
CHANGED
|
@@ -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:
|
|
8
|
+
filter(ports: PortWithOptionalBinding[]): BoundPorts;
|
|
9
9
|
static fromInspectResult(inspectResult: InspectResult): BoundPorts;
|
|
10
10
|
}
|
package/dist/bound-ports.js
CHANGED
|
@@ -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 (
|
|
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 {
|
|
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:
|
|
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
|
-
|
|
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
|
};
|
|
@@ -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
|
-
|
|
16
|
-
|
|
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
|
|
3
|
-
[
|
|
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
|
|
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
|
-
|
|
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
|
|
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 {
|
|
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:
|
|
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:
|
|
46
|
-
protected addExposedPorts(...ports:
|
|
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
|
|
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;
|
package/dist/labels.d.ts
ADDED
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
|
|
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
|
});
|
|
@@ -21,7 +21,8 @@ const creds_store_1 = require("./creds-store");
|
|
|
21
21
|
const auths_1 = require("./auths");
|
|
22
22
|
const logger_1 = require("../logger");
|
|
23
23
|
const DEFAULT_REGISTRY = "https://index.docker.io/v1/";
|
|
24
|
-
const
|
|
24
|
+
const dockerConfigLocation = process.env.DOCKER_CONFIG || `${os_1.default.homedir()}/.docker`;
|
|
25
|
+
const dockerConfigFile = path_1.default.resolve(dockerConfigLocation, "config.json");
|
|
25
26
|
const readDockerConfig = () => __awaiter(void 0, void 0, void 0, function* () {
|
|
26
27
|
if (!fs_1.existsSync(dockerConfigFile)) {
|
|
27
28
|
return Promise.resolve({});
|
package/dist/test-container.d.ts
CHANGED
|
@@ -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:
|
|
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.
|
|
4
|
+
"version": "8.5.0",
|
|
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.
|
|
36
|
+
"@types/dockerode": "^3.2.7",
|
|
37
37
|
"archiver": "^5.3.0",
|
|
38
38
|
"byline": "^5.0.0",
|
|
39
|
-
"debug": "^4.3.
|
|
40
|
-
"docker-compose": "^0.23.
|
|
39
|
+
"debug": "^4.3.3",
|
|
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.
|
|
54
|
+
"@types/jest": "^27.0.3",
|
|
55
55
|
"@types/node": "^16.9.6",
|
|
56
|
-
"@types/node-fetch": "^2.5.
|
|
57
|
-
"@types/pg": "^8.6.
|
|
56
|
+
"@types/node-fetch": "^2.5.12",
|
|
57
|
+
"@types/pg": "^8.6.4",
|
|
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.
|
|
67
|
-
"jest": "^27.2.
|
|
66
|
+
"husky": "^7.0.4",
|
|
67
|
+
"jest": "^27.2.5",
|
|
68
68
|
"kafkajs": "^1.15.0",
|
|
69
|
-
"lint-staged": "^11.1.
|
|
70
|
-
"mysql2": "^2.3.
|
|
71
|
-
"neo4j-driver": "^4.3.
|
|
72
|
-
"node-fetch": "^2.6.
|
|
73
|
-
"pg": "^8.7.
|
|
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.
|
|
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"
|