@flisk-sh/flisk-import 0.15.0 → 0.16.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 +184 -13
- package/dist/flisk.js +2 -2
- package/dist/merge.js +5 -0
- package/dist/write.js +40 -4
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -10,10 +10,11 @@ flisk import . --yes # write service.json files and project setup
|
|
|
10
10
|
|
|
11
11
|
## What to expect
|
|
12
12
|
|
|
13
|
-
- **Reads one
|
|
13
|
+
- **Reads one project:** the root `docker-compose.yml`, or `compose.yaml`, `compose.yml` or `docker-compose.yaml` when that is absent, through `docker compose config --no-interpolate --no-env-resolution --no-path-resolution` when docker compose is installed, so `include:`, `extends` and anchors are resolved as `docker compose up` resolves them (wger keeps db, cache and powersync in `services/*.yaml`), `${VAR}` templates stay as written, and an included file's paths stay relative to its own folder. Without docker compose (or with `FLISK_IMPORT_DOCKER=0`) the file is read as YAML, and the plan says when that leaves `include` or `extends` unresolved. Not override 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
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).
|
|
16
16
|
- **Keeps:** `profiles` (the service starts only with `flisk up --profile`), `env_file` as `envFile`, `tmpfs` and `deploy.resources.limits` (`container.tmpfs`, `cpus`, `memory`).
|
|
17
|
+
- **Keeps (Flisk 1.4.28):** each `depends_on` condition as a `{ name, condition }` entry (the list form as `service_started`), `build.args` as `buildArgs` (and, for a generated Dockerfile on an image with variables, each variable as a build argument, so `ARG POSTGRES_VERSION` gets the project's value), `logging` (options as strings), bind paths from the environment (`${OM_AGENTS_DIR:-./agents}`, the default rewritten relative to `service.json`), SELinux labels (`:z`, `:Z`, `bind.selinux`) as `selinux`, and images with `${NAME:?message}`. A bind path from a variable without a default (`${STORAGE_DIR}/redis`) and `extra_hosts` with a variable are still not carried over.
|
|
17
18
|
- **Keeps:** `stop_grace_period` (`container.stopGracePeriod`, such as `30s`), the `aliases` of every network (`container.networkAliases`), `extra_hosts` in list or map form (`container.extraHosts`, such as `host.docker.internal:host-gateway`) and `ulimits` (`container.ulimits`). A service renamed to kebab-case (`lychee_api` becomes `lychee-api`) keeps its Compose name as a network alias, so `http://lychee_api:8000` in another service still resolves. `container_name` is dropped with a note: Flisk names containers itself. These fields, an image with variables and `on-failure:N` need the Flisk core release that reads them; an older core does not apply them.
|
|
18
19
|
- **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.
|
|
19
20
|
- **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}` or `${VAR-default}` host side as `publish`), `network_mode: service:<name>` (`sidecarOf`) and `restart`.
|
|
@@ -22,7 +23,7 @@ flisk import . --yes # write service.json files and project setup
|
|
|
22
23
|
- **A tracked `.env`:** a repo that commits its `.env` (Symfony) keeps it as it is, and it is not added to `.gitignore`; values that must stay local go to `.env.local` when git already ignores it, else to `.flisk/.env.import` (added to `.gitignore`), and each service that needs them reads that file as `envFile`.
|
|
23
24
|
- **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 string is split into exec-form arguments as Compose splits it, with no shell, so `command: server /export` on `minio/minio` stays the arguments of its `ENTRYPOINT`; `$$` becomes `$` as Compose passes it to the container; only a string with Compose variables (`${VAR}`, filled from `.env` by Compose, by the container's shell here) or unbalanced quotes runs through `sh -c`, and the plan says so), a `healthcheck` as `healthCheck.command`, a published database port as `extraPorts`, and `restart: on-failure:N` with its retry limit (N from 1 to 100).
|
|
24
25
|
- **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 invalid profile names, `extends`, an out-of-tree build, or an image with characters or variable forms other than `${NAME}`, `${NAME:-default}` and `${NAME-default}` (`postgres:${POSTGRES_VERSION:-18}-alpine` is written as it is).
|
|
25
|
-
- **Does not start anything.** Run `flisk up` with Flisk core **1.4.
|
|
26
|
+
- **Does not start anything.** Run `flisk up` with Flisk core **1.4.28 or later** (dependsOn conditions, buildArgs, logging, bind paths from the environment, SELinux labels and `${NAME:?message}` images need it). The importer does not check the core version.
|
|
26
27
|
- **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.`).
|
|
27
28
|
|
|
28
29
|
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,3 +1,4 @@
|
|
|
1
|
+
import { execFileSync } from "node:child_process";
|
|
1
2
|
import { readFileSync } from "node:fs";
|
|
2
3
|
import { basename, dirname, isAbsolute, join, normalize } from "node:path/posix";
|
|
3
4
|
import { isAlias, isMap, parseDocument, visit } from "yaml";
|
|
@@ -51,6 +52,7 @@ const TRANSLATED_KEYS = new Set([
|
|
|
51
52
|
"env_file",
|
|
52
53
|
"tmpfs",
|
|
53
54
|
"deploy",
|
|
55
|
+
"logging",
|
|
54
56
|
"stop_grace_period",
|
|
55
57
|
"extra_hosts",
|
|
56
58
|
"ulimits",
|
|
@@ -152,7 +154,8 @@ function networkAliasesOf(value) {
|
|
|
152
154
|
bad.push(alias);
|
|
153
155
|
}
|
|
154
156
|
}
|
|
155
|
-
|
|
157
|
+
// Sorted, so the YAML reader and docker compose config (which sorts networks) give the same list.
|
|
158
|
+
return { kept: [...new Set(kept)].sort(), bad };
|
|
156
159
|
}
|
|
157
160
|
/** stop_grace_period, network aliases, extra_hosts and ulimits, and a line for each value that is not carried over. */
|
|
158
161
|
function networkOptionsOf(raw) {
|
|
@@ -271,10 +274,44 @@ function hostSideOf(port, hostIp, hostPort) {
|
|
|
271
274
|
const publish = `${hostIp}:${hostNumber ?? port}`;
|
|
272
275
|
return /^\d{1,3}(\.\d{1,3}){3}:\d+$/.test(publish) ? { publish } : undefined;
|
|
273
276
|
}
|
|
277
|
+
/**
|
|
278
|
+
* The project as `docker compose config` resolves it (include, extends, merges, anchors), with ${VAR} templates and env
|
|
279
|
+
* files left as written. Undefined when docker compose is not available, fails, or FLISK_IMPORT_DOCKER=0.
|
|
280
|
+
*/
|
|
281
|
+
function composeConfigOf(absPath) {
|
|
282
|
+
if (process.env.FLISK_IMPORT_DOCKER === "0")
|
|
283
|
+
return undefined;
|
|
284
|
+
try {
|
|
285
|
+
const out = execFileSync("docker", [
|
|
286
|
+
"compose",
|
|
287
|
+
"--profile",
|
|
288
|
+
"*",
|
|
289
|
+
"-f",
|
|
290
|
+
absPath,
|
|
291
|
+
"config",
|
|
292
|
+
"--no-interpolate",
|
|
293
|
+
"--no-env-resolution",
|
|
294
|
+
// Paths stay as written in the root file; an included file's are rebased onto the project directory.
|
|
295
|
+
"--no-path-resolution",
|
|
296
|
+
"--format",
|
|
297
|
+
"json",
|
|
298
|
+
], {
|
|
299
|
+
cwd: dirname(absPath),
|
|
300
|
+
encoding: "utf8",
|
|
301
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
302
|
+
timeout: 60_000,
|
|
303
|
+
});
|
|
304
|
+
const config = JSON.parse(out);
|
|
305
|
+
return isObj(config) ? config : undefined;
|
|
306
|
+
}
|
|
307
|
+
catch {
|
|
308
|
+
return undefined;
|
|
309
|
+
}
|
|
310
|
+
}
|
|
274
311
|
/** Whether a `ports:` entry names a host port ("8000:8000", "127.0.0.1:8000:8000", `published:`), not only a container port. */
|
|
275
312
|
function hasHostSide(entry) {
|
|
276
313
|
if (isObj(entry))
|
|
277
|
-
return entry.published !== undefined;
|
|
314
|
+
return entry.published !== undefined && entry.published !== "";
|
|
278
315
|
return typeof entry === "string" && (entry.split("/")[0] ?? "").includes(":");
|
|
279
316
|
}
|
|
280
317
|
function publishedPort(entry) {
|
|
@@ -284,7 +321,7 @@ function publishedPort(entry) {
|
|
|
284
321
|
let protocol = "tcp";
|
|
285
322
|
let hostSide;
|
|
286
323
|
if (isObj(entry)) {
|
|
287
|
-
const published = entry.published === undefined ? undefined : String(entry.published);
|
|
324
|
+
const published = entry.published === undefined || entry.published === "" ? undefined : String(entry.published);
|
|
288
325
|
const hostIp = typeof entry.host_ip === "string" ? entry.host_ip : undefined;
|
|
289
326
|
if (entry.protocol === "udp")
|
|
290
327
|
protocol = "udp";
|
|
@@ -318,33 +355,69 @@ function publishedPort(entry) {
|
|
|
318
355
|
...(protocol === "udp" ? { protocol } : {}),
|
|
319
356
|
};
|
|
320
357
|
}
|
|
358
|
+
/** A volume entry in short form for the plan, also when docker compose config wrote it in long form. */
|
|
359
|
+
function volumeText(entry) {
|
|
360
|
+
if (!isObj(entry) || typeof entry.target !== "string")
|
|
361
|
+
return entry;
|
|
362
|
+
const source = typeof entry.source === "string" && entry.source ? `${entry.source}:` : "";
|
|
363
|
+
return `${source}${entry.target}${entry.read_only === true ? ":ro" : ""}`;
|
|
364
|
+
}
|
|
321
365
|
/** One `volumes:` entry. Bind sources are resolved against the Compose file's directory. */
|
|
322
366
|
function volumeOf(entry, composeDir) {
|
|
323
367
|
let source;
|
|
324
368
|
let target;
|
|
325
369
|
let readOnly = false;
|
|
326
370
|
let type;
|
|
371
|
+
let selinux;
|
|
327
372
|
if (typeof entry === "string") {
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
[source, target] = parts;
|
|
332
|
-
readOnly = parts[2] === "ro";
|
|
333
|
-
if (parts[2] !== undefined && parts[2] !== "ro" && parts[2] !== "rw")
|
|
373
|
+
// A `${NAME:-./path}` source has a colon of its own.
|
|
374
|
+
const match = entry.match(/^(\$\{[^}]*\}|[^:]*):([^:]*)(?::([^:]*))?$/);
|
|
375
|
+
if (!match)
|
|
334
376
|
return undefined;
|
|
377
|
+
[, source, target] = match;
|
|
378
|
+
for (const option of match[3] === undefined ? [] : match[3].split(",")) {
|
|
379
|
+
if (option === "ro")
|
|
380
|
+
readOnly = true;
|
|
381
|
+
else if (option === "z" || option === "Z")
|
|
382
|
+
selinux = option;
|
|
383
|
+
else if (option !== "rw")
|
|
384
|
+
return undefined;
|
|
385
|
+
}
|
|
335
386
|
}
|
|
336
387
|
else if (isObj(entry)) {
|
|
337
388
|
({ source, target, type } = entry);
|
|
338
389
|
readOnly = entry.read_only === true;
|
|
339
390
|
if (type !== undefined && type !== "volume" && type !== "bind")
|
|
340
391
|
return undefined;
|
|
392
|
+
const label = isObj(entry.bind) ? entry.bind.selinux : undefined;
|
|
393
|
+
if (label === "z" || label === "Z")
|
|
394
|
+
selinux = label;
|
|
395
|
+
else if (label !== undefined)
|
|
396
|
+
return undefined;
|
|
341
397
|
}
|
|
342
398
|
else {
|
|
343
399
|
return undefined;
|
|
344
400
|
}
|
|
345
401
|
if (typeof source !== "string" || typeof target !== "string" || !CONTAINER_PATH.test(target))
|
|
346
402
|
return undefined;
|
|
403
|
+
// `${NAME:-./path}`: a bind path from the environment, with a default inside the project.
|
|
404
|
+
const fromEnv = source.match(/^\$\{([A-Za-z_][A-Za-z0-9_]*)(:?-)(\.{1,2}(?:\/[A-Za-z0-9_.-]+)*\/?)\}$/);
|
|
405
|
+
if (fromEnv) {
|
|
406
|
+
const resolved = normalize(join(composeDir, fromEnv[3]));
|
|
407
|
+
if (resolved === ".." || resolved.startsWith("../"))
|
|
408
|
+
return undefined;
|
|
409
|
+
return {
|
|
410
|
+
source: resolved,
|
|
411
|
+
target,
|
|
412
|
+
...(readOnly ? { readOnly } : {}),
|
|
413
|
+
bind: true,
|
|
414
|
+
fromEnv: { name: fromEnv[1], op: fromEnv[2] },
|
|
415
|
+
...(selinux ? { selinux } : {}),
|
|
416
|
+
};
|
|
417
|
+
}
|
|
347
418
|
const bind = type === "bind" || source.startsWith(".") || source.startsWith("/");
|
|
419
|
+
if (!bind && selinux)
|
|
420
|
+
return undefined;
|
|
348
421
|
if (!bind) {
|
|
349
422
|
return VOLUME_NAME.test(source)
|
|
350
423
|
? { source, target, ...(readOnly ? { readOnly } : {}), bind }
|
|
@@ -355,7 +428,62 @@ function volumeOf(entry, composeDir) {
|
|
|
355
428
|
const resolved = normalize(join(composeDir, source));
|
|
356
429
|
if (resolved === ".." || resolved.startsWith("../"))
|
|
357
430
|
return undefined;
|
|
358
|
-
return {
|
|
431
|
+
return {
|
|
432
|
+
source: resolved,
|
|
433
|
+
target,
|
|
434
|
+
...(readOnly ? { readOnly } : {}),
|
|
435
|
+
bind,
|
|
436
|
+
...(selinux ? { selinux } : {}),
|
|
437
|
+
};
|
|
438
|
+
}
|
|
439
|
+
const ARG_NAME = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
440
|
+
/** Compose build.args (a map, or a list of NAME=value) as strings; undefined when there are none or one is unusable. */
|
|
441
|
+
function buildArgsOf(build) {
|
|
442
|
+
if (!isObj(build) || build.args === undefined)
|
|
443
|
+
return undefined;
|
|
444
|
+
const pairs = Array.isArray(build.args)
|
|
445
|
+
? build.args.map((a) => typeof a === "string" && a.includes("=")
|
|
446
|
+
? [a.slice(0, a.indexOf("=")), a.slice(a.indexOf("=") + 1)]
|
|
447
|
+
: [String(a), undefined])
|
|
448
|
+
: isObj(build.args)
|
|
449
|
+
? Object.entries(build.args)
|
|
450
|
+
: [];
|
|
451
|
+
const out = {};
|
|
452
|
+
for (const [name, value] of pairs) {
|
|
453
|
+
if (!ARG_NAME.test(name) || value === undefined || value === null || typeof value === "object")
|
|
454
|
+
return undefined;
|
|
455
|
+
const text = String(value);
|
|
456
|
+
if (text.includes("\n"))
|
|
457
|
+
return undefined;
|
|
458
|
+
out[name] = text;
|
|
459
|
+
}
|
|
460
|
+
return Object.keys(out).length ? out : undefined;
|
|
461
|
+
}
|
|
462
|
+
/** Compose logging: a driver and string options (max-file: 3 becomes "3"). */
|
|
463
|
+
function loggingOf(value) {
|
|
464
|
+
if (!isObj(value))
|
|
465
|
+
return undefined;
|
|
466
|
+
const out = {};
|
|
467
|
+
if (value.driver !== undefined) {
|
|
468
|
+
if (typeof value.driver !== "string" || !/^[a-z0-9][a-z0-9_.-]*$/.test(value.driver))
|
|
469
|
+
return undefined;
|
|
470
|
+
out.driver = value.driver;
|
|
471
|
+
}
|
|
472
|
+
if (value.options !== undefined) {
|
|
473
|
+
if (!isObj(value.options))
|
|
474
|
+
return undefined;
|
|
475
|
+
const options = {};
|
|
476
|
+
for (const [key, v] of Object.entries(value.options)) {
|
|
477
|
+
if (!/^[A-Za-z0-9][A-Za-z0-9_.-]*$/.test(key) || v === null || typeof v === "object")
|
|
478
|
+
return undefined;
|
|
479
|
+
const text = String(v);
|
|
480
|
+
if (/[\n\r]/.test(text))
|
|
481
|
+
return undefined;
|
|
482
|
+
options[key] = text;
|
|
483
|
+
}
|
|
484
|
+
out.options = options;
|
|
485
|
+
}
|
|
486
|
+
return Object.keys(out).length ? out : undefined;
|
|
359
487
|
}
|
|
360
488
|
/** Compose's project name: top-level `name`, else the directory name, lowercased with other characters dropped. */
|
|
361
489
|
export function projectNameOf(root, absPath) {
|
|
@@ -784,9 +912,21 @@ registerDetector({
|
|
|
784
912
|
reason: "uses YAML anchors/aliases; values were read after resolution",
|
|
785
913
|
});
|
|
786
914
|
}
|
|
787
|
-
const
|
|
915
|
+
const rawRoot = doc.toJS();
|
|
916
|
+
// `docker compose config` resolves include, extends and merges as `docker compose up` does; the YAML is the fallback.
|
|
917
|
+
const resolved = composeConfigOf(absPath);
|
|
918
|
+
const root = resolved ?? rawRoot;
|
|
919
|
+
const resolves = isObj(rawRoot) &&
|
|
920
|
+
(rawRoot.include !== undefined ||
|
|
921
|
+
(isObj(rawRoot.services) &&
|
|
922
|
+
Object.values(rawRoot.services).some((s) => isObj(s) && s.extends !== undefined)));
|
|
923
|
+
if (!resolved && resolves)
|
|
924
|
+
skips.push({
|
|
925
|
+
...base,
|
|
926
|
+
reason: "uses include or extends, which only `docker compose config` resolves; docker compose was not available (or failed), so the file was read as YAML",
|
|
927
|
+
});
|
|
788
928
|
const services = isObj(root) && isObj(root.services) ? root.services : null;
|
|
789
|
-
if (!services || !isMap(doc.get("services", true))) {
|
|
929
|
+
if (!services || (!resolved && !isMap(doc.get("services", true)))) {
|
|
790
930
|
return { candidates, skips: [...skips, { ...base, reason: "no `services:` map" }] };
|
|
791
931
|
}
|
|
792
932
|
const project = projectNameOf(root, absPath);
|
|
@@ -866,6 +1006,34 @@ registerDetector({
|
|
|
866
1006
|
reason: `service "${name}" publishes extra container ports (${ports.filter((port) => port !== selectedPort).join(", ")}); Flisk routes only ${selectedPort} and keeps the others published as extraPorts`,
|
|
867
1007
|
});
|
|
868
1008
|
}
|
|
1009
|
+
// Every dependency keeps its Compose condition; the list form, and a map entry without one, mean service_started.
|
|
1010
|
+
const dependsOnConditions = {};
|
|
1011
|
+
const dependencyEntries = Array.isArray(raw.depends_on)
|
|
1012
|
+
? raw.depends_on
|
|
1013
|
+
.filter((d) => typeof d === "string")
|
|
1014
|
+
.map((d) => [d, undefined])
|
|
1015
|
+
: isObj(raw.depends_on)
|
|
1016
|
+
? Object.entries(raw.depends_on)
|
|
1017
|
+
: [];
|
|
1018
|
+
for (const [dep, value] of dependencyEntries) {
|
|
1019
|
+
const condition = isObj(value) ? value.condition : undefined;
|
|
1020
|
+
dependsOnConditions[dep] =
|
|
1021
|
+
condition === "service_healthy" || condition === "service_completed_successfully"
|
|
1022
|
+
? condition
|
|
1023
|
+
: "service_started";
|
|
1024
|
+
}
|
|
1025
|
+
const buildArgs = buildArgsOf(raw.build);
|
|
1026
|
+
if (raw.build !== undefined && isObj(raw.build) && raw.build.args !== undefined && !buildArgs)
|
|
1027
|
+
skips.push({
|
|
1028
|
+
...base,
|
|
1029
|
+
reason: `service "${name}" build.args ${JSON.stringify(raw.build.args)} has names or values Flisk does not accept; not used`,
|
|
1030
|
+
});
|
|
1031
|
+
const logging = loggingOf(raw.logging);
|
|
1032
|
+
if (raw.logging !== undefined && !logging)
|
|
1033
|
+
skips.push({
|
|
1034
|
+
...base,
|
|
1035
|
+
reason: `service "${name}" logging ${JSON.stringify(raw.logging)} has a driver or options Flisk does not accept; not used`,
|
|
1036
|
+
});
|
|
869
1037
|
const dependsOn = Array.isArray(raw.depends_on)
|
|
870
1038
|
? raw.depends_on.filter((d) => typeof d === "string")
|
|
871
1039
|
: isObj(raw.depends_on)
|
|
@@ -968,7 +1136,7 @@ registerDetector({
|
|
|
968
1136
|
else
|
|
969
1137
|
skips.push({
|
|
970
1138
|
...base,
|
|
971
|
-
reason: `service "${name}" volume ${JSON.stringify(entry)} is not translated (an anonymous volume, a path outside the imported directory, or Compose variables); not mounted`,
|
|
1139
|
+
reason: `service "${name}" volume ${JSON.stringify(volumeText(entry))} is not translated (an anonymous volume, a path outside the imported directory, or Compose variables); not mounted`,
|
|
972
1140
|
});
|
|
973
1141
|
}
|
|
974
1142
|
// `on-failure:5` caps the retries at 5; service.json keeps the cap (1 to 100).
|
|
@@ -1013,6 +1181,9 @@ registerDetector({
|
|
|
1013
1181
|
...(Object.keys(env).length ? { env } : {}),
|
|
1014
1182
|
...(Object.keys(envRefs).length ? { envRefs } : {}),
|
|
1015
1183
|
...(dependsOn.length ? { dependsOn } : {}),
|
|
1184
|
+
...(Object.keys(dependsOnConditions).length ? { dependsOnConditions } : {}),
|
|
1185
|
+
...(buildArgs ? { buildArgs } : {}),
|
|
1186
|
+
...(logging ? { logging } : {}),
|
|
1016
1187
|
...(container ? { container } : {}),
|
|
1017
1188
|
...(extraPorts.length ? { extraPorts } : {}),
|
|
1018
1189
|
...(sidecarOf ? { sidecarOf } : {}),
|
package/dist/flisk.js
CHANGED
|
@@ -9,7 +9,7 @@ export const SERVICE_MANIFEST_SCHEMA_URL = "https://flisk.sh/schema.service.json
|
|
|
9
9
|
* release after 1.4.10 (flisk#2646) accepts a bring-your-own port outside 4000-5999 and two-letter
|
|
10
10
|
* names, which this importer now writes. Stated, not checked: it does not read the installed version.
|
|
11
11
|
*/
|
|
12
|
-
export const MIN_CORE_VERSION = "1.4.
|
|
12
|
+
export const MIN_CORE_VERSION = "1.4.28";
|
|
13
13
|
/**
|
|
14
14
|
* The core change that runs a registered Tiltfile (`orchestration.source: "tiltfile"`) from `flisk up`. Stated, not
|
|
15
15
|
* checked, like MIN_CORE_VERSION.
|
|
@@ -26,7 +26,7 @@ export const HEALTH_CHECK_PATH_PATTERN = /^\/[a-z0-9/-]*$/;
|
|
|
26
26
|
* An image reference, which may hold Compose variables `${NAME}`, `${NAME:-default}` and `${NAME-default}` (postgres:${PG_VERSION:-18}-alpine);
|
|
27
27
|
* Compose resolves them from the project .env. Nothing else: no quotes, spaces or newlines that could change generated YAML.
|
|
28
28
|
*/
|
|
29
|
-
export const IMAGE_PATTERN = /^(?:[A-Za-z0-9._/:@-]|\$\{[A-Za-z_][A-Za-z0-9_]*(?::?-[A-Za-z0-9._/:@-]
|
|
29
|
+
export const IMAGE_PATTERN = /^(?:[A-Za-z0-9._/:@-]|\$\{[A-Za-z_][A-Za-z0-9_]*(?::?-[A-Za-z0-9._/:@-]+|:?\?[A-Za-z0-9 !#%&()*+,./:;<=>?@[\]^_|~-]*)?\})+$/;
|
|
30
30
|
/** A DNS name for a network alias: letters, digits, '-', '_' and '.'. */
|
|
31
31
|
export const NETWORK_ALIAS = /^[A-Za-z0-9][A-Za-z0-9_.-]{0,252}$/;
|
|
32
32
|
const KEBAB = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
package/dist/merge.js
CHANGED
|
@@ -76,6 +76,11 @@ function mergeGroup(name, dir, members, resolve) {
|
|
|
76
76
|
...firstOf(ordered, "models"),
|
|
77
77
|
...firstOf(ordered, "profiles"),
|
|
78
78
|
...firstOf(ordered, "envFile"),
|
|
79
|
+
...firstOf(ordered, "buildArgs"),
|
|
80
|
+
...firstOf(ordered, "logging"),
|
|
81
|
+
dependsOnConditions: Object.fromEntries([...ordered]
|
|
82
|
+
.reverse()
|
|
83
|
+
.flatMap((c) => Object.entries(c.dependsOnConditions ?? {}).map(([d, v]) => [resolve(d), v]))),
|
|
79
84
|
...firstOf(ordered, "healthCheck"),
|
|
80
85
|
...(ordered.find((c) => c.sidecarOf)?.sidecarOf
|
|
81
86
|
? { sidecarOf: resolve(ordered.find((c) => c.sidecarOf)?.sidecarOf) }
|
package/dist/write.js
CHANGED
|
@@ -40,9 +40,15 @@ function looksSecret(value) {
|
|
|
40
40
|
* `ARG` lines for the Compose variables in an image, so `FROM postgres:${POSTGRES_VERSION:-18}-alpine` can use them:
|
|
41
41
|
* Docker substitutes only build arguments declared before FROM. The default is the expression's.
|
|
42
42
|
*/
|
|
43
|
+
function imageArgExpressions(image) {
|
|
44
|
+
const out = {};
|
|
45
|
+
for (const m of image.matchAll(/\$\{([A-Za-z_][A-Za-z0-9_]*)[^}]*\}/g))
|
|
46
|
+
out[m[1]] ??= m[0];
|
|
47
|
+
return out;
|
|
48
|
+
}
|
|
43
49
|
function buildArgLines(image) {
|
|
44
50
|
const args = new Map();
|
|
45
|
-
for (const m of image.matchAll(/\$\{([A-Za-z_][A-Za-z0-9_]*)(?::?-([^}]*))?\}/g))
|
|
51
|
+
for (const m of image.matchAll(/\$\{([A-Za-z_][A-Za-z0-9_]*)(?::?-([^}]*)|:?\?[^}]*)?\}/g))
|
|
46
52
|
if (!args.has(m[1]))
|
|
47
53
|
args.set(m[1], m[2]);
|
|
48
54
|
return [...args]
|
|
@@ -179,7 +185,13 @@ export function planWrites(plan, force) {
|
|
|
179
185
|
}
|
|
180
186
|
if (status !== "exists")
|
|
181
187
|
used.set(path, port);
|
|
182
|
-
|
|
188
|
+
// A Compose condition is kept as a { name, condition } entry (Flisk 1.4.28); a plain dependency stays a name.
|
|
189
|
+
const dependsOn = service.dependsOn
|
|
190
|
+
.filter((d) => d !== service.name && names.has(d))
|
|
191
|
+
.map((d) => {
|
|
192
|
+
const condition = service.dependsOnConditions[d];
|
|
193
|
+
return condition ? { name: d, condition } : d;
|
|
194
|
+
});
|
|
183
195
|
const dropped = service.dependsOn.filter((d) => d !== service.name && !names.has(d));
|
|
184
196
|
if (dropped.length)
|
|
185
197
|
notes.push(`dependsOn not written, not in this import: ${dropped.join(", ")}`);
|
|
@@ -249,6 +261,20 @@ export function planWrites(plan, force) {
|
|
|
249
261
|
// Omit `image` from service.json so Flisk builds from the Dockerfile instead of pulling the tag.
|
|
250
262
|
// Flisk has no container `command`, so a Compose `command` on an image becomes the CMD of a one-line Dockerfile.
|
|
251
263
|
const files = [];
|
|
264
|
+
// Build arguments: Compose build.args for a Dockerfile build, and, for a generated Dockerfile on an image with
|
|
265
|
+
// variables, each variable with its own expression, so the project's .env reaches `ARG NAME` before FROM.
|
|
266
|
+
const imageArgs = !service.dockerfile &&
|
|
267
|
+
service.image &&
|
|
268
|
+
(service.containerCommand || service.containerEntrypoint)
|
|
269
|
+
? imageArgExpressions(service.image)
|
|
270
|
+
: {};
|
|
271
|
+
const buildArgsAll = {
|
|
272
|
+
...(service.dockerfile ? (service.buildArgs ?? {}) : {}),
|
|
273
|
+
...imageArgs,
|
|
274
|
+
};
|
|
275
|
+
const buildArgs = Object.keys(buildArgsAll).length ? buildArgsAll : undefined;
|
|
276
|
+
if (service.buildArgs && !service.dockerfile)
|
|
277
|
+
notes.push("build.args not written: the service has no build of its own");
|
|
252
278
|
const wrapWithCommand = !service.dockerfile &&
|
|
253
279
|
service.image &&
|
|
254
280
|
(service.containerCommand || service.containerEntrypoint);
|
|
@@ -326,7 +352,14 @@ export function planWrites(plan, force) {
|
|
|
326
352
|
return [];
|
|
327
353
|
}
|
|
328
354
|
const rel = relative(serviceDir, absolute).split("\\").join("/") || ".";
|
|
329
|
-
|
|
355
|
+
const path = rel.startsWith(".") ? rel : `./${rel}`;
|
|
356
|
+
// `${NAME:-./path}` keeps reading NAME; its default is made relative to the service directory.
|
|
357
|
+
const source = volume.fromEnv
|
|
358
|
+
? `\${${volume.fromEnv.name}${volume.fromEnv.op}${path}}`
|
|
359
|
+
: path;
|
|
360
|
+
if (volume.fromEnv)
|
|
361
|
+
notes.push(`volume ${volume.target} reads its path from ${volume.fromEnv.name}; the default is written relative to this service.json`);
|
|
362
|
+
return [{ ...volume, source }];
|
|
330
363
|
});
|
|
331
364
|
// env_file paths are relative to the service directory in service.json, and must stay inside the project.
|
|
332
365
|
// With the project .env tracked by git, secrets live in another local file, which Flisk then reads as envFile.
|
|
@@ -354,6 +387,8 @@ export function planWrites(plan, force) {
|
|
|
354
387
|
...source,
|
|
355
388
|
...(isPrivate ? { exposeViaProxy: false } : {}),
|
|
356
389
|
...(dependsOn.length ? { dependsOn } : {}),
|
|
390
|
+
...(buildArgs ? { buildArgs } : {}),
|
|
391
|
+
...(service.logging ? { logging: service.logging } : {}),
|
|
357
392
|
...(Object.keys(env).length
|
|
358
393
|
? { env: Object.fromEntries(Object.entries(env).sort(([a], [b]) => a.localeCompare(b))) }
|
|
359
394
|
: {}),
|
|
@@ -370,10 +405,11 @@ export function planWrites(plan, force) {
|
|
|
370
405
|
...(volumes.length
|
|
371
406
|
? {
|
|
372
407
|
// `name` keeps the Docker volume the Compose install already has, so its data is used with no copy.
|
|
373
|
-
volumes: volumes.map(({ source, target, readOnly, name }) => ({
|
|
408
|
+
volumes: volumes.map(({ source, target, readOnly, name, selinux }) => ({
|
|
374
409
|
source,
|
|
375
410
|
target,
|
|
376
411
|
...(readOnly ? { readOnly } : {}),
|
|
412
|
+
...(selinux ? { selinux } : {}),
|
|
377
413
|
...(name ? { name } : {}),
|
|
378
414
|
})),
|
|
379
415
|
}
|
package/package.json
CHANGED