@flisk-sh/flisk-import 0.10.0 → 0.12.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 +132 -17
- package/dist/flisk.js +1 -1
- package/dist/merge.js +2 -0
- package/dist/write.js +22 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -13,12 +13,13 @@ flisk import . --yes # write service.json files and project setup
|
|
|
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
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
|
+
- **Keeps:** `profiles` (the service starts only with `flisk up --profile`), `env_file` as `envFile`, `tmpfs` and `deploy.resources.limits` (`container.tmpfs`, `cpus`, `memory`).
|
|
16
17
|
- **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.
|
|
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 127.0.0.1
|
|
18
|
+
- **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`.
|
|
18
19
|
- **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.
|
|
19
20
|
- **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`.
|
|
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
|
|
21
|
-
- **Does not start anything.** Run `flisk up` with Flisk core **1.4.
|
|
21
|
+
- **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 a `${VAR}` image.
|
|
22
|
+
- **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.
|
|
22
23
|
- **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.`).
|
|
23
24
|
|
|
24
25
|
See [docs/import.md](docs/import.md) for the full mapping and limits. `flisk import` is the core CLI command backed by this package.
|
|
@@ -47,6 +47,10 @@ const TRANSLATED_KEYS = new Set([
|
|
|
47
47
|
"command",
|
|
48
48
|
"entrypoint",
|
|
49
49
|
"models",
|
|
50
|
+
"profiles",
|
|
51
|
+
"env_file",
|
|
52
|
+
"tmpfs",
|
|
53
|
+
"deploy",
|
|
50
54
|
// Flisk gives every service its own stack network and container name, so these change nothing it needs.
|
|
51
55
|
"networks",
|
|
52
56
|
"expose",
|
|
@@ -120,33 +124,65 @@ function containerOf(raw) {
|
|
|
120
124
|
return { ...(Object.keys(container).length ? { container } : {}), dropped };
|
|
121
125
|
}
|
|
122
126
|
/** One published port: container port, host port when Compose names a literal one, and protocol. */
|
|
127
|
+
/** A `publish` value Flisk accepts: digits, dots, colons and `${NAME:-default}` with a port or IPv4 address:port default. */
|
|
128
|
+
const PUBLISH = /^(?:[0-9.:]|\$\{[A-Za-z_][A-Za-z0-9_]*:-[0-9.:]+\})+$/;
|
|
129
|
+
/** The host address and port of a published port. Compose publishes on every interface unless an address is given. */
|
|
130
|
+
function hostSideOf(port, hostIp, hostPort) {
|
|
131
|
+
const hostNumber = hostPort && /^\d+$/.test(hostPort) ? Number.parseInt(hostPort, 10) : undefined;
|
|
132
|
+
const keepHostPort = hostNumber !== undefined && hostNumber >= 1 && hostNumber <= 65535 && hostNumber !== port
|
|
133
|
+
? { hostPort: hostNumber }
|
|
134
|
+
: {};
|
|
135
|
+
if (hostPort?.includes("${")) {
|
|
136
|
+
const publish = hostIp ? `${hostIp}:${hostPort}` : hostPort;
|
|
137
|
+
return PUBLISH.test(publish) ? { publish } : undefined;
|
|
138
|
+
}
|
|
139
|
+
if (hostIp === undefined || hostIp === "" || hostIp === "0.0.0.0") {
|
|
140
|
+
// A bare container port ("3000") gets a random host port from Compose; Flisk keeps it on 127.0.0.1.
|
|
141
|
+
return hostPort === undefined ? {} : { ...keepHostPort, bind: "0.0.0.0" };
|
|
142
|
+
}
|
|
143
|
+
if (hostIp === "127.0.0.1")
|
|
144
|
+
return keepHostPort;
|
|
145
|
+
const publish = `${hostIp}:${hostNumber ?? port}`;
|
|
146
|
+
return /^\d{1,3}(\.\d{1,3}){3}:\d+$/.test(publish) ? { publish } : undefined;
|
|
147
|
+
}
|
|
123
148
|
function publishedPort(entry) {
|
|
124
149
|
const port = containerPort(entry);
|
|
125
150
|
if (port === undefined)
|
|
126
151
|
return undefined;
|
|
127
|
-
let host;
|
|
128
152
|
let protocol = "tcp";
|
|
153
|
+
let hostSide;
|
|
129
154
|
if (isObj(entry)) {
|
|
130
|
-
const published =
|
|
131
|
-
|
|
132
|
-
host = published;
|
|
155
|
+
const published = entry.published === undefined ? undefined : String(entry.published);
|
|
156
|
+
const hostIp = typeof entry.host_ip === "string" ? entry.host_ip : undefined;
|
|
133
157
|
if (entry.protocol === "udp")
|
|
134
158
|
protocol = "udp";
|
|
159
|
+
hostSide = hostSideOf(port, hostIp, published);
|
|
135
160
|
}
|
|
136
161
|
else if (typeof entry === "string") {
|
|
137
162
|
const [mapping, proto] = entry.split("/");
|
|
138
163
|
if (proto === "udp")
|
|
139
164
|
protocol = "udp";
|
|
140
|
-
|
|
141
|
-
const
|
|
142
|
-
|
|
143
|
-
|
|
165
|
+
// [HOST_IP:][HOST_PORT:]CONTAINER_PORT, where the host side may hold ${VAR:-default}, which has colons of its own.
|
|
166
|
+
const match = (mapping ?? "").match(/^(?:(.*):)?([^:]+)$/);
|
|
167
|
+
const host = match?.[1];
|
|
168
|
+
if (host === undefined)
|
|
169
|
+
hostSide = hostSideOf(port, undefined, undefined);
|
|
170
|
+
else if (host.includes("${"))
|
|
171
|
+
hostSide = hostSideOf(port, undefined, host);
|
|
172
|
+
else {
|
|
173
|
+
const at = host.lastIndexOf(":");
|
|
174
|
+
hostSide =
|
|
175
|
+
at === -1
|
|
176
|
+
? hostSideOf(port, undefined, host)
|
|
177
|
+
: hostSideOf(port, host.slice(0, at), host.slice(at + 1));
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
else {
|
|
181
|
+
hostSide = {};
|
|
144
182
|
}
|
|
145
183
|
return {
|
|
146
184
|
port,
|
|
147
|
-
...(
|
|
148
|
-
? { hostPort: host }
|
|
149
|
-
: {}),
|
|
185
|
+
...(hostSide ?? {}),
|
|
150
186
|
...(protocol === "udp" ? { protocol } : {}),
|
|
151
187
|
};
|
|
152
188
|
}
|
|
@@ -325,6 +361,67 @@ function modelsOf(uses, defined) {
|
|
|
325
361
|
}
|
|
326
362
|
return out.length ? out : undefined;
|
|
327
363
|
}
|
|
364
|
+
const PROFILE = /^[a-z0-9][a-z0-9-]*$/;
|
|
365
|
+
const TMPFS = /^\/[A-Za-z0-9_./-]*(:[a-z0-9=,_-]+)?$/;
|
|
366
|
+
function profilesOf(value) {
|
|
367
|
+
if (!Array.isArray(value) || value.length === 0)
|
|
368
|
+
return undefined;
|
|
369
|
+
return value.every((p) => typeof p === "string" && PROFILE.test(p))
|
|
370
|
+
? [...new Set(value)]
|
|
371
|
+
: undefined;
|
|
372
|
+
}
|
|
373
|
+
/** Compose env_file entries (a string, strings, or { path, required }) as paths from the import root; undefined when one leaves it. */
|
|
374
|
+
function envFilesOf(value, composeDir) {
|
|
375
|
+
const entries = typeof value === "string" ? [value] : Array.isArray(value) ? value : [];
|
|
376
|
+
const out = [];
|
|
377
|
+
for (const entry of entries) {
|
|
378
|
+
const path = typeof entry === "string" ? entry : isObj(entry) ? entry.path : undefined;
|
|
379
|
+
if (typeof path !== "string" || isAbsolute(path) || path.includes("$"))
|
|
380
|
+
return undefined;
|
|
381
|
+
const resolved = normalize(join(composeDir, path));
|
|
382
|
+
if (resolved === ".." || resolved.startsWith("../") || !/^[A-Za-z0-9_./-]+$/.test(resolved))
|
|
383
|
+
return undefined;
|
|
384
|
+
out.push(resolved);
|
|
385
|
+
}
|
|
386
|
+
return out.length ? out : undefined;
|
|
387
|
+
}
|
|
388
|
+
/** tmpfs and deploy.resources.limits, and the parts that could not be carried over. */
|
|
389
|
+
function runtimeLimitsOf(raw) {
|
|
390
|
+
const dropped = [];
|
|
391
|
+
const out = {};
|
|
392
|
+
if (raw.tmpfs !== undefined) {
|
|
393
|
+
const list = typeof raw.tmpfs === "string" ? [raw.tmpfs] : Array.isArray(raw.tmpfs) ? raw.tmpfs : [];
|
|
394
|
+
const kept = list.filter((t) => typeof t === "string" && TMPFS.test(t));
|
|
395
|
+
if (kept.length !== list.length || list.length === 0)
|
|
396
|
+
dropped.push("tmpfs");
|
|
397
|
+
if (kept.length)
|
|
398
|
+
out.tmpfs = kept;
|
|
399
|
+
}
|
|
400
|
+
if (raw.deploy !== undefined) {
|
|
401
|
+
const deploy = isObj(raw.deploy) ? raw.deploy : {};
|
|
402
|
+
const resources = isObj(deploy.resources) ? deploy.resources : {};
|
|
403
|
+
const limits = isObj(resources.limits) ? resources.limits : {};
|
|
404
|
+
const cpus = typeof limits.cpus === "number" ? String(limits.cpus) : limits.cpus;
|
|
405
|
+
if (typeof cpus === "string" && /^[0-9]+(\.[0-9]{1,2})?$/.test(cpus) && Number(cpus) > 0)
|
|
406
|
+
out.cpus = Number.isInteger(Number(cpus)) && Number(cpus) >= 1 ? Number(cpus) : cpus;
|
|
407
|
+
else if (limits.cpus !== undefined)
|
|
408
|
+
dropped.push("deploy.resources.limits.cpus");
|
|
409
|
+
if (typeof limits.memory === "string" && /^[0-9]+([bBkKmMgG][bB]?)?$/.test(limits.memory))
|
|
410
|
+
out.memory = limits.memory;
|
|
411
|
+
else if (limits.memory !== undefined)
|
|
412
|
+
dropped.push("deploy.resources.limits.memory");
|
|
413
|
+
const other = [
|
|
414
|
+
...Object.keys(deploy)
|
|
415
|
+
.filter((k) => k !== "resources")
|
|
416
|
+
.map((k) => `deploy.${k}`),
|
|
417
|
+
...Object.keys(resources)
|
|
418
|
+
.filter((k) => k !== "limits")
|
|
419
|
+
.map((k) => `deploy.resources.${k}`),
|
|
420
|
+
];
|
|
421
|
+
dropped.push(...other);
|
|
422
|
+
}
|
|
423
|
+
return { ...out, dropped };
|
|
424
|
+
}
|
|
328
425
|
function healthPathOf(healthcheck) {
|
|
329
426
|
if (!isObj(healthcheck) || !Array.isArray(healthcheck.test))
|
|
330
427
|
return undefined;
|
|
@@ -391,10 +488,11 @@ registerDetector({
|
|
|
391
488
|
});
|
|
392
489
|
continue;
|
|
393
490
|
}
|
|
394
|
-
|
|
491
|
+
const profiles = profilesOf(raw.profiles);
|
|
492
|
+
if (raw.profiles !== undefined && !profiles) {
|
|
395
493
|
skips.push({
|
|
396
494
|
...base,
|
|
397
|
-
reason: `service "${name}" has profiles ${JSON.stringify(raw.profiles)}
|
|
495
|
+
reason: `service "${name}" has profiles ${JSON.stringify(raw.profiles)} that Flisk cannot name (lowercase letters, digits and '-'); not imported`,
|
|
398
496
|
});
|
|
399
497
|
continue;
|
|
400
498
|
}
|
|
@@ -438,13 +536,13 @@ registerDetector({
|
|
|
438
536
|
if (ports.length > 1 && selectedPort === undefined) {
|
|
439
537
|
skips.push({
|
|
440
538
|
...base,
|
|
441
|
-
reason: `service "${name}" lists several ports (${ports.join(", ")}); no single HTTP container port could be identified, so none is routed (all
|
|
539
|
+
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)`,
|
|
442
540
|
});
|
|
443
541
|
}
|
|
444
542
|
else if (ports.length > 1) {
|
|
445
543
|
skips.push({
|
|
446
544
|
...base,
|
|
447
|
-
reason: `service "${name}" publishes extra container ports (${ports.filter((port) => port !== selectedPort).join(", ")}); Flisk routes only ${selectedPort} and
|
|
545
|
+
reason: `service "${name}" publishes extra container ports (${ports.filter((port) => port !== selectedPort).join(", ")}); Flisk routes only ${selectedPort} and keeps the others published as extraPorts`,
|
|
448
546
|
});
|
|
449
547
|
}
|
|
450
548
|
const dependsOn = Array.isArray(raw.depends_on)
|
|
@@ -470,14 +568,29 @@ registerDetector({
|
|
|
470
568
|
reason: `service "${name}" health path "${foundHealthPath}" has characters Flisk does not accept (only a-z, 0-9, / and -); not used`,
|
|
471
569
|
});
|
|
472
570
|
}
|
|
473
|
-
const { container, dropped } = containerOf(raw);
|
|
571
|
+
const { container: options, dropped } = containerOf(raw);
|
|
572
|
+
const limits = runtimeLimitsOf(raw);
|
|
573
|
+
const { dropped: limitsDropped, ...limitValues } = limits;
|
|
574
|
+
const container = options || Object.keys(limitValues).length
|
|
575
|
+
? { ...(options ?? {}), ...limitValues }
|
|
576
|
+
: undefined;
|
|
577
|
+
for (const key of limitsDropped) {
|
|
578
|
+
skips.push({ ...base, reason: `service "${name}" ${key} is not carried over; not used` });
|
|
579
|
+
}
|
|
580
|
+
const envFile = envFilesOf(raw.env_file, composeDir);
|
|
581
|
+
if (raw.env_file !== undefined && !envFile) {
|
|
582
|
+
skips.push({
|
|
583
|
+
...base,
|
|
584
|
+
reason: `service "${name}" env_file ${JSON.stringify(raw.env_file)} is outside the imported directory or uses Compose variables; not used`,
|
|
585
|
+
});
|
|
586
|
+
}
|
|
474
587
|
for (const key of dropped) {
|
|
475
588
|
skips.push({
|
|
476
589
|
...base,
|
|
477
590
|
reason: `service "${name}" ${key} ${JSON.stringify(raw[key])} has values Flisk does not accept; not used`,
|
|
478
591
|
});
|
|
479
592
|
}
|
|
480
|
-
// Every published port Flisk does not route stays published
|
|
593
|
+
// Every published port Flisk does not route stays published on the same host address (service.json extraPorts).
|
|
481
594
|
const proxied = selectedPort !== undefined && !NON_HTTP_PORTS.has(selectedPort);
|
|
482
595
|
const extraPorts = (Array.isArray(raw.ports) ? raw.ports : [])
|
|
483
596
|
.map(publishedPort)
|
|
@@ -549,6 +662,8 @@ registerDetector({
|
|
|
549
662
|
...(containerCommand ? { containerCommand } : {}),
|
|
550
663
|
...(containerEntrypoint ? { containerEntrypoint } : {}),
|
|
551
664
|
...(models ? { models } : {}),
|
|
665
|
+
...(profiles ? { profiles } : {}),
|
|
666
|
+
...(envFile ? { envFile } : {}),
|
|
552
667
|
...(healthProbe ? { healthCheck: healthProbe } : {}),
|
|
553
668
|
...(selectedPort !== undefined ? { port: selectedPort } : {}),
|
|
554
669
|
...(healthCheckPath ? { healthCheckPath } : {}),
|
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.26";
|
|
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.
|
package/dist/merge.js
CHANGED
|
@@ -74,6 +74,8 @@ function mergeGroup(name, dir, members, resolve) {
|
|
|
74
74
|
...firstOf(ordered, "containerCommand"),
|
|
75
75
|
...firstOf(ordered, "containerEntrypoint"),
|
|
76
76
|
...firstOf(ordered, "models"),
|
|
77
|
+
...firstOf(ordered, "profiles"),
|
|
78
|
+
...firstOf(ordered, "envFile"),
|
|
77
79
|
...firstOf(ordered, "healthCheck"),
|
|
78
80
|
...(ordered.find((c) => c.sidecarOf)?.sidecarOf
|
|
79
81
|
? { sidecarOf: resolve(ordered.find((c) => c.sidecarOf)?.sidecarOf) }
|
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) => {
|
|
@@ -253,6 +261,16 @@ export function planWrites(plan, force) {
|
|
|
253
261
|
const rel = relative(serviceDir, absolute).split("\\").join("/") || ".";
|
|
254
262
|
return [{ ...volume, source: rel.startsWith(".") ? rel : `./${rel}` }];
|
|
255
263
|
});
|
|
264
|
+
// env_file paths are relative to the service directory in service.json, and must stay inside the project.
|
|
265
|
+
const envFile = (service.envFile ?? []).flatMap((file) => {
|
|
266
|
+
const absolute = resolve(plan.root, file);
|
|
267
|
+
const fromProject = relative(projectRoot, absolute).split("\\").join("/");
|
|
268
|
+
if (fromProject === ".." || fromProject.startsWith("../")) {
|
|
269
|
+
notes.push(`env_file ${file} not written: it is outside the project`);
|
|
270
|
+
return [];
|
|
271
|
+
}
|
|
272
|
+
return [relative(serviceDir, absolute).split("\\").join("/")];
|
|
273
|
+
});
|
|
256
274
|
const manifest = {
|
|
257
275
|
$schema: SERVICE_MANIFEST_SCHEMA_URL,
|
|
258
276
|
schemaVersion: SERVICE_MANIFEST_SCHEMA_VERSION,
|
|
@@ -272,6 +290,8 @@ export function planWrites(plan, force) {
|
|
|
272
290
|
...(service.restart ? { restart: service.restart } : {}),
|
|
273
291
|
...(service.container ? { container: service.container } : {}),
|
|
274
292
|
...(service.models ? { models: service.models } : {}),
|
|
293
|
+
...(service.profiles ? { profiles: service.profiles } : {}),
|
|
294
|
+
...(envFile.length ? { envFile } : {}),
|
|
275
295
|
...(extraPorts.length ? { extraPorts } : {}),
|
|
276
296
|
...(sidecarOf ? { sidecarOf } : {}),
|
|
277
297
|
...(volumes.length
|
package/package.json
CHANGED