@flisk-sh/flisk-import 0.9.0 → 0.11.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 +3 -2
- package/dist/detectors/compose.js +72 -16
- package/dist/write.js +13 -3
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -12,12 +12,13 @@ flisk import . --yes # write service.json files and project setup
|
|
|
12
12
|
|
|
13
13
|
- **Reads one file:** the root `docker-compose.yml`, or `compose.yaml`, `compose.yml` or `docker-compose.yaml` when that is absent. Not override files, nested Compose files, Dockerfiles on their own, `package.json`, or Procfiles.
|
|
14
14
|
- **Writes:** one bring-your-own `service.json` per Compose service, plus `.flisk/project.json` if the repo is not a Flisk project yet. It never edits the Compose file or overwrites a `service.json` without `--force`.
|
|
15
|
+
- **Keeps the install's data:** each named volume gets `name`, the Docker volume `docker compose up` created (`<project>_<volume>`, its own `name:`, or an `external` volume), so `flisk up` mounts the same data with no copy (needs the Flisk release with flisk#2964).
|
|
15
16
|
- **Keeps:** the service names (`db` stays `db`, so it stays the hostname), the build context and Dockerfile (or the image), the container port as written (3000, 80, 5432), `depends_on`, and an HTTP health path. Databases and brokers are private, with no proxy route.
|
|
16
|
-
- **Runtime keys:** named volumes and bind mounts inside the project (`volumes`), `cap_add`, `cap_drop`, `init`, `read_only`, `user`, `security_opt` and `shm_size` (`container`), extra published ports (`extraPorts`, on 127.0.0.1
|
|
17
|
+
- **Runtime keys:** named volumes and bind mounts inside the project (`volumes`), `cap_add`, `cap_drop`, `init`, `read_only`, `user`, `security_opt` and `shm_size` (`container`), extra published ports (`extraPorts`, on the same host address as in Compose: `"80:80"` stays on every interface, `"127.0.0.1:5432:5432"` on loopback, and another address or a `${VAR:-default}` host side as `publish`), `network_mode: service:<name>` (`sidecarOf`) and `restart`.
|
|
17
18
|
- **Environment:** values that cannot be secrets go into the committed `service.json` `env`: numbers, `true`/`false`, another imported service's address without a user, password or query (`http://invidious:3000`, `redis:6379`), and pure `${VAR}` references. Every other name, and any name that looks like a credential (`PASSWORD`, `TOKEN`, `KEY`, ...), goes into `secrets.required`, with its literal value copied to the project `.env`, which is added to `.gitignore`. Values in `.env` already are kept.
|
|
18
19
|
- **Keeps:** a `command` or `entrypoint` on an image as the `CMD`/`ENTRYPOINT` of a small `Dockerfile` next to `service.json` (Flisk sets no container command), a `healthcheck` as `healthCheck.command`, a published database port as `extraPorts`, and `restart: on-failure:N` as `on-failure`.
|
|
19
20
|
- **Drops, and says so:** `command` on a service with a `build`, `env_file` and every other untranslated key; anonymous volumes and paths outside the project; `network_mode` other than `service:<name>`; services with `profiles`, `extends`, an out-of-tree build or a `${VAR}` image.
|
|
20
|
-
- **Does not start anything.** Run `flisk up` with Flisk core **1.4.
|
|
21
|
+
- **Does not start anything.** Run `flisk up` with Flisk core **1.4.26 or later** (`publish` needs it). The importer does not check the core version.
|
|
21
22
|
- **Exit codes:** `0` when the plan succeeds (`--dry-run`, files written, or all `service.json` files already exist), `1` when no root `docker-compose.yml` or mappable service is found, and `2` when the write prompt is declined or closed without an answer (`Nothing written.`).
|
|
22
23
|
|
|
23
24
|
See [docs/import.md](docs/import.md) for the full mapping and limits. `flisk import` is the core CLI command backed by this package.
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { readFileSync } from "node:fs";
|
|
2
|
-
import { dirname, isAbsolute, join, normalize } from "node:path/posix";
|
|
2
|
+
import { basename, dirname, isAbsolute, join, normalize } from "node:path/posix";
|
|
3
3
|
import { isAlias, isMap, parseDocument, visit } from "yaml";
|
|
4
4
|
import { HEALTH_CHECK_PATH_PATTERN } from "../flisk.js";
|
|
5
5
|
import { registerDetector } from "../registry.js";
|
|
@@ -120,33 +120,65 @@ function containerOf(raw) {
|
|
|
120
120
|
return { ...(Object.keys(container).length ? { container } : {}), dropped };
|
|
121
121
|
}
|
|
122
122
|
/** One published port: container port, host port when Compose names a literal one, and protocol. */
|
|
123
|
+
/** A `publish` value Flisk accepts: digits, dots, colons and `${NAME:-default}` with a port or IPv4 address:port default. */
|
|
124
|
+
const PUBLISH = /^(?:[0-9.:]|\$\{[A-Za-z_][A-Za-z0-9_]*:-[0-9.:]+\})+$/;
|
|
125
|
+
/** The host address and port of a published port. Compose publishes on every interface unless an address is given. */
|
|
126
|
+
function hostSideOf(port, hostIp, hostPort) {
|
|
127
|
+
const hostNumber = hostPort && /^\d+$/.test(hostPort) ? Number.parseInt(hostPort, 10) : undefined;
|
|
128
|
+
const keepHostPort = hostNumber !== undefined && hostNumber >= 1 && hostNumber <= 65535 && hostNumber !== port
|
|
129
|
+
? { hostPort: hostNumber }
|
|
130
|
+
: {};
|
|
131
|
+
if (hostPort?.includes("${")) {
|
|
132
|
+
const publish = hostIp ? `${hostIp}:${hostPort}` : hostPort;
|
|
133
|
+
return PUBLISH.test(publish) ? { publish } : undefined;
|
|
134
|
+
}
|
|
135
|
+
if (hostIp === undefined || hostIp === "" || hostIp === "0.0.0.0") {
|
|
136
|
+
// A bare container port ("3000") gets a random host port from Compose; Flisk keeps it on 127.0.0.1.
|
|
137
|
+
return hostPort === undefined ? {} : { ...keepHostPort, bind: "0.0.0.0" };
|
|
138
|
+
}
|
|
139
|
+
if (hostIp === "127.0.0.1")
|
|
140
|
+
return keepHostPort;
|
|
141
|
+
const publish = `${hostIp}:${hostNumber ?? port}`;
|
|
142
|
+
return /^\d{1,3}(\.\d{1,3}){3}:\d+$/.test(publish) ? { publish } : undefined;
|
|
143
|
+
}
|
|
123
144
|
function publishedPort(entry) {
|
|
124
145
|
const port = containerPort(entry);
|
|
125
146
|
if (port === undefined)
|
|
126
147
|
return undefined;
|
|
127
|
-
let host;
|
|
128
148
|
let protocol = "tcp";
|
|
149
|
+
let hostSide;
|
|
129
150
|
if (isObj(entry)) {
|
|
130
|
-
const published =
|
|
131
|
-
|
|
132
|
-
host = published;
|
|
151
|
+
const published = entry.published === undefined ? undefined : String(entry.published);
|
|
152
|
+
const hostIp = typeof entry.host_ip === "string" ? entry.host_ip : undefined;
|
|
133
153
|
if (entry.protocol === "udp")
|
|
134
154
|
protocol = "udp";
|
|
155
|
+
hostSide = hostSideOf(port, hostIp, published);
|
|
135
156
|
}
|
|
136
157
|
else if (typeof entry === "string") {
|
|
137
158
|
const [mapping, proto] = entry.split("/");
|
|
138
159
|
if (proto === "udp")
|
|
139
160
|
protocol = "udp";
|
|
140
|
-
|
|
141
|
-
const
|
|
142
|
-
|
|
143
|
-
|
|
161
|
+
// [HOST_IP:][HOST_PORT:]CONTAINER_PORT, where the host side may hold ${VAR:-default}, which has colons of its own.
|
|
162
|
+
const match = (mapping ?? "").match(/^(?:(.*):)?([^:]+)$/);
|
|
163
|
+
const host = match?.[1];
|
|
164
|
+
if (host === undefined)
|
|
165
|
+
hostSide = hostSideOf(port, undefined, undefined);
|
|
166
|
+
else if (host.includes("${"))
|
|
167
|
+
hostSide = hostSideOf(port, undefined, host);
|
|
168
|
+
else {
|
|
169
|
+
const at = host.lastIndexOf(":");
|
|
170
|
+
hostSide =
|
|
171
|
+
at === -1
|
|
172
|
+
? hostSideOf(port, undefined, host)
|
|
173
|
+
: hostSideOf(port, host.slice(0, at), host.slice(at + 1));
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
else {
|
|
177
|
+
hostSide = {};
|
|
144
178
|
}
|
|
145
179
|
return {
|
|
146
180
|
port,
|
|
147
|
-
...(
|
|
148
|
-
? { hostPort: host }
|
|
149
|
-
: {}),
|
|
181
|
+
...(hostSide ?? {}),
|
|
150
182
|
...(protocol === "udp" ? { protocol } : {}),
|
|
151
183
|
};
|
|
152
184
|
}
|
|
@@ -189,6 +221,26 @@ function volumeOf(entry, composeDir) {
|
|
|
189
221
|
return undefined;
|
|
190
222
|
return { source: resolved, target, ...(readOnly ? { readOnly } : {}), bind };
|
|
191
223
|
}
|
|
224
|
+
/** Compose's project name: top-level `name`, else the directory name, lowercased with other characters dropped. */
|
|
225
|
+
function projectNameOf(root, absPath) {
|
|
226
|
+
const raw = isObj(root) && typeof root.name === "string" && !root.name.includes("$")
|
|
227
|
+
? root.name
|
|
228
|
+
: basename(dirname(absPath));
|
|
229
|
+
const name = raw.toLowerCase().replace(/[^a-z0-9_-]/g, "");
|
|
230
|
+
return /^[a-z0-9]/.test(name) ? name : undefined;
|
|
231
|
+
}
|
|
232
|
+
/**
|
|
233
|
+
* The Docker name of a named volume, as `docker compose up` created it: its own `name:`, the key itself when it is
|
|
234
|
+
* `external`, else `<project>_<key>`. Undefined when the name uses Compose variables.
|
|
235
|
+
*/
|
|
236
|
+
function dockerVolumeName(key, volumes, project) {
|
|
237
|
+
const def = isObj(volumes) ? volumes[key] : undefined;
|
|
238
|
+
if (isObj(def) && typeof def.name === "string")
|
|
239
|
+
return /^[A-Za-z0-9][A-Za-z0-9_.-]*$/.test(def.name) ? def.name : undefined;
|
|
240
|
+
if (isObj(def) && (def.external === true || isObj(def.external)))
|
|
241
|
+
return key;
|
|
242
|
+
return project ? `${project}_${key}` : undefined;
|
|
243
|
+
}
|
|
192
244
|
function envNames(environment) {
|
|
193
245
|
if (Array.isArray(environment)) {
|
|
194
246
|
return environment
|
|
@@ -358,6 +410,7 @@ registerDetector({
|
|
|
358
410
|
if (!services || !isMap(doc.get("services", true))) {
|
|
359
411
|
return { candidates, skips: [...skips, { ...base, reason: "no `services:` map" }] };
|
|
360
412
|
}
|
|
413
|
+
const project = projectNameOf(root, absPath);
|
|
361
414
|
for (const [name, raw] of Object.entries(services)) {
|
|
362
415
|
if (!isObj(raw)) {
|
|
363
416
|
skips.push({ ...base, reason: `service "${name}" is not a map` });
|
|
@@ -417,13 +470,13 @@ registerDetector({
|
|
|
417
470
|
if (ports.length > 1 && selectedPort === undefined) {
|
|
418
471
|
skips.push({
|
|
419
472
|
...base,
|
|
420
|
-
reason: `service "${name}" lists several ports (${ports.join(", ")}); no single HTTP container port could be identified, so none is routed (all
|
|
473
|
+
reason: `service "${name}" lists several ports (${ports.join(", ")}); no single HTTP container port could be identified, so none is routed (all stay published, as extraPorts)`,
|
|
421
474
|
});
|
|
422
475
|
}
|
|
423
476
|
else if (ports.length > 1) {
|
|
424
477
|
skips.push({
|
|
425
478
|
...base,
|
|
426
|
-
reason: `service "${name}" publishes extra container ports (${ports.filter((port) => port !== selectedPort).join(", ")}); Flisk routes only ${selectedPort} and
|
|
479
|
+
reason: `service "${name}" publishes extra container ports (${ports.filter((port) => port !== selectedPort).join(", ")}); Flisk routes only ${selectedPort} and keeps the others published as extraPorts`,
|
|
427
480
|
});
|
|
428
481
|
}
|
|
429
482
|
const dependsOn = Array.isArray(raw.depends_on)
|
|
@@ -456,7 +509,7 @@ registerDetector({
|
|
|
456
509
|
reason: `service "${name}" ${key} ${JSON.stringify(raw[key])} has values Flisk does not accept; not used`,
|
|
457
510
|
});
|
|
458
511
|
}
|
|
459
|
-
// Every published port Flisk does not route stays published
|
|
512
|
+
// Every published port Flisk does not route stays published on the same host address (service.json extraPorts).
|
|
460
513
|
const proxied = selectedPort !== undefined && !NON_HTTP_PORTS.has(selectedPort);
|
|
461
514
|
const extraPorts = (Array.isArray(raw.ports) ? raw.ports : [])
|
|
462
515
|
.map(publishedPort)
|
|
@@ -477,8 +530,11 @@ registerDetector({
|
|
|
477
530
|
const volumes = [];
|
|
478
531
|
for (const entry of Array.isArray(raw.volumes) ? raw.volumes : []) {
|
|
479
532
|
const volume = volumeOf(entry, composeDir);
|
|
533
|
+
const dockerName = volume && !volume.bind
|
|
534
|
+
? dockerVolumeName(volume.source, isObj(root) ? root.volumes : undefined, project)
|
|
535
|
+
: undefined;
|
|
480
536
|
if (volume)
|
|
481
|
-
volumes.push(volume);
|
|
537
|
+
volumes.push(dockerName ? { ...volume, name: dockerName } : volume);
|
|
482
538
|
else
|
|
483
539
|
skips.push({
|
|
484
540
|
...base,
|
package/dist/write.js
CHANGED
|
@@ -237,8 +237,16 @@ export function planWrites(plan, force) {
|
|
|
237
237
|
const extraPorts = sidecarOf ? [] : (service.extraPorts ?? []);
|
|
238
238
|
if (sidecarOf && service.extraPorts?.length)
|
|
239
239
|
notes.push(`ports not written: a sidecar has none of its own; publish them on "${sidecarOf}"`);
|
|
240
|
-
|
|
241
|
-
|
|
240
|
+
const portText = (p) => `${p.publish ?? p.hostPort ?? p.port}->${p.port}/${p.protocol ?? "tcp"}`;
|
|
241
|
+
const loopback = extraPorts.filter((p) => !p.bind && !p.publish);
|
|
242
|
+
const everywhere = extraPorts.filter((p) => p.bind === "0.0.0.0");
|
|
243
|
+
const fromCompose = extraPorts.filter((p) => p.publish);
|
|
244
|
+
if (loopback.length)
|
|
245
|
+
notes.push(`published on 127.0.0.1 only: ${loopback.map(portText).join(", ")}`);
|
|
246
|
+
if (everywhere.length)
|
|
247
|
+
notes.push(`published on every interface, as in Compose: ${everywhere.map(portText).join(", ")} (flisk doctor lists these)`);
|
|
248
|
+
if (fromCompose.length)
|
|
249
|
+
notes.push(`published as Compose wrote the host side: ${fromCompose.map(portText).join(", ")}`);
|
|
242
250
|
// Bind mounts are relative to the service directory in service.json, and must stay inside the project.
|
|
243
251
|
const serviceDir = dirname(path);
|
|
244
252
|
const volumes = (service.volumes ?? []).flatMap((volume) => {
|
|
@@ -276,10 +284,12 @@ export function planWrites(plan, force) {
|
|
|
276
284
|
...(sidecarOf ? { sidecarOf } : {}),
|
|
277
285
|
...(volumes.length
|
|
278
286
|
? {
|
|
279
|
-
|
|
287
|
+
// `name` keeps the Docker volume the Compose install already has, so its data is used with no copy.
|
|
288
|
+
volumes: volumes.map(({ source, target, readOnly, name }) => ({
|
|
280
289
|
source,
|
|
281
290
|
target,
|
|
282
291
|
...(readOnly ? { readOnly } : {}),
|
|
292
|
+
...(name ? { name } : {}),
|
|
283
293
|
})),
|
|
284
294
|
}
|
|
285
295
|
: {}),
|
package/package.json
CHANGED