signalk-container 0.2.0 → 1.0.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/NOTICE +22 -0
- package/README.md +127 -28
- package/dist/containers.d.ts +72 -0
- package/dist/containers.d.ts.map +1 -1
- package/dist/containers.js +235 -2
- package/dist/containers.js.map +1 -1
- package/dist/index.js +296 -1
- package/dist/index.js.map +1 -1
- package/dist/jobs.d.ts.map +1 -1
- package/dist/jobs.js +4 -5
- package/dist/jobs.js.map +1 -1
- package/dist/runtime.d.ts +15 -1
- package/dist/runtime.d.ts.map +1 -1
- package/dist/runtime.js +60 -25
- package/dist/runtime.js.map +1 -1
- package/dist/types.d.ts +96 -0
- package/dist/types.d.ts.map +1 -1
- package/doc/plugin-developer-guide.md +5 -0
- package/icon.svg +17 -0
- package/package.json +4 -3
- package/public/540.js +1 -1
- package/public/main.js +1 -1
- package/public/remoteEntry.js +1 -1
package/NOTICE
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
signalk-container
|
|
2
|
+
Copyright (c) 2026 Dirk Wahrheit
|
|
3
|
+
|
|
4
|
+
This product includes a third-party icon:
|
|
5
|
+
|
|
6
|
+
icon.svg — Lucide "container" icon
|
|
7
|
+
Source: https://lucide.dev / https://github.com/lucide-icons/lucide
|
|
8
|
+
License: ISC License
|
|
9
|
+
Copyright (c) Lucide Icons and Contributors
|
|
10
|
+
|
|
11
|
+
Permission to use, copy, modify, and/or distribute this software for
|
|
12
|
+
any purpose with or without fee is hereby granted, provided that the
|
|
13
|
+
above copyright notice and this permission notice appear in all copies.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL
|
|
16
|
+
WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED
|
|
17
|
+
WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL
|
|
19
|
+
DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR
|
|
20
|
+
PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER
|
|
21
|
+
TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
|
22
|
+
PERFORMANCE OF THIS SOFTWARE.
|
package/README.md
CHANGED
|
@@ -13,7 +13,9 @@ Instead of each plugin implementing its own container orchestration, they delega
|
|
|
13
13
|
- **Resource limits editor** -- interactive UI in the config panel for setting CPU/memory/PID caps per container. Values are applied live via `podman update` when possible (no downtime), falls back to recreate when needed. Stored overrides are minimized against the consumer plugin's defaults so a future default bump flows through automatically. See the [developer guide](doc/plugin-developer-guide.md#resource-limits).
|
|
14
14
|
- **Reset to plugin default** -- one-click restore of a container's original resource limits, clearing any user override.
|
|
15
15
|
- **Image management** -- scheduled pruning of dangling images (weekly/monthly)
|
|
16
|
-
- **
|
|
16
|
+
- **Zero-config data dir sharing** -- `signalkDataMount` mounts the SignalK data directory into any managed container automatically, whether Signal K runs bare-metal, in Docker (named volume), or in Podman (named volume or bind mount). No host paths to configure.
|
|
17
|
+
- **Zero-config container service connectivity** -- `signalkAccessiblePorts` lets the SignalK process connect back to a service running inside a managed container (e.g. an HTTP or TCP server). signalk-container picks the right networking strategy automatically — port binding on the host loopback for bare-metal deployments, or a shared Docker network with DNS for containerised ones. No host ports are exposed unnecessarily.
|
|
18
|
+
- **SELinux support** -- `:Z` volume flags for Podman bind mounts on Fedora/RHEL; named volumes are handled correctly (`:Z` is not applied)
|
|
17
19
|
- **Podman image qualification** -- automatically prefixes `docker.io/` for short image names
|
|
18
20
|
- **Cross-plugin API** -- other plugins use `globalThis.__signalk_containerManager`
|
|
19
21
|
|
|
@@ -71,12 +73,19 @@ if (!containers) {
|
|
|
71
73
|
await containers.ensureRunning("my-service", {
|
|
72
74
|
image: "myorg/myimage",
|
|
73
75
|
tag: "latest",
|
|
74
|
-
|
|
75
|
-
|
|
76
|
+
signalkDataMount: "/data", // resolves to the SignalK data dir, regardless of deployment
|
|
77
|
+
signalkAccessiblePorts: [8080], // port 8080 in the container must be reachable by SignalK
|
|
76
78
|
env: { MY_VAR: "value" },
|
|
77
79
|
restart: "unless-stopped",
|
|
78
80
|
});
|
|
79
81
|
|
|
82
|
+
// Get the actual address to connect to (resolved after ensureRunning)
|
|
83
|
+
const addr = await containers.resolveContainerAddress("my-service", 8080);
|
|
84
|
+
if (!addr) throw new Error("Container address not available");
|
|
85
|
+
// bare-metal → "127.0.0.1:8080" (or "127.0.0.1:8081" if 8080 was taken)
|
|
86
|
+
// containerised → "sk-my-service:8080" (Docker DNS, no host port exposed)
|
|
87
|
+
const response = await fetch(`http://${addr}/status`);
|
|
88
|
+
|
|
80
89
|
// Run a one-shot job
|
|
81
90
|
const result = await containers.runJob({
|
|
82
91
|
image: "myorg/converter",
|
|
@@ -91,31 +100,33 @@ See [doc/plugin-developer-guide.md](doc/plugin-developer-guide.md) for the full
|
|
|
91
100
|
|
|
92
101
|
## API
|
|
93
102
|
|
|
94
|
-
| Method | Description
|
|
95
|
-
| --------------------------------------- |
|
|
96
|
-
| `getRuntime()` | Returns `{ runtime, version, isPodmanDockerShim }` or `null`
|
|
97
|
-
| `pullImage(image, onProgress?)` | Pull a container image (auto-qualifies for Podman)
|
|
98
|
-
| `imageExists(image)` | Check if image exists locally
|
|
99
|
-
| `getImageDigest(imageOrContainer)` | Local image ID (sha256) for an image:tag or container
|
|
100
|
-
| `ensureRunning(name, config, options?)` | Create and start container if not running
|
|
101
|
-
| `start(name)` | Start a stopped container
|
|
102
|
-
| `stop(name)` | Stop a running container
|
|
103
|
-
| `remove(name)` | Stop and remove a container
|
|
104
|
-
| `getState(name)` | Returns `running`, `stopped`, `missing`, or `no-runtime`
|
|
105
|
-
| `runJob(config)` | Execute a one-shot container job
|
|
106
|
-
| `prune()` | Remove dangling images
|
|
107
|
-
| `listContainers()` | List all `sk-` prefixed containers
|
|
108
|
-
| `execInContainer(name, command)` | Run a command inside a running container
|
|
109
|
-
| `ensureNetwork(name)` | Create a Podman/Docker network if it doesn't exist
|
|
110
|
-
| `removeNetwork(name)` | Remove a network
|
|
111
|
-
| `connectToNetwork(container, network)` | Add a container to a network (bridge mode only)
|
|
112
|
-
| `disconnectFromNetwork(container, net)` | Remove a container from a network
|
|
113
|
-
| `updates.register(reg)` | Register a container for update detection
|
|
114
|
-
| `updates.unregister(pluginId)` | Stop tracking updates for a plugin
|
|
115
|
-
| `updates.checkOne(pluginId)` | Force a fresh update check (or coalesce with in-flight)
|
|
116
|
-
| `updates.getLastResult(pluginId)` | Cached last result, no network
|
|
117
|
-
| `updateResources(name, limits)` | Apply new resource limits live, fall back to recreate
|
|
118
|
-
| `getResources(name)` | Currently effective limits (plugin defaults ⊕ user override)
|
|
103
|
+
| Method | Description |
|
|
104
|
+
| --------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
105
|
+
| `getRuntime()` | Returns `{ runtime, version, isPodmanDockerShim }` or `null` |
|
|
106
|
+
| `pullImage(image, onProgress?)` | Pull a container image (auto-qualifies for Podman) |
|
|
107
|
+
| `imageExists(image)` | Check if image exists locally |
|
|
108
|
+
| `getImageDigest(imageOrContainer)` | Local image ID (sha256) for an image:tag or container |
|
|
109
|
+
| `ensureRunning(name, config, options?)` | Create and start container if not running |
|
|
110
|
+
| `start(name)` | Start a stopped container |
|
|
111
|
+
| `stop(name)` | Stop a running container |
|
|
112
|
+
| `remove(name)` | Stop and remove a container |
|
|
113
|
+
| `getState(name)` | Returns `running`, `stopped`, `missing`, or `no-runtime` |
|
|
114
|
+
| `runJob(config)` | Execute a one-shot container job |
|
|
115
|
+
| `prune()` | Remove dangling images |
|
|
116
|
+
| `listContainers()` | List all `sk-` prefixed containers |
|
|
117
|
+
| `execInContainer(name, command)` | Run a command inside a running container |
|
|
118
|
+
| `ensureNetwork(name)` | Create a Podman/Docker network if it doesn't exist |
|
|
119
|
+
| `removeNetwork(name)` | Remove a network |
|
|
120
|
+
| `connectToNetwork(container, network)` | Add a container to a network (bridge mode only) |
|
|
121
|
+
| `disconnectFromNetwork(container, net)` | Remove a container from a network |
|
|
122
|
+
| `updates.register(reg)` | Register a container for update detection |
|
|
123
|
+
| `updates.unregister(pluginId)` | Stop tracking updates for a plugin |
|
|
124
|
+
| `updates.checkOne(pluginId)` | Force a fresh update check (or coalesce with in-flight) |
|
|
125
|
+
| `updates.getLastResult(pluginId)` | Cached last result, no network |
|
|
126
|
+
| `updateResources(name, limits)` | Apply new resource limits live, fall back to recreate |
|
|
127
|
+
| `getResources(name)` | Currently effective limits (plugin defaults ⊕ user override) |
|
|
128
|
+
| `resolveSignalkDataMount()` | Resolve the volume name or host path that backs `app.getDataDirPath()` in the current deployment; returns `null` if the runtime is not yet initialised |
|
|
129
|
+
| `resolveContainerAddress(name, port)` | Return the `host:port` string to reach `port` on a managed container from the SignalK process; call after `ensureRunning()` with `signalkAccessiblePorts` set |
|
|
119
130
|
|
|
120
131
|
## REST Endpoints
|
|
121
132
|
|
|
@@ -148,6 +159,88 @@ All mounted at `/plugins/signalk-container/api/`:
|
|
|
148
159
|
| Background update checks | `true` | Periodically check for updates in the background. Disable on metered connections — manual checks via the UI button still work. |
|
|
149
160
|
| Container overrides | `{}` | Per-container resource limits (CPU, memory, PIDs). Field-level merged on top of consumer plugin defaults. See dev guide. |
|
|
150
161
|
|
|
162
|
+
## Mounting the SignalK data directory (`signalkDataMount`)
|
|
163
|
+
|
|
164
|
+
When a managed container needs to read or write files that Signal K also accesses (e.g. HLS segments, exports, caches), use `signalkDataMount` instead of computing and hardcoding a host path or volume name.
|
|
165
|
+
|
|
166
|
+
```typescript
|
|
167
|
+
const SK_MOUNT = "/signalk-data";
|
|
168
|
+
|
|
169
|
+
await containers.ensureRunning("my-worker", {
|
|
170
|
+
image: "myorg/myworker",
|
|
171
|
+
tag: "latest",
|
|
172
|
+
signalkDataMount: SK_MOUNT, // ← mount the SignalK data dir here
|
|
173
|
+
command: ["--output", path.join(SK_MOUNT, "my-plugin/output/result.bin")],
|
|
174
|
+
});
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
signalk-container resolves the correct source automatically:
|
|
178
|
+
|
|
179
|
+
| Deployment | What gets mounted |
|
|
180
|
+
| ------------------------------ | ----------------------------------------------------------------- |
|
|
181
|
+
| Bare-metal Signal K | `app.getDataDirPath()` as a bind mount (already a host path) |
|
|
182
|
+
| Docker, volume-backed data dir | the named volume (e.g. `mystack_signalk-data`) |
|
|
183
|
+
| Docker, bind-backed data dir | the exact host path, even when a parent directory is bind-mounted |
|
|
184
|
+
| Podman (rootless or root) | same logic; named volumes receive no `:Z` flag |
|
|
185
|
+
|
|
186
|
+
The content at `SK_MOUNT` inside the managed container always corresponds to the root of `app.getDataDirPath()`. Build paths using `path.join`:
|
|
187
|
+
|
|
188
|
+
```typescript
|
|
189
|
+
// Path inside managed container that corresponds to an absolute SignalK path:
|
|
190
|
+
const containerPath = path.join(
|
|
191
|
+
SK_MOUNT,
|
|
192
|
+
path.relative(app.getDataDirPath(), absSignalkPath),
|
|
193
|
+
);
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
> [!note]
|
|
197
|
+
> Docker/Podman do not support subpath mounts on named volumes. If your data directory
|
|
198
|
+
> is backed by a named volume, the entire volume is mounted at `SK_MOUNT`. Avoid writing
|
|
199
|
+
> to paths inside `SK_MOUNT` that are also bind-mounted in the Signal K container (e.g.
|
|
200
|
+
> a plugin's own directory if mounted with `./:/home/node/.signalk/node_modules/my-plugin`)
|
|
201
|
+
> — those paths are not visible from inside the managed container.
|
|
202
|
+
|
|
203
|
+
You can also call `containers.resolveSignalkDataMount()` if you need to inspect the resolved source at runtime (e.g. for logging).
|
|
204
|
+
|
|
205
|
+
## Connecting back to a container service (`signalkAccessiblePorts`)
|
|
206
|
+
|
|
207
|
+
When a managed container exposes an HTTP, TCP, or other service that the SignalK process itself needs to connect to (e.g. a video stream, a database, an inference engine), use `signalkAccessiblePorts` instead of hardcoding port bindings or writing deployment-detection logic in your plugin.
|
|
208
|
+
|
|
209
|
+
```typescript
|
|
210
|
+
const STREAM_PORT = 8090;
|
|
211
|
+
|
|
212
|
+
await containers.ensureRunning("my-streamer", {
|
|
213
|
+
image: "myorg/streamer",
|
|
214
|
+
tag: "latest",
|
|
215
|
+
signalkAccessiblePorts: [STREAM_PORT],
|
|
216
|
+
restart: "unless-stopped",
|
|
217
|
+
command: ["--listen", String(STREAM_PORT)],
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
const addr = await containers.resolveContainerAddress(
|
|
221
|
+
"my-streamer",
|
|
222
|
+
STREAM_PORT,
|
|
223
|
+
);
|
|
224
|
+
if (!addr) throw new Error("Container address not available");
|
|
225
|
+
// Connect from the SignalK process — addr is always the right host:port:
|
|
226
|
+
http.get(`http://${addr}/stream`, handleResponse);
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
signalk-container resolves the correct networking strategy automatically:
|
|
230
|
+
|
|
231
|
+
| Deployment | Strategy | Address returned |
|
|
232
|
+
| -------------------------------------- | ------------------------------------------------------------------------------- | -------------------------------------------------------- |
|
|
233
|
+
| Bare-metal Signal K | Port bound to `127.0.0.1` (first free port ≥ declared value) | `127.0.0.1:8090` (or `127.0.0.1:8091` if 8090 was taken) |
|
|
234
|
+
| Containerised, user-defined network | Container attached to SignalK's own Docker/Podman network; no host port exposed | `sk-my-streamer:8090` (Docker DNS) |
|
|
235
|
+
| Containerised, default bridge (no DNS) | Container shares SignalK's network namespace | `127.0.0.1:8090` |
|
|
236
|
+
|
|
237
|
+
The allocated address is cached for the lifetime of the plugin session, so repeated `ensureRunning()` calls never trigger an unwanted container recreate due to a port number change.
|
|
238
|
+
|
|
239
|
+
> [!note]
|
|
240
|
+
> `signalkAccessiblePorts` sets up networking automatically. Do not combine it with
|
|
241
|
+
> a manual `ports` or `networkMode` entry for the same container — the field takes
|
|
242
|
+
> full ownership of those concerns.
|
|
243
|
+
|
|
151
244
|
## Setting Resource Limits
|
|
152
245
|
|
|
153
246
|
On a boat with limited compute (typically a Pi 4/5 or low-power x86 mini PC), one runaway container can starve Signal K, raise NMEA decode latency, trigger thermal throttling, or even take the host down via OOM. signalk-container exposes podman/docker resource flags so consumer plugins can set sensible defaults — and you, as the user, can tune them per-container in two ways: **the config panel UI (recommended)** or direct JSON edit (for scripted/automated setups).
|
|
@@ -340,3 +433,9 @@ ecosystem (signalk-questdb, signalk-grafana) are designed for this case.
|
|
|
340
433
|
## License
|
|
341
434
|
|
|
342
435
|
MIT
|
|
436
|
+
|
|
437
|
+
## Acknowledgements
|
|
438
|
+
|
|
439
|
+
The plugin icon (`icon.svg`) is the **container** glyph from
|
|
440
|
+
[Lucide Icons](https://lucide.dev), used under the ISC License. See
|
|
441
|
+
[`NOTICE`](NOTICE) for the full attribution.
|
package/dist/containers.d.ts
CHANGED
|
@@ -1,4 +1,16 @@
|
|
|
1
1
|
import { ContainerConfig, ContainerInfo, ContainerRuntimeInfo, ContainerState, HealthCheckOptions } from "./types";
|
|
2
|
+
/**
|
|
3
|
+
* Build the value for a `-v <source>:<dest>[:flags]` argument with the
|
|
4
|
+
* correct SELinux relabel suffix for the runtime.
|
|
5
|
+
*
|
|
6
|
+
* `:Z` is for SELinux relabelling of bind-mount host paths under Podman
|
|
7
|
+
* on Fedora/RHEL. Named volumes (no leading '/' or '.') reject `:Z` with
|
|
8
|
+
* "invalid option z for named volume", so we omit the flag for them.
|
|
9
|
+
*
|
|
10
|
+
* Used by both ContainerConfig.volumes (containers.ts) and JobConfig
|
|
11
|
+
* inputs/outputs (jobs.ts) so the named-volume guard stays in one place.
|
|
12
|
+
*/
|
|
13
|
+
export declare function volumeArg(hostPath: string, containerPath: string, runtime: ContainerRuntimeInfo, readOnly?: boolean): string;
|
|
2
14
|
export declare function qualifyImage(image: string, runtime: ContainerRuntimeInfo): string;
|
|
3
15
|
export declare function imageExists(runtime: ContainerRuntimeInfo, image: string): Promise<boolean>;
|
|
4
16
|
/**
|
|
@@ -63,6 +75,66 @@ export declare function ensureNetwork(runtime: ContainerRuntimeInfo, name: strin
|
|
|
63
75
|
export declare function removeNetwork(runtime: ContainerRuntimeInfo, name: string): Promise<void>;
|
|
64
76
|
export declare function connectToNetwork(runtime: ContainerRuntimeInfo, containerName: string, networkName: string): Promise<void>;
|
|
65
77
|
export declare function disconnectFromNetwork(runtime: ContainerRuntimeInfo, containerName: string, networkName: string): Promise<void>;
|
|
78
|
+
/**
|
|
79
|
+
* Resolve what to mount in a managed container to give it access to
|
|
80
|
+
* the SignalK data directory, regardless of how SignalK itself is deployed.
|
|
81
|
+
*
|
|
82
|
+
* Returns the string to use as the LEFT side of a `-v <source>:<dest>` flag:
|
|
83
|
+
* - Bare-metal SignalK: returns dataDir directly (it is already a host path).
|
|
84
|
+
* - SignalK in Docker, volume-backed dataDir: returns the named volume.
|
|
85
|
+
* - SignalK in Docker, bind-backed dataDir: returns the exact host path
|
|
86
|
+
* (computing the subpath when a parent directory is bind-mounted).
|
|
87
|
+
* - Fallback (mount not found): returns dataDir — the caller's `-v` will
|
|
88
|
+
* fail gracefully at container-create time with a clear Docker error.
|
|
89
|
+
*
|
|
90
|
+
* The result can be used directly as `volumes: { [mountPoint]: source }` in
|
|
91
|
+
* a ContainerConfig. The content visible at mountPoint inside the managed
|
|
92
|
+
* container will always correspond to the root of dataDir.
|
|
93
|
+
*/
|
|
94
|
+
export declare function resolveSignalkDataSource(dataDir: string, runtime: ContainerRuntimeInfo, debug?: (msg: string) => void): Promise<string>;
|
|
95
|
+
/**
|
|
96
|
+
* Release a port that was reserved by `findAvailablePort()`.
|
|
97
|
+
* Must be called after the container runtime has successfully bound the port
|
|
98
|
+
* (so the OS-level bind now prevents collisions), or when the container
|
|
99
|
+
* creation failed (so the next attempt can re-probe freely).
|
|
100
|
+
*/
|
|
101
|
+
export declare function releaseReservedPort(port: number): void;
|
|
102
|
+
/**
|
|
103
|
+
* Find the lowest available TCP port on 127.0.0.1 starting at `preferred`.
|
|
104
|
+
*
|
|
105
|
+
* Probes by briefly binding a server socket and also skips ports that are
|
|
106
|
+
* already reserved in-process by a concurrent `findAvailablePort()` call,
|
|
107
|
+
* eliminating the TOCTOU window between the probe and the container create.
|
|
108
|
+
*
|
|
109
|
+
* The chosen port is added to the process-local `reservedPorts` set before
|
|
110
|
+
* this function resolves. The caller is responsible for releasing it via
|
|
111
|
+
* `releaseReservedPort()` once the runtime holds the binding or the attempt
|
|
112
|
+
* fails.
|
|
113
|
+
*
|
|
114
|
+
* Used by the `signalkAccessiblePorts` bare-metal path to prefer the
|
|
115
|
+
* declared port number while gracefully stepping over conflicts.
|
|
116
|
+
*/
|
|
117
|
+
export declare function findAvailablePort(preferred: number): Promise<number>;
|
|
118
|
+
/**
|
|
119
|
+
* Return the user-defined Docker/Podman networks that the current SignalK
|
|
120
|
+
* container is connected to (i.e. networks other than the default `bridge`,
|
|
121
|
+
* `host`, or `none`).
|
|
122
|
+
*
|
|
123
|
+
* Used by the `signalkAccessiblePorts` containerized path to attach a
|
|
124
|
+
* managed container to SignalK's own network so the two can communicate
|
|
125
|
+
* via DNS name without exposing any host port.
|
|
126
|
+
*
|
|
127
|
+
* Returns:
|
|
128
|
+
* - `null` when running bare-metal, HOSTNAME is unset, or `docker inspect`
|
|
129
|
+
* fails (e.g. host-network mode where HOSTNAME is the machine
|
|
130
|
+
* name, not a container ID). Callers should treat this like
|
|
131
|
+
* bare-metal and publish ports instead.
|
|
132
|
+
* - `string[]` (possibly empty) when inspect succeeds. An empty array means
|
|
133
|
+
* SignalK is only on the default bridge — callers should fall
|
|
134
|
+
* back to `networkMode: container:<HOSTNAME>`. A non-empty
|
|
135
|
+
* array contains the user-defined network names to attach to.
|
|
136
|
+
*/
|
|
137
|
+
export declare function resolveSignalkNetworks(runtime: ContainerRuntimeInfo, debug?: (msg: string) => void): Promise<string[] | null>;
|
|
66
138
|
export declare function waitForReady(url: string, timeoutMs?: number, intervalMs?: number): Promise<void>;
|
|
67
139
|
export {};
|
|
68
140
|
//# sourceMappingURL=containers.d.ts.map
|
package/dist/containers.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"containers.d.ts","sourceRoot":"","sources":["../src/containers.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"containers.d.ts","sourceRoot":"","sources":["../src/containers.ts"],"names":[],"mappings":"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"}
|
package/dist/containers.js
CHANGED
|
@@ -1,5 +1,39 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
2
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.volumeArg = volumeArg;
|
|
3
37
|
exports.qualifyImage = qualifyImage;
|
|
4
38
|
exports.imageExists = imageExists;
|
|
5
39
|
exports.getImageDigest = getImageDigest;
|
|
@@ -17,7 +51,12 @@ exports.ensureNetwork = ensureNetwork;
|
|
|
17
51
|
exports.removeNetwork = removeNetwork;
|
|
18
52
|
exports.connectToNetwork = connectToNetwork;
|
|
19
53
|
exports.disconnectFromNetwork = disconnectFromNetwork;
|
|
54
|
+
exports.resolveSignalkDataSource = resolveSignalkDataSource;
|
|
55
|
+
exports.releaseReservedPort = releaseReservedPort;
|
|
56
|
+
exports.findAvailablePort = findAvailablePort;
|
|
57
|
+
exports.resolveSignalkNetworks = resolveSignalkNetworks;
|
|
20
58
|
exports.waitForReady = waitForReady;
|
|
59
|
+
const net = __importStar(require("net"));
|
|
21
60
|
const runtime_1 = require("./runtime");
|
|
22
61
|
const resources_1 = require("./resources");
|
|
23
62
|
const CONTAINER_PREFIX = "sk-";
|
|
@@ -26,6 +65,27 @@ function prefixedName(name) {
|
|
|
26
65
|
? name
|
|
27
66
|
: `${CONTAINER_PREFIX}${name}`;
|
|
28
67
|
}
|
|
68
|
+
/**
|
|
69
|
+
* Build the value for a `-v <source>:<dest>[:flags]` argument with the
|
|
70
|
+
* correct SELinux relabel suffix for the runtime.
|
|
71
|
+
*
|
|
72
|
+
* `:Z` is for SELinux relabelling of bind-mount host paths under Podman
|
|
73
|
+
* on Fedora/RHEL. Named volumes (no leading '/' or '.') reject `:Z` with
|
|
74
|
+
* "invalid option z for named volume", so we omit the flag for them.
|
|
75
|
+
*
|
|
76
|
+
* Used by both ContainerConfig.volumes (containers.ts) and JobConfig
|
|
77
|
+
* inputs/outputs (jobs.ts) so the named-volume guard stays in one place.
|
|
78
|
+
*/
|
|
79
|
+
function volumeArg(hostPath, containerPath, runtime, readOnly = false) {
|
|
80
|
+
const isNamedVolume = !hostPath.startsWith("/") && !hostPath.startsWith(".");
|
|
81
|
+
const flags = [];
|
|
82
|
+
if (readOnly)
|
|
83
|
+
flags.push("ro");
|
|
84
|
+
if (runtime.runtime === "podman" && !isNamedVolume)
|
|
85
|
+
flags.push("Z");
|
|
86
|
+
const suffix = flags.length > 0 ? `:${flags.join(",")}` : "";
|
|
87
|
+
return `${hostPath}:${containerPath}${suffix}`;
|
|
88
|
+
}
|
|
29
89
|
function qualifyImage(image, runtime) {
|
|
30
90
|
// Podman requires fully qualified image names when unqualified-search
|
|
31
91
|
// registries are not configured. Prefix docker.io/ if missing.
|
|
@@ -244,8 +304,7 @@ function buildRunArgs(name, config, runtime) {
|
|
|
244
304
|
}
|
|
245
305
|
if (config.volumes) {
|
|
246
306
|
for (const [containerPath, hostPath] of Object.entries(config.volumes)) {
|
|
247
|
-
|
|
248
|
-
args.push("-v", `${hostPath}:${containerPath}${suffix}`);
|
|
307
|
+
args.push("-v", volumeArg(hostPath, containerPath, runtime));
|
|
249
308
|
}
|
|
250
309
|
}
|
|
251
310
|
if (config.env) {
|
|
@@ -440,6 +499,180 @@ async function disconnectFromNetwork(runtime, containerName, networkName) {
|
|
|
440
499
|
throw new Error(`Failed to disconnect ${fullName} from ${networkName}: ${result.stderr}`);
|
|
441
500
|
}
|
|
442
501
|
}
|
|
502
|
+
/**
|
|
503
|
+
* Resolve what to mount in a managed container to give it access to
|
|
504
|
+
* the SignalK data directory, regardless of how SignalK itself is deployed.
|
|
505
|
+
*
|
|
506
|
+
* Returns the string to use as the LEFT side of a `-v <source>:<dest>` flag:
|
|
507
|
+
* - Bare-metal SignalK: returns dataDir directly (it is already a host path).
|
|
508
|
+
* - SignalK in Docker, volume-backed dataDir: returns the named volume.
|
|
509
|
+
* - SignalK in Docker, bind-backed dataDir: returns the exact host path
|
|
510
|
+
* (computing the subpath when a parent directory is bind-mounted).
|
|
511
|
+
* - Fallback (mount not found): returns dataDir — the caller's `-v` will
|
|
512
|
+
* fail gracefully at container-create time with a clear Docker error.
|
|
513
|
+
*
|
|
514
|
+
* The result can be used directly as `volumes: { [mountPoint]: source }` in
|
|
515
|
+
* a ContainerConfig. The content visible at mountPoint inside the managed
|
|
516
|
+
* container will always correspond to the root of dataDir.
|
|
517
|
+
*/
|
|
518
|
+
async function resolveSignalkDataSource(dataDir, runtime, debug = () => { }) {
|
|
519
|
+
if (!(0, runtime_1.isContainerized)()) {
|
|
520
|
+
// Running bare-metal: dataDir is already a host filesystem path.
|
|
521
|
+
return dataDir;
|
|
522
|
+
}
|
|
523
|
+
// Running inside a container. Docker/Podman set HOSTNAME to the
|
|
524
|
+
// (short) container ID, which is enough for `inspect`.
|
|
525
|
+
const selfId = process.env.HOSTNAME ?? "";
|
|
526
|
+
if (!selfId) {
|
|
527
|
+
debug(`resolveSignalkDataSource: HOSTNAME unset, falling back to dataDir=${dataDir}`);
|
|
528
|
+
return dataDir;
|
|
529
|
+
}
|
|
530
|
+
const result = await (0, runtime_1.execRuntime)(runtime, [
|
|
531
|
+
"inspect",
|
|
532
|
+
"--format",
|
|
533
|
+
"{{range .Mounts}}{{.Type}}|{{.Name}}|{{.Source}}|{{.Destination}}\n{{end}}",
|
|
534
|
+
selfId,
|
|
535
|
+
]);
|
|
536
|
+
if (result.exitCode !== 0) {
|
|
537
|
+
debug(`resolveSignalkDataSource: inspect ${selfId} failed (exit=${result.exitCode}): ${result.stderr.trim()}; falling back to dataDir=${dataDir}`);
|
|
538
|
+
return dataDir;
|
|
539
|
+
}
|
|
540
|
+
const mounts = result.stdout
|
|
541
|
+
.split("\n")
|
|
542
|
+
.filter(Boolean)
|
|
543
|
+
.map((line) => {
|
|
544
|
+
const [type, name, source, dest] = line.split("|");
|
|
545
|
+
return { type, name, source, dest };
|
|
546
|
+
});
|
|
547
|
+
// Find the mount whose Destination is the longest prefix of dataDir
|
|
548
|
+
// (handles both exact matches and parent-directory bind mounts).
|
|
549
|
+
let best = null;
|
|
550
|
+
for (const m of mounts) {
|
|
551
|
+
if (dataDir === m.dest || dataDir.startsWith(m.dest + "/")) {
|
|
552
|
+
if (!best || m.dest.length > best.dest.length) {
|
|
553
|
+
best = m;
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
if (!best) {
|
|
558
|
+
debug(`resolveSignalkDataSource: no mount covers dataDir=${dataDir}; mounts=${JSON.stringify(mounts)}; falling back to dataDir`);
|
|
559
|
+
return dataDir;
|
|
560
|
+
}
|
|
561
|
+
if (best.type === "volume") {
|
|
562
|
+
// Named volume. Docker doesn't support subpath mounts on volumes,
|
|
563
|
+
// so we return the volume name as-is. The consumer's mount point
|
|
564
|
+
// will correspond to best.dest; if that equals dataDir (the common
|
|
565
|
+
// case) the consumer can use mountPoint directly. If best.dest is a
|
|
566
|
+
// parent of dataDir, the consumer must append the relative suffix —
|
|
567
|
+
// signalk-container surfaces this via ContainerManagerApi if needed.
|
|
568
|
+
return best.name;
|
|
569
|
+
}
|
|
570
|
+
// Bind mount. Compute the exact host path that corresponds to dataDir,
|
|
571
|
+
// even when the bind covers a parent directory.
|
|
572
|
+
return best.source + dataDir.slice(best.dest.length);
|
|
573
|
+
}
|
|
574
|
+
/**
|
|
575
|
+
* Process-local set of ports that are currently reserved by an in-flight
|
|
576
|
+
* `findAvailablePort()` call. Prevents two concurrent `ensureRunning()`
|
|
577
|
+
* calls from probing and claiming the same host port before either
|
|
578
|
+
* container has actually been created.
|
|
579
|
+
*
|
|
580
|
+
* Ports are added here just before `findAvailablePort()` resolves and
|
|
581
|
+
* removed via `releaseReservedPort()` once the container runtime holds the
|
|
582
|
+
* binding (successful create) or the attempt fails.
|
|
583
|
+
*/
|
|
584
|
+
const reservedPorts = new Set();
|
|
585
|
+
/**
|
|
586
|
+
* Release a port that was reserved by `findAvailablePort()`.
|
|
587
|
+
* Must be called after the container runtime has successfully bound the port
|
|
588
|
+
* (so the OS-level bind now prevents collisions), or when the container
|
|
589
|
+
* creation failed (so the next attempt can re-probe freely).
|
|
590
|
+
*/
|
|
591
|
+
function releaseReservedPort(port) {
|
|
592
|
+
reservedPorts.delete(port);
|
|
593
|
+
}
|
|
594
|
+
/**
|
|
595
|
+
* Test whether `port` is bindable on 127.0.0.1.
|
|
596
|
+
* Returns `true` if the socket can be opened and closed without error.
|
|
597
|
+
*/
|
|
598
|
+
function isPortAvailable(port) {
|
|
599
|
+
return new Promise((resolve) => {
|
|
600
|
+
const server = net.createServer();
|
|
601
|
+
server.once("error", () => resolve(false));
|
|
602
|
+
server.once("listening", () => server.close(() => resolve(true)));
|
|
603
|
+
server.listen(port, "127.0.0.1");
|
|
604
|
+
});
|
|
605
|
+
}
|
|
606
|
+
/**
|
|
607
|
+
* Find the lowest available TCP port on 127.0.0.1 starting at `preferred`.
|
|
608
|
+
*
|
|
609
|
+
* Probes by briefly binding a server socket and also skips ports that are
|
|
610
|
+
* already reserved in-process by a concurrent `findAvailablePort()` call,
|
|
611
|
+
* eliminating the TOCTOU window between the probe and the container create.
|
|
612
|
+
*
|
|
613
|
+
* The chosen port is added to the process-local `reservedPorts` set before
|
|
614
|
+
* this function resolves. The caller is responsible for releasing it via
|
|
615
|
+
* `releaseReservedPort()` once the runtime holds the binding or the attempt
|
|
616
|
+
* fails.
|
|
617
|
+
*
|
|
618
|
+
* Used by the `signalkAccessiblePorts` bare-metal path to prefer the
|
|
619
|
+
* declared port number while gracefully stepping over conflicts.
|
|
620
|
+
*/
|
|
621
|
+
async function findAvailablePort(preferred) {
|
|
622
|
+
for (let port = preferred; port <= 65535; port++) {
|
|
623
|
+
if (reservedPorts.has(port))
|
|
624
|
+
continue;
|
|
625
|
+
if (await isPortAvailable(port)) {
|
|
626
|
+
reservedPorts.add(port);
|
|
627
|
+
return port;
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
throw new Error("No available port found in range 1024–65535");
|
|
631
|
+
}
|
|
632
|
+
/**
|
|
633
|
+
* Return the user-defined Docker/Podman networks that the current SignalK
|
|
634
|
+
* container is connected to (i.e. networks other than the default `bridge`,
|
|
635
|
+
* `host`, or `none`).
|
|
636
|
+
*
|
|
637
|
+
* Used by the `signalkAccessiblePorts` containerized path to attach a
|
|
638
|
+
* managed container to SignalK's own network so the two can communicate
|
|
639
|
+
* via DNS name without exposing any host port.
|
|
640
|
+
*
|
|
641
|
+
* Returns:
|
|
642
|
+
* - `null` when running bare-metal, HOSTNAME is unset, or `docker inspect`
|
|
643
|
+
* fails (e.g. host-network mode where HOSTNAME is the machine
|
|
644
|
+
* name, not a container ID). Callers should treat this like
|
|
645
|
+
* bare-metal and publish ports instead.
|
|
646
|
+
* - `string[]` (possibly empty) when inspect succeeds. An empty array means
|
|
647
|
+
* SignalK is only on the default bridge — callers should fall
|
|
648
|
+
* back to `networkMode: container:<HOSTNAME>`. A non-empty
|
|
649
|
+
* array contains the user-defined network names to attach to.
|
|
650
|
+
*/
|
|
651
|
+
async function resolveSignalkNetworks(runtime, debug = () => { }) {
|
|
652
|
+
if (!(0, runtime_1.isContainerized)())
|
|
653
|
+
return null;
|
|
654
|
+
const selfId = process.env.HOSTNAME ?? "";
|
|
655
|
+
if (!selfId) {
|
|
656
|
+
debug("resolveSignalkNetworks: HOSTNAME unset, returning null");
|
|
657
|
+
return null;
|
|
658
|
+
}
|
|
659
|
+
const result = await (0, runtime_1.execRuntime)(runtime, [
|
|
660
|
+
"inspect",
|
|
661
|
+
"--format",
|
|
662
|
+
"{{range $k,$v := .NetworkSettings.Networks}}{{$k}}\n{{end}}",
|
|
663
|
+
selfId,
|
|
664
|
+
]);
|
|
665
|
+
if (result.exitCode !== 0) {
|
|
666
|
+
debug(`resolveSignalkNetworks: inspect ${selfId} failed (exit=${result.exitCode}): ${result.stderr.trim()} — treating as bare-metal`);
|
|
667
|
+
return null;
|
|
668
|
+
}
|
|
669
|
+
const all = result.stdout.split("\n").filter(Boolean);
|
|
670
|
+
// The default bridge network does not support container-name DNS
|
|
671
|
+
// resolution, so exclude it along with the virtual modes.
|
|
672
|
+
const userDefined = all.filter((n) => n !== "bridge" && n !== "host" && n !== "none");
|
|
673
|
+
debug(`resolveSignalkNetworks: all=${all.join(",")} userDefined=${userDefined.join(",")}`);
|
|
674
|
+
return userDefined;
|
|
675
|
+
}
|
|
443
676
|
async function waitForReady(url, timeoutMs = 30000, intervalMs = 500) {
|
|
444
677
|
const deadline = Date.now() + timeoutMs;
|
|
445
678
|
while (Date.now() < deadline) {
|