flisk-import 0.5.0 → 0.7.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 +4 -3
- package/dist/detectors/compose.js +89 -4
- package/dist/merge.js +3 -0
- package/dist/plan.js +9 -13
- package/dist/write.js +81 -9
- 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
|
-
- **Environment:**
|
|
18
|
-
- **
|
|
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
|
+
- **Keeps:** a `command` on an image as the `CMD` of a one-line `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,7 @@ const TRANSLATED_KEYS = new Set([
|
|
|
44
44
|
"network_mode",
|
|
45
45
|
"volumes",
|
|
46
46
|
"restart",
|
|
47
|
+
"command",
|
|
47
48
|
]);
|
|
48
49
|
const CAPABILITY = /^[A-Z_]+$/;
|
|
49
50
|
const USER = /^[A-Za-z0-9_.:-]+$/;
|
|
@@ -211,6 +212,24 @@ function envValues(environment) {
|
|
|
211
212
|
}
|
|
212
213
|
return out;
|
|
213
214
|
}
|
|
215
|
+
/** A value that is nothing but Compose interpolation, such as `${URL}` or `${PORT:-80}`. Holds no secret itself. */
|
|
216
|
+
const ONLY_REFERENCES = /^(\$\{[A-Za-z_][A-Za-z0-9_]*(?::?[-?+][^}\n]*)?\})+$/;
|
|
217
|
+
/** Values that are only `${VAR}` references, which service.json `env` can keep as they are (Flisk 1.4.24). */
|
|
218
|
+
function envReferences(environment) {
|
|
219
|
+
const pairs = Array.isArray(environment)
|
|
220
|
+
? environment
|
|
221
|
+
.filter((e) => typeof e === "string" && e.includes("="))
|
|
222
|
+
.map((e) => [e.slice(0, e.indexOf("=")), e.slice(e.indexOf("=") + 1)])
|
|
223
|
+
: isObj(environment)
|
|
224
|
+
? Object.entries(environment)
|
|
225
|
+
: [];
|
|
226
|
+
const out = {};
|
|
227
|
+
for (const [key, raw] of pairs) {
|
|
228
|
+
if (typeof raw === "string" && ONLY_REFERENCES.test(raw))
|
|
229
|
+
out[key] = raw;
|
|
230
|
+
}
|
|
231
|
+
return out;
|
|
232
|
+
}
|
|
214
233
|
function commandOf(command) {
|
|
215
234
|
if (typeof command === "string")
|
|
216
235
|
return command;
|
|
@@ -218,6 +237,55 @@ function commandOf(command) {
|
|
|
218
237
|
return command.join(" ");
|
|
219
238
|
return undefined;
|
|
220
239
|
}
|
|
240
|
+
/** Compose `command` as the argument vector the container runs; a string goes through a shell, as Compose runs it. */
|
|
241
|
+
function containerCommandOf(command) {
|
|
242
|
+
if (typeof command === "string" && command.trim())
|
|
243
|
+
return ["sh", "-c", command];
|
|
244
|
+
if (Array.isArray(command) && command.length > 0 && command.every((c) => typeof c === "string"))
|
|
245
|
+
return command;
|
|
246
|
+
return undefined;
|
|
247
|
+
}
|
|
248
|
+
/** Seconds from a Compose duration such as `30s` or `2m`; undefined for anything it cannot read exactly. */
|
|
249
|
+
function secondsOf(value) {
|
|
250
|
+
if (typeof value === "number" && value > 0)
|
|
251
|
+
return value;
|
|
252
|
+
const match = typeof value === "string" ? value.match(/^(\d+)(s|m)?$/) : null;
|
|
253
|
+
if (!match)
|
|
254
|
+
return undefined;
|
|
255
|
+
const seconds = Number(match[1]) * (match[2] === "m" ? 60 : 1);
|
|
256
|
+
return seconds > 0 ? seconds : undefined;
|
|
257
|
+
}
|
|
258
|
+
/** Compose `healthcheck` as a probe command (exit 0 is ready). `NONE` and `disable` give no probe. */
|
|
259
|
+
function healthProbeOf(healthcheck) {
|
|
260
|
+
if (!isObj(healthcheck) || healthcheck.disable === true)
|
|
261
|
+
return undefined;
|
|
262
|
+
const test = healthcheck.test;
|
|
263
|
+
let command;
|
|
264
|
+
if (typeof test === "string")
|
|
265
|
+
command = ["sh", "-c", test];
|
|
266
|
+
else if (Array.isArray(test) && test.every((t) => typeof t === "string")) {
|
|
267
|
+
const [kind, ...rest] = test;
|
|
268
|
+
if (kind === "CMD" && rest.length > 0)
|
|
269
|
+
command = rest;
|
|
270
|
+
else if (kind === "CMD-SHELL" && rest.length > 0)
|
|
271
|
+
command = ["sh", "-c", rest.join(" ")];
|
|
272
|
+
}
|
|
273
|
+
if (!command)
|
|
274
|
+
return undefined;
|
|
275
|
+
const interval = secondsOf(healthcheck.interval);
|
|
276
|
+
const timeout = secondsOf(healthcheck.timeout);
|
|
277
|
+
const retries = typeof healthcheck.retries === "number" &&
|
|
278
|
+
Number.isInteger(healthcheck.retries) &&
|
|
279
|
+
healthcheck.retries > 0
|
|
280
|
+
? healthcheck.retries
|
|
281
|
+
: undefined;
|
|
282
|
+
return {
|
|
283
|
+
command,
|
|
284
|
+
...(interval ? { interval } : {}),
|
|
285
|
+
...(timeout ? { timeout } : {}),
|
|
286
|
+
...(retries ? { retries } : {}),
|
|
287
|
+
};
|
|
288
|
+
}
|
|
221
289
|
function healthPathOf(healthcheck) {
|
|
222
290
|
if (!isObj(healthcheck) || !Array.isArray(healthcheck.test))
|
|
223
291
|
return undefined;
|
|
@@ -237,7 +305,7 @@ function healthPathOf(healthcheck) {
|
|
|
237
305
|
}
|
|
238
306
|
registerDetector({
|
|
239
307
|
id: "compose",
|
|
240
|
-
claim: (relPath) =>
|
|
308
|
+
claim: (relPath) => /^(docker-)?compose\.ya?ml$/.test(relPath),
|
|
241
309
|
parse(absPath, relPath) {
|
|
242
310
|
const composeDir = dirname(relPath) === "." ? "" : dirname(relPath);
|
|
243
311
|
const base = { file: relPath, detector: "compose" };
|
|
@@ -345,8 +413,11 @@ registerDetector({
|
|
|
345
413
|
? Object.keys(raw.depends_on)
|
|
346
414
|
: [];
|
|
347
415
|
const command = commandOf(raw.command);
|
|
416
|
+
const containerCommand = containerCommandOf(raw.command);
|
|
417
|
+
const healthProbe = healthProbeOf(raw.healthcheck);
|
|
348
418
|
const envKeys = envNames(raw.environment);
|
|
349
419
|
const env = envValues(raw.environment);
|
|
420
|
+
const envRefs = envReferences(raw.environment);
|
|
350
421
|
const foundHealthPath = healthPathOf(raw.healthcheck);
|
|
351
422
|
const healthCheckPath = foundHealthPath && HEALTH_CHECK_PATH_PATTERN.test(foundHealthPath)
|
|
352
423
|
? foundHealthPath
|
|
@@ -365,9 +436,10 @@ registerDetector({
|
|
|
365
436
|
});
|
|
366
437
|
}
|
|
367
438
|
// Every published port Flisk does not route stays published, on 127.0.0.1 (service.json extraPorts).
|
|
439
|
+
const proxied = selectedPort !== undefined && !NON_HTTP_PORTS.has(selectedPort);
|
|
368
440
|
const extraPorts = (Array.isArray(raw.ports) ? raw.ports : [])
|
|
369
441
|
.map(publishedPort)
|
|
370
|
-
.filter((p) => p !== undefined && p.port !== selectedPort);
|
|
442
|
+
.filter((p) => p !== undefined && (p.port !== selectedPort || !proxied));
|
|
371
443
|
let sidecarOf;
|
|
372
444
|
if (raw.network_mode !== undefined) {
|
|
373
445
|
const mode = typeof raw.network_mode === "string" ? raw.network_mode : "";
|
|
@@ -392,9 +464,19 @@ registerDetector({
|
|
|
392
464
|
reason: `service "${name}" volume ${JSON.stringify(entry)} is not translated (an anonymous volume, a path outside the imported directory, or Compose variables); not mounted`,
|
|
393
465
|
});
|
|
394
466
|
}
|
|
395
|
-
|
|
396
|
-
|
|
467
|
+
// `on-failure:5` caps the retries; Flisk has the policy without the cap.
|
|
468
|
+
const restartName = typeof raw.restart === "string"
|
|
469
|
+
? raw.restart.replace(/^(on-failure):\d+$/, "$1")
|
|
397
470
|
: undefined;
|
|
471
|
+
const restart = restartName !== undefined && RESTART.has(restartName)
|
|
472
|
+
? restartName
|
|
473
|
+
: undefined;
|
|
474
|
+
if (restart && restartName !== raw.restart) {
|
|
475
|
+
skips.push({
|
|
476
|
+
...base,
|
|
477
|
+
reason: `service "${name}" restart ${JSON.stringify(raw.restart)} keeps "${restart}"; the retry limit is not carried over`,
|
|
478
|
+
});
|
|
479
|
+
}
|
|
398
480
|
if (raw.restart !== undefined && !restart) {
|
|
399
481
|
skips.push({
|
|
400
482
|
...base,
|
|
@@ -419,11 +501,14 @@ registerDetector({
|
|
|
419
501
|
...(typeof raw.image === "string" ? { image: raw.image } : {}),
|
|
420
502
|
...(dockerfile ? { dockerfile } : {}),
|
|
421
503
|
...(command ? { command } : {}),
|
|
504
|
+
...(containerCommand ? { containerCommand } : {}),
|
|
505
|
+
...(healthProbe ? { healthCheck: healthProbe } : {}),
|
|
422
506
|
...(selectedPort !== undefined ? { port: selectedPort } : {}),
|
|
423
507
|
...(healthCheckPath ? { healthCheckPath } : {}),
|
|
424
508
|
exposeViaProxy,
|
|
425
509
|
...(envKeys.length ? { envKeys } : {}),
|
|
426
510
|
...(Object.keys(env).length ? { env } : {}),
|
|
511
|
+
...(Object.keys(envRefs).length ? { envRefs } : {}),
|
|
427
512
|
...(dependsOn.length ? { dependsOn } : {}),
|
|
428
513
|
...(container ? { container } : {}),
|
|
429
514
|
...(extraPorts.length ? { extraPorts } : {}),
|
package/dist/merge.js
CHANGED
|
@@ -63,6 +63,7 @@ function mergeGroup(name, dir, members, resolve) {
|
|
|
63
63
|
kind: ordered[0].kind,
|
|
64
64
|
envKeys: [...new Set(ordered.flatMap((c) => c.envKeys ?? []))].sort(),
|
|
65
65
|
env: Object.assign({}, ...[...ordered].reverse().map((c) => c.env ?? {})),
|
|
66
|
+
envRefs: Object.assign({}, ...[...ordered].reverse().map((c) => c.envRefs ?? {})),
|
|
66
67
|
dependsOn: [...new Set(ordered.flatMap((c) => (c.dependsOn ?? []).map(resolve)))].sort(),
|
|
67
68
|
exposeViaProxy: ordered.find((c) => c.exposeViaProxy !== undefined)?.exposeViaProxy,
|
|
68
69
|
// Only Compose states these, so the first source that has one wins.
|
|
@@ -70,6 +71,8 @@ function mergeGroup(name, dir, members, resolve) {
|
|
|
70
71
|
...firstOf(ordered, "extraPorts"),
|
|
71
72
|
...firstOf(ordered, "volumes"),
|
|
72
73
|
...firstOf(ordered, "restart"),
|
|
74
|
+
...firstOf(ordered, "containerCommand"),
|
|
75
|
+
...firstOf(ordered, "healthCheck"),
|
|
73
76
|
...(ordered.find((c) => c.sidecarOf)?.sidecarOf
|
|
74
77
|
? { sidecarOf: resolve(ordered.find((c) => c.sidecarOf)?.sidecarOf) }
|
|
75
78
|
: {}),
|
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
|
@@ -6,6 +6,31 @@ import { toResourceName } from "./merge.js";
|
|
|
6
6
|
import { usedManifestPorts } from "./plan.js";
|
|
7
7
|
/** Secret names the service schema accepts (`secrets.required`). */
|
|
8
8
|
const ENV_NAME = /^[A-Z_][A-Z0-9_]*$/;
|
|
9
|
+
/** Names that may hold a credential whatever their value looks like; these always stay secrets. */
|
|
10
|
+
const CREDENTIAL_NAME = /PASS|SECRET|TOKEN|KEY|PRIVATE|CREDENTIAL|AUTH|HMAC|SALT|CERT|SESSION|COOKIE/i;
|
|
11
|
+
/**
|
|
12
|
+
* Whether a literal Compose value cannot be a secret, so it may be committed in service.json `env`: a number, true or false,
|
|
13
|
+
* or the address of another service in this import (a URL or host:port with no user, password, query or fragment).
|
|
14
|
+
*/
|
|
15
|
+
function isPlainValue(value, serviceHosts) {
|
|
16
|
+
if (/^-?\d+(\.\d+)?$/.test(value) || /^(true|false)$/i.test(value))
|
|
17
|
+
return true;
|
|
18
|
+
const hostPort = value.match(/^([a-z0-9][a-z0-9._-]*):(\d{1,5})$/);
|
|
19
|
+
if (hostPort)
|
|
20
|
+
return serviceHosts.has(hostPort[1]);
|
|
21
|
+
try {
|
|
22
|
+
const url = new URL(value);
|
|
23
|
+
return (serviceHosts.has(url.hostname) &&
|
|
24
|
+
!url.username &&
|
|
25
|
+
!url.password &&
|
|
26
|
+
!url.search &&
|
|
27
|
+
!url.hash &&
|
|
28
|
+
/^[A-Za-z0-9._~/-]*$/.test(url.pathname));
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
return false;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
9
34
|
function readEnvKeys(path) {
|
|
10
35
|
if (!existsSync(path))
|
|
11
36
|
return new Set();
|
|
@@ -78,6 +103,8 @@ export function planWrites(plan, force) {
|
|
|
78
103
|
return !reason;
|
|
79
104
|
});
|
|
80
105
|
const names = new Set(accepted.map((s) => s.name));
|
|
106
|
+
// Hostnames other services use for these: the resource name, which stays the container hostname, and the Compose name.
|
|
107
|
+
const serviceHosts = new Set(plan.services.flatMap((s) => (s.originalName ? [s.name, s.originalName] : [s.name])));
|
|
81
108
|
for (const service of accepted) {
|
|
82
109
|
const path = resolve(servicesDir, stack, service.name, "service.json");
|
|
83
110
|
const exists = existsSync(path);
|
|
@@ -133,8 +160,32 @@ export function planWrites(plan, force) {
|
|
|
133
160
|
notes.push("no health path: the service does not serve HTTP (non-HTTP image or job)");
|
|
134
161
|
if (!isPrivate && !nonHttp && !service.healthCheckPath)
|
|
135
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");
|
|
136
|
-
//
|
|
137
|
-
const
|
|
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;
|
|
167
|
+
// Values that cannot be secrets (numbers, booleans, another service's address, pure ${VAR} references) go into the
|
|
168
|
+
// committed service.json `env` (Flisk 1.4.24). Every other name is a secret: Flisk injects it from the gitignored .env.
|
|
169
|
+
const validNames = service.envKeys.filter((key) => ENV_NAME.test(key));
|
|
170
|
+
const env = {};
|
|
171
|
+
for (const key of validNames) {
|
|
172
|
+
if (CREDENTIAL_NAME.test(key))
|
|
173
|
+
continue;
|
|
174
|
+
const literal = service.env[key];
|
|
175
|
+
const reference = service.envRefs[key];
|
|
176
|
+
if (literal !== undefined && isPlainValue(literal, serviceHosts))
|
|
177
|
+
env[key] = literal;
|
|
178
|
+
else if (literal === undefined && reference !== undefined)
|
|
179
|
+
env[key] = reference;
|
|
180
|
+
}
|
|
181
|
+
const referenced = [
|
|
182
|
+
...new Set(Object.values(env).flatMap((value) => [...value.matchAll(/\$\{([A-Za-z_][A-Za-z0-9_]*)(:?-)?/g)]
|
|
183
|
+
.filter((m) => !m[2])
|
|
184
|
+
.map((m) => m[1]))),
|
|
185
|
+
].filter((name) => !envKept.has(name) && !(name in envAdd));
|
|
186
|
+
if (referenced.length)
|
|
187
|
+
notes.push(`env reads ${referenced.join(", ")} from the project .env; add them there`);
|
|
188
|
+
const secretNames = validNames.filter((key) => !(key in env));
|
|
138
189
|
const skippedNames = service.envKeys.filter((key) => !ENV_NAME.test(key));
|
|
139
190
|
if (skippedNames.length)
|
|
140
191
|
notes.push(`environment names not written (Flisk accepts A-Z, 0-9 and _): ${skippedNames.join(", ")}`);
|
|
@@ -154,12 +205,27 @@ export function planWrites(plan, force) {
|
|
|
154
205
|
notes.push(`image "${service.image}" is not written: with build and image, Flisk builds from ${service.dockerfile} and does not pull the tag`);
|
|
155
206
|
// With both build and image, Compose builds the Dockerfile and tags the result as `image`.
|
|
156
207
|
// Omit `image` from service.json so Flisk builds from the Dockerfile instead of pulling the tag.
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
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 && service.image && service.containerCommand;
|
|
211
|
+
if (wrapWithCommand) {
|
|
212
|
+
files.push({
|
|
213
|
+
path: resolve(dirname(path), "Dockerfile"),
|
|
214
|
+
content: `# From the Compose \`command\` of this service (flisk import). Flisk sets no container command, so it lives here.\nFROM ${service.image}\nCMD ${JSON.stringify(service.containerCommand)}\n`,
|
|
215
|
+
});
|
|
216
|
+
notes.push(`command ${JSON.stringify(service.containerCommand)} is written as the CMD of a Dockerfile on ${service.image}`);
|
|
217
|
+
}
|
|
218
|
+
else if (service.dockerfile && service.containerCommand) {
|
|
219
|
+
notes.push(`command ${JSON.stringify(service.containerCommand)} not written: set it as CMD in ${service.dockerfile}`);
|
|
220
|
+
}
|
|
221
|
+
const source = wrapWithCommand
|
|
222
|
+
? { buildContext: ".", dockerfile: "Dockerfile" }
|
|
223
|
+
: service.dockerfile
|
|
224
|
+
? {
|
|
225
|
+
buildContext: relative(dirname(path), resolve(plan.root, service.dir)).split("\\").join("/") || ".",
|
|
226
|
+
dockerfile: service.dockerfile,
|
|
227
|
+
}
|
|
228
|
+
: { image: service.image };
|
|
163
229
|
// A sidecar shares another written service's network; one whose host is not written stays a normal service.
|
|
164
230
|
const sidecarOf = service.sidecarOf && service.sidecarOf !== service.name && names.has(service.sidecarOf)
|
|
165
231
|
? service.sidecarOf
|
|
@@ -193,9 +259,13 @@ export function planWrites(plan, force) {
|
|
|
193
259
|
stack,
|
|
194
260
|
port,
|
|
195
261
|
...(healthCheckPath ? { healthCheckPath } : {}),
|
|
262
|
+
...(healthCheck ? { healthCheck } : {}),
|
|
196
263
|
...source,
|
|
197
264
|
...(isPrivate ? { exposeViaProxy: false } : {}),
|
|
198
265
|
...(dependsOn.length ? { dependsOn } : {}),
|
|
266
|
+
...(Object.keys(env).length
|
|
267
|
+
? { env: Object.fromEntries(Object.entries(env).sort(([a], [b]) => a.localeCompare(b))) }
|
|
268
|
+
: {}),
|
|
199
269
|
...(secretNames.length ? { secrets: { required: secretNames } } : {}),
|
|
200
270
|
...(service.restart ? { restart: service.restart } : {}),
|
|
201
271
|
...(service.container ? { container: service.container } : {}),
|
|
@@ -211,7 +281,7 @@ export function planWrites(plan, force) {
|
|
|
211
281
|
}
|
|
212
282
|
: {}),
|
|
213
283
|
};
|
|
214
|
-
items.push({ service, path, manifest, status, notes });
|
|
284
|
+
items.push({ service, path, manifest, files, status, notes });
|
|
215
285
|
}
|
|
216
286
|
return {
|
|
217
287
|
projectRoot,
|
|
@@ -233,6 +303,8 @@ export function applyWrites(writes) {
|
|
|
233
303
|
continue;
|
|
234
304
|
mkdirSync(dirname(item.path), { recursive: true });
|
|
235
305
|
writeFileSync(item.path, `${JSON.stringify(item.manifest, null, 2)}\n`);
|
|
306
|
+
for (const file of item.files)
|
|
307
|
+
writeFileSync(file.path, file.content);
|
|
236
308
|
written.push(item);
|
|
237
309
|
}
|
|
238
310
|
const keys = Object.keys(writes.env.add);
|