flisk-import 0.6.0 → 0.8.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 +75 -4
- package/dist/merge.js +3 -0
- package/dist/plan.js +9 -13
- package/dist/write.js +31 -7
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -10,12 +10,13 @@ flisk import . --yes # write service.json files and project setup
|
|
|
10
10
|
|
|
11
11
|
## What to expect
|
|
12
12
|
|
|
13
|
-
- **Reads one file:** the root `docker-compose.yml
|
|
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
15
|
- **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
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 only), `network_mode: service:<name>` (`sidecarOf`) and `restart`.
|
|
17
17
|
- **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
|
-
- **
|
|
18
|
+
- **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
|
+
- **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.
|
|
19
20
|
- **Does not start anything.** Run `flisk up` with Flisk core **1.4.24 or later**. The importer does not check the core version.
|
|
20
21
|
- **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.`).
|
|
21
22
|
|
|
@@ -44,6 +44,11 @@ const TRANSLATED_KEYS = new Set([
|
|
|
44
44
|
"network_mode",
|
|
45
45
|
"volumes",
|
|
46
46
|
"restart",
|
|
47
|
+
"command",
|
|
48
|
+
"entrypoint",
|
|
49
|
+
// Flisk gives every service its own stack network and container name, so these change nothing it needs.
|
|
50
|
+
"networks",
|
|
51
|
+
"expose",
|
|
47
52
|
]);
|
|
48
53
|
const CAPABILITY = /^[A-Z_]+$/;
|
|
49
54
|
const USER = /^[A-Za-z0-9_.:-]+$/;
|
|
@@ -236,6 +241,55 @@ function commandOf(command) {
|
|
|
236
241
|
return command.join(" ");
|
|
237
242
|
return undefined;
|
|
238
243
|
}
|
|
244
|
+
/** Compose `command` as the argument vector the container runs; a string goes through a shell, as Compose runs it. */
|
|
245
|
+
function containerCommandOf(command) {
|
|
246
|
+
if (typeof command === "string" && command.trim())
|
|
247
|
+
return ["sh", "-c", command];
|
|
248
|
+
if (Array.isArray(command) && command.length > 0 && command.every((c) => typeof c === "string"))
|
|
249
|
+
return command;
|
|
250
|
+
return undefined;
|
|
251
|
+
}
|
|
252
|
+
/** Seconds from a Compose duration such as `30s` or `2m`; undefined for anything it cannot read exactly. */
|
|
253
|
+
function secondsOf(value) {
|
|
254
|
+
if (typeof value === "number" && value > 0)
|
|
255
|
+
return value;
|
|
256
|
+
const match = typeof value === "string" ? value.match(/^(\d+)(s|m)?$/) : null;
|
|
257
|
+
if (!match)
|
|
258
|
+
return undefined;
|
|
259
|
+
const seconds = Number(match[1]) * (match[2] === "m" ? 60 : 1);
|
|
260
|
+
return seconds > 0 ? seconds : undefined;
|
|
261
|
+
}
|
|
262
|
+
/** Compose `healthcheck` as a probe command (exit 0 is ready). `NONE` and `disable` give no probe. */
|
|
263
|
+
function healthProbeOf(healthcheck) {
|
|
264
|
+
if (!isObj(healthcheck) || healthcheck.disable === true)
|
|
265
|
+
return undefined;
|
|
266
|
+
const test = healthcheck.test;
|
|
267
|
+
let command;
|
|
268
|
+
if (typeof test === "string")
|
|
269
|
+
command = ["sh", "-c", test];
|
|
270
|
+
else if (Array.isArray(test) && test.every((t) => typeof t === "string")) {
|
|
271
|
+
const [kind, ...rest] = test;
|
|
272
|
+
if (kind === "CMD" && rest.length > 0)
|
|
273
|
+
command = rest;
|
|
274
|
+
else if (kind === "CMD-SHELL" && rest.length > 0)
|
|
275
|
+
command = ["sh", "-c", rest.join(" ")];
|
|
276
|
+
}
|
|
277
|
+
if (!command)
|
|
278
|
+
return undefined;
|
|
279
|
+
const interval = secondsOf(healthcheck.interval);
|
|
280
|
+
const timeout = secondsOf(healthcheck.timeout);
|
|
281
|
+
const retries = typeof healthcheck.retries === "number" &&
|
|
282
|
+
Number.isInteger(healthcheck.retries) &&
|
|
283
|
+
healthcheck.retries > 0
|
|
284
|
+
? healthcheck.retries
|
|
285
|
+
: undefined;
|
|
286
|
+
return {
|
|
287
|
+
command,
|
|
288
|
+
...(interval ? { interval } : {}),
|
|
289
|
+
...(timeout ? { timeout } : {}),
|
|
290
|
+
...(retries ? { retries } : {}),
|
|
291
|
+
};
|
|
292
|
+
}
|
|
239
293
|
function healthPathOf(healthcheck) {
|
|
240
294
|
if (!isObj(healthcheck) || !Array.isArray(healthcheck.test))
|
|
241
295
|
return undefined;
|
|
@@ -255,7 +309,7 @@ function healthPathOf(healthcheck) {
|
|
|
255
309
|
}
|
|
256
310
|
registerDetector({
|
|
257
311
|
id: "compose",
|
|
258
|
-
claim: (relPath) =>
|
|
312
|
+
claim: (relPath) => /^(docker-)?compose\.ya?ml$/.test(relPath),
|
|
259
313
|
parse(absPath, relPath) {
|
|
260
314
|
const composeDir = dirname(relPath) === "." ? "" : dirname(relPath);
|
|
261
315
|
const base = { file: relPath, detector: "compose" };
|
|
@@ -363,6 +417,9 @@ registerDetector({
|
|
|
363
417
|
? Object.keys(raw.depends_on)
|
|
364
418
|
: [];
|
|
365
419
|
const command = commandOf(raw.command);
|
|
420
|
+
const containerCommand = containerCommandOf(raw.command);
|
|
421
|
+
const containerEntrypoint = containerCommandOf(raw.entrypoint);
|
|
422
|
+
const healthProbe = healthProbeOf(raw.healthcheck);
|
|
366
423
|
const envKeys = envNames(raw.environment);
|
|
367
424
|
const env = envValues(raw.environment);
|
|
368
425
|
const envRefs = envReferences(raw.environment);
|
|
@@ -384,9 +441,10 @@ registerDetector({
|
|
|
384
441
|
});
|
|
385
442
|
}
|
|
386
443
|
// Every published port Flisk does not route stays published, on 127.0.0.1 (service.json extraPorts).
|
|
444
|
+
const proxied = selectedPort !== undefined && !NON_HTTP_PORTS.has(selectedPort);
|
|
387
445
|
const extraPorts = (Array.isArray(raw.ports) ? raw.ports : [])
|
|
388
446
|
.map(publishedPort)
|
|
389
|
-
.filter((p) => p !== undefined && p.port !== selectedPort);
|
|
447
|
+
.filter((p) => p !== undefined && (p.port !== selectedPort || !proxied));
|
|
390
448
|
let sidecarOf;
|
|
391
449
|
if (raw.network_mode !== undefined) {
|
|
392
450
|
const mode = typeof raw.network_mode === "string" ? raw.network_mode : "";
|
|
@@ -411,9 +469,19 @@ registerDetector({
|
|
|
411
469
|
reason: `service "${name}" volume ${JSON.stringify(entry)} is not translated (an anonymous volume, a path outside the imported directory, or Compose variables); not mounted`,
|
|
412
470
|
});
|
|
413
471
|
}
|
|
414
|
-
|
|
415
|
-
|
|
472
|
+
// `on-failure:5` caps the retries; Flisk has the policy without the cap.
|
|
473
|
+
const restartName = typeof raw.restart === "string"
|
|
474
|
+
? raw.restart.replace(/^(on-failure):\d+$/, "$1")
|
|
475
|
+
: undefined;
|
|
476
|
+
const restart = restartName !== undefined && RESTART.has(restartName)
|
|
477
|
+
? restartName
|
|
416
478
|
: undefined;
|
|
479
|
+
if (restart && restartName !== raw.restart) {
|
|
480
|
+
skips.push({
|
|
481
|
+
...base,
|
|
482
|
+
reason: `service "${name}" restart ${JSON.stringify(raw.restart)} keeps "${restart}"; the retry limit is not carried over`,
|
|
483
|
+
});
|
|
484
|
+
}
|
|
417
485
|
if (raw.restart !== undefined && !restart) {
|
|
418
486
|
skips.push({
|
|
419
487
|
...base,
|
|
@@ -438,6 +506,9 @@ registerDetector({
|
|
|
438
506
|
...(typeof raw.image === "string" ? { image: raw.image } : {}),
|
|
439
507
|
...(dockerfile ? { dockerfile } : {}),
|
|
440
508
|
...(command ? { command } : {}),
|
|
509
|
+
...(containerCommand ? { containerCommand } : {}),
|
|
510
|
+
...(containerEntrypoint ? { containerEntrypoint } : {}),
|
|
511
|
+
...(healthProbe ? { healthCheck: healthProbe } : {}),
|
|
441
512
|
...(selectedPort !== undefined ? { port: selectedPort } : {}),
|
|
442
513
|
...(healthCheckPath ? { healthCheckPath } : {}),
|
|
443
514
|
exposeViaProxy,
|
package/dist/merge.js
CHANGED
|
@@ -71,6 +71,9 @@ function mergeGroup(name, dir, members, resolve) {
|
|
|
71
71
|
...firstOf(ordered, "extraPorts"),
|
|
72
72
|
...firstOf(ordered, "volumes"),
|
|
73
73
|
...firstOf(ordered, "restart"),
|
|
74
|
+
...firstOf(ordered, "containerCommand"),
|
|
75
|
+
...firstOf(ordered, "containerEntrypoint"),
|
|
76
|
+
...firstOf(ordered, "healthCheck"),
|
|
74
77
|
...(ordered.find((c) => c.sidecarOf)?.sidecarOf
|
|
75
78
|
? { sidecarOf: resolve(ordered.find((c) => c.sidecarOf)?.sidecarOf) }
|
|
76
79
|
: {}),
|
package/dist/plan.js
CHANGED
|
@@ -7,7 +7,7 @@ import { selectDetectors } from "./registry.js";
|
|
|
7
7
|
import { scanTree } from "./scan.js";
|
|
8
8
|
import { findUnsupported } from "./unsupported.js";
|
|
9
9
|
const COMPOSE_FILE = "docker-compose.yml";
|
|
10
|
-
/** Names `docker compose` also picks up, in its own order of preference
|
|
10
|
+
/** Names `docker compose` also picks up, in its own order of preference. They are read only when docker-compose.yml is absent. */
|
|
11
11
|
const OTHER_COMPOSE_FILES = ["compose.yaml", "compose.yml", "docker-compose.yaml"];
|
|
12
12
|
/** Files `docker compose up` merges over the main file; none of them is applied. */
|
|
13
13
|
const OVERRIDE_FILES = [
|
|
@@ -16,37 +16,33 @@ const OVERRIDE_FILES = [
|
|
|
16
16
|
"docker-compose.override.yaml",
|
|
17
17
|
"docker-compose.override.yml",
|
|
18
18
|
];
|
|
19
|
-
/** Read the one supported input, root
|
|
19
|
+
/** Read the one supported input, the root Compose file. This function writes nothing. */
|
|
20
20
|
export function buildPlan(dir) {
|
|
21
21
|
const root = resolve(dir);
|
|
22
22
|
if (!statSync(root, { throwIfNoEntry: false })?.isDirectory()) {
|
|
23
23
|
throw new Error(`${dir} is not a directory`);
|
|
24
24
|
}
|
|
25
|
-
const composePath = COMPOSE_FILE;
|
|
26
25
|
const detectors = selectDetectors(["compose"]);
|
|
27
26
|
const candidates = [];
|
|
28
27
|
const skips = [];
|
|
29
28
|
const isFile = (name) => statSync(resolve(root, name), { throwIfNoEntry: false })?.isFile() ?? false;
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
`Other Compose file names are not read yet. To import ${others[0]}, link it: ln -s ${others[0]} docker-compose.yml`,
|
|
35
|
-
]
|
|
36
|
-
: ["Run flisk import in the directory that holds docker-compose.yml"]);
|
|
29
|
+
const present = OTHER_COMPOSE_FILES.filter(isFile);
|
|
30
|
+
const composePath = isFile(COMPOSE_FILE) ? COMPOSE_FILE : present[0];
|
|
31
|
+
if (!composePath) {
|
|
32
|
+
throw new ImportError(`No root docker-compose.yml found in ${root} (nor compose.yaml); flisk import reads only the root Compose file`, ["Run flisk import in the directory that holds docker-compose.yml (or compose.yaml)"]);
|
|
37
33
|
}
|
|
38
|
-
for (const name of
|
|
34
|
+
for (const name of present.filter((n) => n !== composePath)) {
|
|
39
35
|
skips.push({
|
|
40
36
|
file: name,
|
|
41
37
|
detector: "compose",
|
|
42
|
-
reason: `not read; \`docker compose\` prefers it over ${
|
|
38
|
+
reason: `not read; \`docker compose\` prefers it over ${composePath}, so what it runs may differ from this plan`,
|
|
43
39
|
});
|
|
44
40
|
}
|
|
45
41
|
for (const name of OVERRIDE_FILES.filter(isFile)) {
|
|
46
42
|
skips.push({
|
|
47
43
|
file: name,
|
|
48
44
|
detector: "compose",
|
|
49
|
-
reason: `not applied; \`docker compose up\` merges it over ${
|
|
45
|
+
reason: `not applied; \`docker compose up\` merges it over ${composePath}, flisk import does not`,
|
|
50
46
|
});
|
|
51
47
|
}
|
|
52
48
|
for (const detector of detectors) {
|
package/dist/write.js
CHANGED
|
@@ -160,6 +160,10 @@ export function planWrites(plan, force) {
|
|
|
160
160
|
notes.push("no health path: the service does not serve HTTP (non-HTTP image or job)");
|
|
161
161
|
if (!isPrivate && !nonHttp && !service.healthCheckPath)
|
|
162
162
|
notes.push("health path / is assumed: no HTTP health path could be read from Compose. Traefik routes only while it answers 2xx or 3xx");
|
|
163
|
+
// A probe command is the only way to describe a non-HTTP check; a routed service keeps its HTTP path.
|
|
164
|
+
const healthCheck = service.healthCheck && !(healthCheckPath && service.healthCheckPath)
|
|
165
|
+
? service.healthCheck
|
|
166
|
+
: undefined;
|
|
163
167
|
// Values that cannot be secrets (numbers, booleans, another service's address, pure ${VAR} references) go into the
|
|
164
168
|
// committed service.json `env` (Flisk 1.4.24). Every other name is a secret: Flisk injects it from the gitignored .env.
|
|
165
169
|
const validNames = service.envKeys.filter((key) => ENV_NAME.test(key));
|
|
@@ -201,12 +205,29 @@ export function planWrites(plan, force) {
|
|
|
201
205
|
notes.push(`image "${service.image}" is not written: with build and image, Flisk builds from ${service.dockerfile} and does not pull the tag`);
|
|
202
206
|
// With both build and image, Compose builds the Dockerfile and tags the result as `image`.
|
|
203
207
|
// Omit `image` from service.json so Flisk builds from the Dockerfile instead of pulling the tag.
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
208
|
+
// Flisk has no container `command`, so a Compose `command` on an image becomes the CMD of a one-line Dockerfile.
|
|
209
|
+
const files = [];
|
|
210
|
+
const wrapWithCommand = !service.dockerfile &&
|
|
211
|
+
service.image &&
|
|
212
|
+
(service.containerCommand || service.containerEntrypoint);
|
|
213
|
+
if (wrapWithCommand) {
|
|
214
|
+
files.push({
|
|
215
|
+
path: resolve(dirname(path), "Dockerfile"),
|
|
216
|
+
content: `# From the Compose \`command\` of this service (flisk import). Flisk sets no container command, so it lives here.\nFROM ${service.image}\n${service.containerEntrypoint ? `ENTRYPOINT ${JSON.stringify(service.containerEntrypoint)}\n` : ""}${service.containerCommand ? `CMD ${JSON.stringify(service.containerCommand)}\n` : ""}`,
|
|
217
|
+
});
|
|
218
|
+
notes.push(`${service.containerEntrypoint ? "entrypoint" : "command"} ${JSON.stringify(service.containerEntrypoint ?? service.containerCommand)} is written to a Dockerfile on ${service.image}`);
|
|
219
|
+
}
|
|
220
|
+
else if (service.dockerfile && (service.containerCommand || service.containerEntrypoint)) {
|
|
221
|
+
notes.push(`command/entrypoint not written: set it as CMD/ENTRYPOINT in ${service.dockerfile}`);
|
|
222
|
+
}
|
|
223
|
+
const source = wrapWithCommand
|
|
224
|
+
? { buildContext: ".", dockerfile: "Dockerfile" }
|
|
225
|
+
: service.dockerfile
|
|
226
|
+
? {
|
|
227
|
+
buildContext: relative(dirname(path), resolve(plan.root, service.dir)).split("\\").join("/") || ".",
|
|
228
|
+
dockerfile: service.dockerfile,
|
|
229
|
+
}
|
|
230
|
+
: { image: service.image };
|
|
210
231
|
// A sidecar shares another written service's network; one whose host is not written stays a normal service.
|
|
211
232
|
const sidecarOf = service.sidecarOf && service.sidecarOf !== service.name && names.has(service.sidecarOf)
|
|
212
233
|
? service.sidecarOf
|
|
@@ -240,6 +261,7 @@ export function planWrites(plan, force) {
|
|
|
240
261
|
stack,
|
|
241
262
|
port,
|
|
242
263
|
...(healthCheckPath ? { healthCheckPath } : {}),
|
|
264
|
+
...(healthCheck ? { healthCheck } : {}),
|
|
243
265
|
...source,
|
|
244
266
|
...(isPrivate ? { exposeViaProxy: false } : {}),
|
|
245
267
|
...(dependsOn.length ? { dependsOn } : {}),
|
|
@@ -261,7 +283,7 @@ export function planWrites(plan, force) {
|
|
|
261
283
|
}
|
|
262
284
|
: {}),
|
|
263
285
|
};
|
|
264
|
-
items.push({ service, path, manifest, status, notes });
|
|
286
|
+
items.push({ service, path, manifest, files, status, notes });
|
|
265
287
|
}
|
|
266
288
|
return {
|
|
267
289
|
projectRoot,
|
|
@@ -283,6 +305,8 @@ export function applyWrites(writes) {
|
|
|
283
305
|
continue;
|
|
284
306
|
mkdirSync(dirname(item.path), { recursive: true });
|
|
285
307
|
writeFileSync(item.path, `${JSON.stringify(item.manifest, null, 2)}\n`);
|
|
308
|
+
for (const file of item.files)
|
|
309
|
+
writeFileSync(file.path, file.content);
|
|
286
310
|
written.push(item);
|
|
287
311
|
}
|
|
288
312
|
const keys = Object.keys(writes.env.add);
|