podkeeper 0.3.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/LICENSE ADDED
@@ -0,0 +1,7 @@
1
+ Copyright 2024 Degu Labs, Inc
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
4
+
5
+ The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
6
+
7
+ THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,102 @@
1
+ # PodKeeper
2
+
3
+ > ⚠️ **Warning:** PodKeeper is currently in pre-1.0.0 release. Expect potential changes and experimental features that may not be fully stable yet.
4
+
5
+ PodKeeper is a node.js open-source library for starting and stopping Docker containers in a way that ensures they won’t linger if the host program crashes or exits unexpectedly without properly stopping them.
6
+
7
+ PodKeeper is written in TypeScript and comes bundled with TypeScript types.
8
+
9
+ - [Getting Started](#getting-started)
10
+ - [Bundled Services & API](#bundled-services--api)
11
+ - [How It Works](#how-it-works)
12
+ - [PodKeeper vs. TestContainers](#podkeeper-vs-testcontainers)
13
+
14
+ ## Getting Started
15
+
16
+ 1. Install podkeeper:
17
+
18
+ ```sh
19
+ npm i --save-dev podkeeper
20
+ ```
21
+
22
+ 1. Pull container you wish to launch beforehand:
23
+
24
+ ```sh
25
+ docker pull postgres:latest
26
+ ```
27
+
28
+ 1. Start / stop container programmatically:
29
+
30
+ ```ts
31
+ import { Postgres } from 'podkeeper';
32
+
33
+ const postgres = await Postgres.start();
34
+ // do something with container...
35
+ await postgres.stop();
36
+ ```
37
+
38
+ ## Bundled services & API
39
+
40
+ PodKeeper comes bundled with the following pre-configured services:
41
+
42
+ * MySQL:
43
+
44
+ ```ts
45
+ import { MySQL } from 'podkeeper';
46
+ ```
47
+
48
+ * Postgres
49
+
50
+ ```ts
51
+ import { Postgres } from 'podkeeper';
52
+ ```
53
+
54
+ * Minio
55
+
56
+ ```ts
57
+ import { Minio } from 'podkeeper';
58
+ ```
59
+
60
+ If a popular service is missing, please do not hesitate to submit a pull request.
61
+ Alternatively, you can launch a generic container service with the `GenericService` class:
62
+
63
+ ```ts
64
+ import { GenericService } from 'podkeeper';
65
+
66
+ cosnt service = await GenericService.start({
67
+ imageName: string,
68
+ ports: number[],
69
+ healthcheck?: {
70
+ test: string[],
71
+ intervalMs: number,
72
+ retries: number,
73
+ startPeriodMs: number,
74
+ timeoutMs: number,
75
+ },
76
+ command?: string[],
77
+ env?: { [key: string]: string | number | boolean | undefined };
78
+ });
79
+ ```
80
+
81
+ ## How It Works
82
+
83
+ Each Docker container has a primary process, known as the "entrypoint," which keeps the container running. For example, when you start a PostgreSQL container, it runs the `postgres` binary. When this binary exits, the container stops as well.
84
+
85
+ PodKeeper wraps the entrypoint in a special binary called `deadmanswitch`, which not only starts the entrypoint but also launches a WebSocket server. The client that initiated the container must connect to this WebSocket server; otherwise, the container will self-terminate after 10 seconds.
86
+
87
+ This setup creates a connection between the launched container and its owner. Whenever this WebSocket disconnects, the `deadmanswitch` program automatically stops the container.
88
+
89
+ ## PodKeeper vs. TestContainers
90
+
91
+ Both PodKeeper and [TestContainers](https://testcontainers.com/) provide solutions for starting, stopping, and cleaning up Docker containers:
92
+
93
+ - **TestContainers** uses a dedicated Docker container called "Ryuk" to manage cleanup.
94
+ - **PodKeeper** relies on a [dead man's switch](https://en.wikipedia.org/wiki/Dead_man%27s_switch) mechanism for cleanup.
95
+
96
+ While TestContainers is a mature, industry-proven tool, PodKeeper is an experimental alternative that explores a different approach.
97
+
98
+ There are also some notable differences in API design philosophy:
99
+
100
+ - **Process Behavior**: PodKeeper services prevent the Node.js process from exiting, while TestContainers services do not.
101
+ - **Container Pulling**: PodKeeper does not implicitly pull containers, requiring them to be available beforehand, whereas TestContainers lazily pulls containers as needed when launching a service.
102
+ - **Healthchecks**: The services that PodKeeper ships out-of-the-box are pre-configured to use proper healthchecks.
@@ -0,0 +1,34 @@
1
+ # DeadManSwitch
2
+
3
+ DeadManSwitch is a lightweight Go program that runs a specified command with arguments and simultaneously starts a WebSocket signaling server.
4
+
5
+ ## Program Behavior
6
+
7
+ DeadManSwitch operates with the following behavior:
8
+
9
+ 1. If the launched command exits, DeadManSwitch exits as well.
10
+ 2. If no client connects to the WebSocket signaling server within `DEADMANSWITCH_TIMEOUT` seconds (default is 10 seconds), the process terminates.
11
+ 3. If a client connects but then disconnects, the process terminates.
12
+
13
+ ## Configuration
14
+
15
+ You can configure DeadManSwitch using the following environment variables:
16
+
17
+ - **`DEADMANSWITCH_TIMEOUT`**: Sets the timeout (in seconds) to wait for clients to connect to the WebSocket signaling server. Default is 10 seconds.
18
+ - **`DEADMANSWITCH_PORT`**: Specifies the port to run the server on.
19
+ - **`DEADMANSWITCH_SUFFIX`**: Defines a suffix for the WebSocket URL endpoint.
20
+
21
+ ## Usage Example
22
+
23
+ The following command launches `sleep 100` and accepts WebSocket connections on `ws://localhost:54321/foobar`:
24
+
25
+ ```bash
26
+ DEADMANSWITCH_TIMEOUT=10 \
27
+ DEADMANSWITCH_PORT=54321 \
28
+ DEADMANSWITCH_SUFFIX=foobar \
29
+ deadmanswitch sleep 100
30
+ ```
31
+
32
+ ## Compilation
33
+
34
+ To compile, run `./build.sh`
@@ -0,0 +1,181 @@
1
+ import http from "http";
2
+ const DOCKER_API_VERSION = "1.41";
3
+ async function listContainers() {
4
+ const containers = await getJSON("/containers/json") ?? [];
5
+ return containers.map((container) => ({
6
+ containerId: container.Id,
7
+ imageId: container.ImageID,
8
+ state: container.State,
9
+ // Note: container names are usually prefixed with '/'.
10
+ // See https://github.com/moby/moby/issues/6705
11
+ names: (container.Names ?? []).map((name) => name.startsWith("/") ? name.substring(1) : name),
12
+ portBindings: container.Ports?.map((portInfo) => ({
13
+ ip: portInfo.IP,
14
+ hostPort: portInfo.PublicPort,
15
+ containerPort: portInfo.PrivatePort
16
+ })) ?? [],
17
+ labels: container.Labels ?? {}
18
+ }));
19
+ }
20
+ async function isContainerHealthy(containerId) {
21
+ const container = await getJSON(`/containers/${containerId}/json`);
22
+ return container?.State?.Health?.Status === "healthy";
23
+ }
24
+ async function launchContainer(options) {
25
+ const ExposedPorts = {};
26
+ const PortBindings = {};
27
+ for (const port of options.ports ?? []) {
28
+ ExposedPorts[`${port.container}/tcp`] = {};
29
+ PortBindings[`${port.container}/tcp`] = [{ HostPort: port.host + "", HostIp: "127.0.0.1" }];
30
+ }
31
+ const container = await postJSON(`/containers/create` + (options.name ? "?name=" + options.name : ""), {
32
+ Cmd: options.command,
33
+ WorkingDir: options.workingDir,
34
+ Labels: options.labels ?? {},
35
+ AttachStdin: false,
36
+ AttachStdout: false,
37
+ AttachStderr: false,
38
+ Image: options.imageId,
39
+ ExposedPorts,
40
+ Entrypoint: options.entrypoint,
41
+ Healthcheck: options.healthcheck ? {
42
+ Test: options.healthcheck.test,
43
+ Interval: options.healthcheck.intervalMs * 1e6,
44
+ // must be in nano seconds
45
+ Timeout: options.healthcheck.timeoutMs * 1e6,
46
+ // must be in nano seconds
47
+ Retries: options.healthcheck.retries,
48
+ StartPeriod: options.healthcheck.startPeriodMs * 1e6
49
+ // must be in nano seconds
50
+ } : void 0,
51
+ Env: dockerProtocolEnv(options.env),
52
+ HostConfig: {
53
+ Binds: options.binds?.map((bind) => `${bind.hostPath}:${bind.containerPath}`),
54
+ Init: true,
55
+ AutoRemove: options.autoRemove,
56
+ ShmSize: 2 * 1024 * 1024 * 1024,
57
+ PortBindings
58
+ }
59
+ });
60
+ await postJSON(`/containers/${container.Id}/start`);
61
+ if (options.waitUntil)
62
+ await postJSON(`/containers/${container.Id}/wait?condition=${options.waitUntil}`);
63
+ return container.Id;
64
+ }
65
+ async function stopContainer(options) {
66
+ await Promise.all([
67
+ // Make sure to wait for the container to be removed.
68
+ postJSON(`/containers/${options.containerId}/wait?condition=${options.waitUntil ?? "not-running"}`),
69
+ postJSON(`/containers/${options.containerId}/kill`)
70
+ ]);
71
+ }
72
+ async function removeContainer(containerId) {
73
+ await Promise.all([
74
+ // Make sure to wait for the container to be removed.
75
+ postJSON(`/containers/${containerId}/wait?condition=removed`),
76
+ callDockerAPI("delete", `/containers/${containerId}`)
77
+ ]);
78
+ }
79
+ async function getContainerLogs(containerId) {
80
+ const rawLogs = await callDockerAPI("get", `/containers/${containerId}/logs?stdout=true&stderr=true`).catch((e) => "");
81
+ if (!rawLogs)
82
+ return [];
83
+ return rawLogs.split("\n").map((line) => {
84
+ if ([0, 1, 2].includes(line.charCodeAt(0)))
85
+ return line.substring(8);
86
+ return line;
87
+ });
88
+ }
89
+ function dockerProtocolEnv(env) {
90
+ const result = [];
91
+ for (const [key, value] of Object.entries(env ?? {}))
92
+ result.push(`${key}=${value}`);
93
+ return result;
94
+ }
95
+ async function commitContainer(options) {
96
+ await postJSON(`/commit?container=${options.containerId}&repo=${options.repo}&tag=${options.tag}`, {
97
+ Entrypoint: options.entrypoint,
98
+ WorkingDir: options.workingDir,
99
+ Env: dockerProtocolEnv(options.env)
100
+ });
101
+ }
102
+ async function inspectImage(imageId) {
103
+ return await getJSON(`/images/${imageId}/json`);
104
+ }
105
+ async function listImages() {
106
+ const rawImages = await getJSON("/images/json") ?? [];
107
+ return rawImages.map((rawImage) => ({
108
+ imageId: rawImage.Id,
109
+ names: rawImage.RepoTags ?? []
110
+ }));
111
+ }
112
+ async function removeImage(imageId) {
113
+ await callDockerAPI("delete", `/images/${imageId}`);
114
+ }
115
+ async function checkEngineRunning() {
116
+ try {
117
+ await callDockerAPI("get", "/info");
118
+ return true;
119
+ } catch (e) {
120
+ return false;
121
+ }
122
+ }
123
+ async function getJSON(url) {
124
+ const result = await callDockerAPI("get", url);
125
+ if (!result)
126
+ return result;
127
+ return JSON.parse(result);
128
+ }
129
+ async function postJSON(url, json = void 0) {
130
+ const result = await callDockerAPI("post", url, json ? JSON.stringify(json) : void 0);
131
+ if (!result)
132
+ return result;
133
+ return JSON.parse(result);
134
+ }
135
+ function callDockerAPI(method, url, body = void 0) {
136
+ const dockerSocket = process.platform === "win32" ? "\\\\.\\pipe\\docker_engine" : "/var/run/docker.sock";
137
+ return new Promise((resolve, reject) => {
138
+ const request = http.request({
139
+ socketPath: dockerSocket,
140
+ path: `/v${DOCKER_API_VERSION}${url}`,
141
+ timeout: 3e4,
142
+ method
143
+ }, (response) => {
144
+ let body2 = "";
145
+ response.on("data", function(chunk) {
146
+ body2 += chunk;
147
+ });
148
+ response.on("end", function() {
149
+ if (!response.statusCode || response.statusCode < 200 || response.statusCode >= 300)
150
+ reject(new Error(`${method} ${url} FAILED with statusCode ${response.statusCode} and body
151
+ ${body2}`));
152
+ else
153
+ resolve(body2);
154
+ });
155
+ });
156
+ request.on("error", function(e) {
157
+ reject(e);
158
+ });
159
+ if (body) {
160
+ request.setHeader("Content-Type", "application/json");
161
+ request.setHeader("Content-Length", body.length);
162
+ request.write(body);
163
+ } else {
164
+ request.setHeader("Content-Type", "text/plain");
165
+ }
166
+ request.end();
167
+ });
168
+ }
169
+ export {
170
+ checkEngineRunning,
171
+ commitContainer,
172
+ getContainerLogs,
173
+ inspectImage,
174
+ isContainerHealthy,
175
+ launchContainer,
176
+ listContainers,
177
+ listImages,
178
+ removeContainer,
179
+ removeImage,
180
+ stopContainer
181
+ };
@@ -0,0 +1,84 @@
1
+ import { setTimeout } from "timers/promises";
2
+ import { WebSocket } from "ws";
3
+ import * as dockerApi from "./dockerApi.js";
4
+ import path from "path";
5
+ import url from "url";
6
+ const __filename = url.fileURLToPath(import.meta.url);
7
+ const __dirname = path.dirname(__filename);
8
+ async function connectWebSocket(address, deadline) {
9
+ while (Date.now() < deadline) {
10
+ const socket = new WebSocket(address);
11
+ const result = await new Promise((resolve, reject) => {
12
+ socket.on("open", () => resolve(true));
13
+ socket.on("error", () => reject(false));
14
+ });
15
+ if (result)
16
+ return socket;
17
+ await setTimeout(100, void 0);
18
+ }
19
+ return void 0;
20
+ }
21
+ class GenericService {
22
+ constructor(_containerId, _bindings, _ws) {
23
+ this._containerId = _containerId;
24
+ this._bindings = _bindings;
25
+ this._ws = _ws;
26
+ }
27
+ static async start(options) {
28
+ const images = await dockerApi.listImages();
29
+ const image = images.find((image2) => image2.names.includes(options.imageName));
30
+ if (!image)
31
+ throw new Error(`ERROR: no image named "${options.imageName}" - run 'docker pull ${options.imageName}'`);
32
+ const metadata = await dockerApi.inspectImage(image.imageId);
33
+ const imageArch = metadata.Architecture;
34
+ const entrypoint = ["/deadmanswitch"];
35
+ const command = [metadata.Config.Entrypoint, options.command ?? metadata.Config.Cmd].flat();
36
+ const deadmanswitchName = imageArch === "arm64" ? "deadmanswitch_linux_aarch64" : "deadmanswitch_linux_x86_64";
37
+ const usedPorts = new Set(options.ports);
38
+ let switchPort = 54321;
39
+ while (usedPorts.has(switchPort))
40
+ ++switchPort;
41
+ const containerId = await dockerApi.launchContainer({
42
+ imageId: image.imageId,
43
+ autoRemove: true,
44
+ binds: [
45
+ { containerPath: "/deadmanswitch", hostPath: path.join(__dirname, "..", "deadmanswitch", "bin", deadmanswitchName) }
46
+ ],
47
+ ports: [
48
+ ...options.ports.map((port) => ({ container: port, host: 0 })),
49
+ { container: switchPort, host: 0 }
50
+ ],
51
+ entrypoint,
52
+ command,
53
+ healthcheck: options.healthcheck,
54
+ env: options.env
55
+ });
56
+ const deadline = Date.now() + 1e4;
57
+ const container = (await dockerApi.listContainers()).find((container2) => container2.containerId === containerId);
58
+ if (!container)
59
+ throw new Error("ERROR: failed to launch container!");
60
+ const switchBinding = container.portBindings.find((binding) => binding.containerPort === switchPort);
61
+ if (!switchBinding || !switchBinding.hostPort) {
62
+ await dockerApi.stopContainer({ containerId: container.containerId });
63
+ throw new Error("Failed to expose service to host");
64
+ }
65
+ while (!await dockerApi.isContainerHealthy(container.containerId))
66
+ await setTimeout(100, void 0);
67
+ const ws = await connectWebSocket(`ws://localhost:${switchBinding.hostPort}/`, deadline);
68
+ if (!ws)
69
+ throw new Error("Failed to connect to launched container");
70
+ const service = new GenericService(containerId, container.portBindings, ws);
71
+ return service;
72
+ }
73
+ mappedPort(containerPort) {
74
+ const binding = this._bindings.find((binding2) => binding2.containerPort === containerPort);
75
+ return binding?.hostPort;
76
+ }
77
+ async stop() {
78
+ this._ws.close();
79
+ await dockerApi.stopContainer({ containerId: this._containerId });
80
+ }
81
+ }
82
+ export {
83
+ GenericService
84
+ };
package/lib/index.js ADDED
@@ -0,0 +1,8 @@
1
+ import * as dockerApi from "./dockerApi.js";
2
+ export * from "./genericService.js";
3
+ export * from "./minio.js";
4
+ export * from "./mysql.js";
5
+ export * from "./postgres.js";
6
+ export {
7
+ dockerApi
8
+ };
package/lib/minio.js ADDED
@@ -0,0 +1,51 @@
1
+ import ms from "ms";
2
+ import { GenericService } from "./genericService.js";
3
+ class Minio {
4
+ constructor(_service, _accessKeyId, _secretAccessKey) {
5
+ this._service = _service;
6
+ this._accessKeyId = _accessKeyId;
7
+ this._secretAccessKey = _secretAccessKey;
8
+ }
9
+ static async start({ accessKeyId = "root", secretAccessKey = "password" } = {}) {
10
+ const service = await GenericService.start({
11
+ imageName: "quay.io/minio/minio:latest",
12
+ ports: [9e3, 9090],
13
+ healthcheck: {
14
+ test: ["CMD", `mc`, `ready`, `local`],
15
+ intervalMs: ms("100ms"),
16
+ retries: 10,
17
+ startPeriodMs: ms("30s"),
18
+ timeoutMs: ms("5s")
19
+ },
20
+ env: {
21
+ "MINIO_ROOT_USER": accessKeyId,
22
+ "MINIO_ROOT_PASSWORD": secretAccessKey
23
+ },
24
+ command: [
25
+ "server",
26
+ "/data",
27
+ "--console-address",
28
+ ":9090"
29
+ ]
30
+ });
31
+ return new Minio(service, accessKeyId, secretAccessKey);
32
+ }
33
+ accessKeyId() {
34
+ return this._accessKeyId;
35
+ }
36
+ secretAccessKey() {
37
+ return this._secretAccessKey;
38
+ }
39
+ apiEndpoint() {
40
+ return "http://localhost:" + this._service.mappedPort(9e3);
41
+ }
42
+ webuiEndpoint() {
43
+ return "http://localhost:" + this._service.mappedPort(9090);
44
+ }
45
+ async stop() {
46
+ await this._service.stop();
47
+ }
48
+ }
49
+ export {
50
+ Minio
51
+ };
package/lib/mysql.js ADDED
@@ -0,0 +1,49 @@
1
+ import ms from "ms";
2
+ import { GenericService } from "./genericService.js";
3
+ const MYSQL_IMAGE_NAME = "mysql:latest";
4
+ const MYSQL_PORT = 3306;
5
+ class MySQL {
6
+ constructor(_service, _rootPassword, _db) {
7
+ this._service = _service;
8
+ this._rootPassword = _rootPassword;
9
+ this._db = _db;
10
+ }
11
+ static async start({ db = "mydatabase", rootPassword = "rootpassword" } = {}) {
12
+ const service = await GenericService.start({
13
+ imageName: "mysql:latest",
14
+ ports: [MYSQL_PORT],
15
+ healthcheck: {
16
+ test: ["CMD-SHELL", `mysqladmin ping --host 127.0.0.1 -u root --password=${rootPassword}`],
17
+ intervalMs: ms("100ms"),
18
+ retries: 10,
19
+ startPeriodMs: 0,
20
+ timeoutMs: ms("5s")
21
+ },
22
+ command: ["mysqld", "--innodb-force-recovery=0", "--skip-innodb-doublewrite"],
23
+ env: {
24
+ "MYSQL_ROOT_PASSWORD": rootPassword,
25
+ "MYSQL_DATABASE": db,
26
+ "MYSQL_INITDB_SKIP_TZINFO": true
27
+ }
28
+ });
29
+ return new MySQL(service, rootPassword, db);
30
+ }
31
+ databaseUrl() {
32
+ return `mysql://root:${this._rootPassword}@localhost:${this._service.mappedPort(MYSQL_PORT)}/${this._db}`;
33
+ }
34
+ connectOptions() {
35
+ return {
36
+ host: "localhost",
37
+ port: this._service.mappedPort(MYSQL_PORT),
38
+ database: this._db,
39
+ user: "root",
40
+ password: this._rootPassword
41
+ };
42
+ }
43
+ async stop() {
44
+ await this._service.stop();
45
+ }
46
+ }
47
+ export {
48
+ MySQL
49
+ };
@@ -0,0 +1,53 @@
1
+ import ms from "ms";
2
+ import { GenericService } from "./genericService.js";
3
+ const IMAGE_NAME = "postgres:latest";
4
+ const POSTGRES_PORT = 5432;
5
+ class Postgres {
6
+ constructor(_service, _user, _password, _db) {
7
+ this._service = _service;
8
+ this._user = _user;
9
+ this._password = _password;
10
+ this._db = _db;
11
+ }
12
+ static async start({ user = "user", password = "password", db = "postgres" } = {}) {
13
+ const service = await GenericService.start({
14
+ imageName: "postgres:latest",
15
+ ports: [POSTGRES_PORT],
16
+ healthcheck: {
17
+ test: ["CMD-SHELL", "pg_isready"],
18
+ intervalMs: ms("1s"),
19
+ retries: 10,
20
+ startPeriodMs: 0,
21
+ timeoutMs: ms("5s")
22
+ },
23
+ env: {
24
+ "POSTGRES_USER": user,
25
+ "POSTGRES_PASSWORD": password,
26
+ "POSTGRES_DB": db,
27
+ // Duplicate env variables for the healthchecks.
28
+ "PGUSER": user,
29
+ "PGPASSWORD": password,
30
+ "PGDATABASE": db
31
+ }
32
+ });
33
+ return new Postgres(service, user, password, db);
34
+ }
35
+ databaseUrl() {
36
+ return `postgres://${this._user}:${this._password}@localhost:${this._service.mappedPort(POSTGRES_PORT)}/${this._db}`;
37
+ }
38
+ connectOptions() {
39
+ return {
40
+ host: "localhost",
41
+ port: this._service.mappedPort(POSTGRES_PORT),
42
+ database: this._db,
43
+ user: this._user,
44
+ password: this._password
45
+ };
46
+ }
47
+ async stop() {
48
+ await this._service.stop();
49
+ }
50
+ }
51
+ export {
52
+ Postgres
53
+ };
package/package.json ADDED
@@ -0,0 +1,38 @@
1
+ {
2
+ "name": "podkeeper",
3
+ "type": "module",
4
+ "version": "0.3.0",
5
+ "description": "",
6
+ "exports": {
7
+ ".": {
8
+ "types": "./types/index.d.ts",
9
+ "import": "./lib/index.js",
10
+ "require": "./lib/index.js"
11
+ }
12
+ },
13
+ "main": "index.js",
14
+ "scripts": {
15
+ "test": "echo \"Error: no test specified\" && exit 1"
16
+ },
17
+ "repository": {
18
+ "type": "git",
19
+ "url": "https://github.com/flakiness/podkeeper.git"
20
+ },
21
+ "homepage": "https://github.com/flakiness/podkeeper",
22
+ "keywords": [],
23
+ "author": "",
24
+ "license": "MIT",
25
+ "devDependencies": {
26
+ "@degulabs/build": "^0.0.1",
27
+ "@playwright/test": "^1.48.2",
28
+ "@types/ms": "^0.7.34",
29
+ "@types/node": "^22.8.5",
30
+ "@types/ws": "^8.5.12",
31
+ "esbuild": "^0.24.0",
32
+ "typescript": "^5.6.3"
33
+ },
34
+ "dependencies": {
35
+ "ms": "^2.1.3",
36
+ "ws": "^8.18.0"
37
+ }
38
+ }
@@ -0,0 +1,71 @@
1
+ export interface DockerImage {
2
+ imageId: string;
3
+ names: string[];
4
+ }
5
+ export interface PortBinding {
6
+ ip: string;
7
+ hostPort: number;
8
+ containerPort: number;
9
+ }
10
+ export interface DockerContainer {
11
+ containerId: string;
12
+ labels: Record<string, string>;
13
+ imageId: string;
14
+ state: 'created' | 'restarting' | 'running' | 'removing' | 'paused' | 'exited' | 'dead';
15
+ names: string[];
16
+ portBindings: PortBinding[];
17
+ }
18
+ export declare function listContainers(): Promise<DockerContainer[]>;
19
+ export declare function isContainerHealthy(containerId: string): Promise<boolean>;
20
+ interface LaunchContainerOptions {
21
+ imageId: string;
22
+ autoRemove: boolean;
23
+ command?: string[];
24
+ entrypoint?: string[];
25
+ labels?: Record<string, string>;
26
+ ports?: {
27
+ container: number;
28
+ host: number;
29
+ }[];
30
+ name?: string;
31
+ healthcheck?: {
32
+ test: string[];
33
+ intervalMs: number;
34
+ timeoutMs: number;
35
+ retries: number;
36
+ startPeriodMs: number;
37
+ };
38
+ binds?: {
39
+ hostPath: string;
40
+ containerPath: string;
41
+ }[];
42
+ workingDir?: string;
43
+ waitUntil?: 'not-running' | 'next-exit' | 'removed';
44
+ env?: {
45
+ [key: string]: string | number | boolean | undefined;
46
+ };
47
+ }
48
+ export declare function launchContainer(options: LaunchContainerOptions): Promise<string>;
49
+ interface StopContainerOptions {
50
+ containerId: string;
51
+ waitUntil?: 'not-running' | 'next-exit' | 'removed';
52
+ }
53
+ export declare function stopContainer(options: StopContainerOptions): Promise<void>;
54
+ export declare function removeContainer(containerId: string): Promise<void>;
55
+ export declare function getContainerLogs(containerId: string): Promise<string[]>;
56
+ interface CommitContainerOptions {
57
+ containerId: string;
58
+ repo: string;
59
+ tag: string;
60
+ entrypoint?: string[];
61
+ workingDir?: string;
62
+ env?: {
63
+ [key: string]: string | number | boolean | undefined;
64
+ };
65
+ }
66
+ export declare function commitContainer(options: CommitContainerOptions): Promise<void>;
67
+ export declare function inspectImage(imageId: string): Promise<any>;
68
+ export declare function listImages(): Promise<DockerImage[]>;
69
+ export declare function removeImage(imageId: string): Promise<void>;
70
+ export declare function checkEngineRunning(): Promise<boolean>;
71
+ export {};
@@ -0,0 +1,25 @@
1
+ import { WebSocket } from 'ws';
2
+ import * as dockerApi from './dockerApi.js';
3
+ export declare class GenericService {
4
+ private _containerId;
5
+ private _bindings;
6
+ private _ws;
7
+ static start(options: {
8
+ imageName: string;
9
+ ports: number[];
10
+ healthcheck?: {
11
+ test: string[];
12
+ intervalMs: number;
13
+ retries: number;
14
+ startPeriodMs: number;
15
+ timeoutMs: number;
16
+ };
17
+ command?: string[];
18
+ env?: {
19
+ [key: string]: string | number | boolean | undefined;
20
+ };
21
+ }): Promise<GenericService>;
22
+ constructor(_containerId: string, _bindings: dockerApi.PortBinding[], _ws: WebSocket);
23
+ mappedPort(containerPort: number): number | undefined;
24
+ stop(): Promise<void>;
25
+ }
@@ -0,0 +1,5 @@
1
+ export * as dockerApi from './dockerApi.js';
2
+ export * from './genericService.js';
3
+ export * from './minio.js';
4
+ export * from './mysql.js';
5
+ export * from './postgres.js';
@@ -0,0 +1,16 @@
1
+ import { GenericService } from './genericService.js';
2
+ export declare class Minio {
3
+ private _service;
4
+ private _accessKeyId;
5
+ private _secretAccessKey;
6
+ static start({ accessKeyId, secretAccessKey }?: {
7
+ accessKeyId?: string | undefined;
8
+ secretAccessKey?: string | undefined;
9
+ }): Promise<Minio>;
10
+ constructor(_service: GenericService, _accessKeyId: string, _secretAccessKey: string);
11
+ accessKeyId(): string;
12
+ secretAccessKey(): string;
13
+ apiEndpoint(): string;
14
+ webuiEndpoint(): string;
15
+ stop(): Promise<void>;
16
+ }
@@ -0,0 +1,20 @@
1
+ import { GenericService } from './genericService.js';
2
+ export declare class MySQL {
3
+ private _service;
4
+ private _rootPassword;
5
+ private _db;
6
+ static start({ db, rootPassword }?: {
7
+ db?: string | undefined;
8
+ rootPassword?: string | undefined;
9
+ }): Promise<MySQL>;
10
+ constructor(_service: GenericService, _rootPassword: string, _db: string);
11
+ databaseUrl(): string;
12
+ connectOptions(): {
13
+ host: string;
14
+ port: number;
15
+ database: string;
16
+ user: string;
17
+ password: string;
18
+ };
19
+ stop(): Promise<void>;
20
+ }
@@ -0,0 +1,22 @@
1
+ import { GenericService } from './genericService.js';
2
+ export declare class Postgres {
3
+ private _service;
4
+ private _user;
5
+ private _password;
6
+ private _db;
7
+ static start({ user, password, db }?: {
8
+ user?: string | undefined;
9
+ password?: string | undefined;
10
+ db?: string | undefined;
11
+ }): Promise<Postgres>;
12
+ constructor(_service: GenericService, _user: string, _password: string, _db: string);
13
+ databaseUrl(): string;
14
+ connectOptions(): {
15
+ host: string;
16
+ port: number;
17
+ database: string;
18
+ user: string;
19
+ password: string;
20
+ };
21
+ stop(): Promise<void>;
22
+ }