@flisk-sh/flisk-export 0.7.0 → 0.9.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 +1 -1
- package/dist/compose.js +72 -8
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -49,7 +49,7 @@ Writes a **standalone** `docker-compose.yml` from the bring-your-own `service.js
|
|
|
49
49
|
networks, no `FLISK_*` variables, no generated secrets. `${VAR}` references in `env`, and every name in `secrets.required`, pass through as `${NAME}` for Compose to read from `.env`.
|
|
50
50
|
A volume's `name` (the Docker volume an install already has) is kept, and the project is named after its stack (or, when `.flisk/project.json` has `composeProjectNames.<stack>` from flisk-import, the Compose project name it had before, such as `pl_mail`), so `docker compose up` finds the same data as before.
|
|
51
51
|
An `image` with Compose variables (`postgres:${POSTGRES_VERSION:-18}-alpine`) is written as it is, for Compose to resolve; `restart: "on-failure:N"` keeps N. From `container`: `stopGracePeriod` becomes `stop_grace_period`, `networkAliases` become `networks: { default: { aliases } }` (not on a `sidecarOf` service, which has no network of its own), `extraHosts` become `extra_hosts` and `ulimits` stay `ulimits`. Each value is checked so it cannot add YAML keys; one that does not fit is an error.
|
|
52
|
-
`healthCheck.command` becomes a Compose `healthcheck` (with `startPeriod` as `start_period`); a service that lists a checked service in `dependsOn` waits for `service_healthy` (only when that healthcheck is written: a service with only `healthCheckPath`, or `enabled: false`, is waited for with `service_started`), and one that lists a `restart: "no"` job waits for `service_completed_successfully`.
|
|
52
|
+
`healthCheck.command` becomes a Compose `healthcheck` (with `startPeriod` as `start_period`); a service that lists a checked service in `dependsOn` waits for `service_healthy` (only when that healthcheck is written: a service with only `healthCheckPath`, or `enabled: false`, is waited for with `service_started`), and a `dependsOn` entry written as `{ name, condition }` (flisk#2983) keeps its condition. `buildArgs` become `build.args`, `logging` stays `logging`, a bind source `${NAME:-./path}` keeps reading NAME with its default rewritten for this file, `selinux` becomes the `:z`/`:Z` mount option, and an image may use `${NAME:?message}` (Flisk 1.4.28 fields). A service with no `restart` gets none (Compose's default), as the project's own Compose file had it. and one that lists a `restart: "no"` job waits for `service_completed_successfully`.
|
|
53
53
|
Other resource types and `command` (native) services are not exported. The file is generated: change `service.json` and run it
|
|
54
54
|
again with `--force`. It never starts a container or calls an API.
|
|
55
55
|
|
package/dist/compose.js
CHANGED
|
@@ -11,10 +11,19 @@ import { ExportError } from "./errors.js";
|
|
|
11
11
|
/** extraPorts `publish`: digits, dots, colons and `${NAME:-default}` / `${NAME-default}`, as flisk core accepts it (flisk#2947). */
|
|
12
12
|
const PUBLISH = /^(?:[0-9.:]|\$\{[A-Za-z_][A-Za-z0-9_]*:?-[0-9.:]+\})+$/;
|
|
13
13
|
/** An image reference, with Compose variables `${NAME}`, `${NAME:-default}` and `${NAME-default}` kept for Compose to resolve. */
|
|
14
|
-
const IMAGE = /^(?:[A-Za-z0-9._/:@-]|\$\{[A-Za-z_][A-Za-z0-9_]*(?::?-[A-Za-z0-9._/:@-]*)?\})+$/;
|
|
14
|
+
const IMAGE = /^(?:[A-Za-z0-9._/:@-]|\$\{[A-Za-z_][A-Za-z0-9_]*(?::?-[A-Za-z0-9._/:@-]+|:?\?[A-Za-z0-9 !#%&()*+,./:;<=>?@[\]^_|~-]*)?\})+$/;
|
|
15
|
+
/** A bind source from the environment, `${NAME:-./path}` (flisk 1.4.28): NAME, the operator and the default path. */
|
|
16
|
+
const ENV_BIND = /^\$\{([A-Za-z_][A-Za-z0-9_]*)(:?-)(\.{1,2}(?:\/[A-Za-z0-9_.-]+)*\/?)\}$/;
|
|
17
|
+
const LOG_DRIVER = /^[a-z0-9][a-z0-9_.-]*$/;
|
|
18
|
+
const LOG_OPTION = /^[A-Za-z0-9][A-Za-z0-9_.-]*$/;
|
|
15
19
|
/** The only resource type the Compose export covers: it is the one whose service.json is a Compose service. */
|
|
16
20
|
export const COMPOSE_TYPE = "bring-your-own";
|
|
17
21
|
const RESTART_POLICIES = new Set(["no", "on-failure", "unless-stopped", "always"]);
|
|
22
|
+
const CONDITIONS = new Set([
|
|
23
|
+
"service_started",
|
|
24
|
+
"service_healthy",
|
|
25
|
+
"service_completed_successfully",
|
|
26
|
+
]);
|
|
18
27
|
/** `on-failure:N` with a retry limit from 1 to 100. */
|
|
19
28
|
const RESTART_LIMIT = /^on-failure:([1-9][0-9]?|100)$/;
|
|
20
29
|
/** A Compose duration such as 30s, 1m30s or 500ms. */
|
|
@@ -86,6 +95,16 @@ function serviceLines(resource, outDir, services) {
|
|
|
86
95
|
lines.push(" build:", ` context: ${q(context)}`);
|
|
87
96
|
if (dockerfile)
|
|
88
97
|
lines.push(` dockerfile: ${q(dockerfile)}`);
|
|
98
|
+
// buildArgs (flisk#2983): NAME to a string, which may use Compose variables such as ${NODE_VERSION:-22}.
|
|
99
|
+
const args = Object.entries(record(c.buildArgs));
|
|
100
|
+
for (const [arg, value] of args) {
|
|
101
|
+
if (!ENV_NAME.test(arg) || typeof value !== "string") {
|
|
102
|
+
throw new ExportError(`${name}: buildArgs "${arg}" needs a name of letters, digits and _ and a string`);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
if (args.length > 0) {
|
|
106
|
+
lines.push(" args:", ...args.map(([arg, value]) => ` ${arg}: ${q(value)}`));
|
|
107
|
+
}
|
|
89
108
|
}
|
|
90
109
|
const container = record(c.container);
|
|
91
110
|
if (container.init !== false)
|
|
@@ -96,9 +115,7 @@ function serviceLines(resource, outDir, services) {
|
|
|
96
115
|
}
|
|
97
116
|
lines.push(` restart: ${q(c.restart)}`);
|
|
98
117
|
}
|
|
99
|
-
|
|
100
|
-
lines.push(' restart: "unless-stopped"');
|
|
101
|
-
}
|
|
118
|
+
// No restart in service.json is written as none: Compose's default, as the project's own Compose file had it.
|
|
102
119
|
const sidecarOf = typeof c.sidecarOf === "string" ? c.sidecarOf : undefined;
|
|
103
120
|
if (sidecarOf) {
|
|
104
121
|
if (!known.has(sidecarOf)) {
|
|
@@ -140,7 +157,15 @@ function serviceLines(resource, outDir, services) {
|
|
|
140
157
|
throw new ExportError(`${name}: a volume needs a source and a target`);
|
|
141
158
|
}
|
|
142
159
|
let source = v.source;
|
|
143
|
-
|
|
160
|
+
const fromEnv = source.match(ENV_BIND);
|
|
161
|
+
if (v.selinux !== undefined && v.selinux !== "z" && v.selinux !== "Z") {
|
|
162
|
+
throw new ExportError(`${name}: volume selinux must be z or Z`);
|
|
163
|
+
}
|
|
164
|
+
if (fromEnv) {
|
|
165
|
+
// The default is relative to the service directory; Compose reads it relative to this file.
|
|
166
|
+
source = `\${${fromEnv[1]}${fromEnv[2]}${hostPath(outDir, resolveFrom(resource.path, fromEnv[3]))}}`;
|
|
167
|
+
}
|
|
168
|
+
else if (isNamedVolume(source)) {
|
|
144
169
|
const dockerName = typeof v.name === "string" ? v.name : undefined;
|
|
145
170
|
if (dockerName !== undefined && !DOCKER_NAME.test(dockerName)) {
|
|
146
171
|
throw new ExportError(`${name}: volume name "${dockerName}" is not a Docker volume name`);
|
|
@@ -150,7 +175,11 @@ function serviceLines(resource, outDir, services) {
|
|
|
150
175
|
else {
|
|
151
176
|
source = hostPath(outDir, resolveFrom(resource.path, source));
|
|
152
177
|
}
|
|
153
|
-
|
|
178
|
+
const options = [
|
|
179
|
+
...(v.readOnly === true ? ["ro"] : []),
|
|
180
|
+
...(typeof v.selinux === "string" ? [v.selinux] : []),
|
|
181
|
+
];
|
|
182
|
+
lines.push(` - ${q(`${source}:${v.target}${options.length ? `:${options.join(",")}` : ""}`)}`);
|
|
154
183
|
}
|
|
155
184
|
}
|
|
156
185
|
const ports = Array.isArray(c.extraPorts) ? c.extraPorts : [];
|
|
@@ -182,11 +211,26 @@ function serviceLines(resource, outDir, services) {
|
|
|
182
211
|
lines.push(` - ${q(`${bind}:${hostPort}:${port}/${protocol}`)}`);
|
|
183
212
|
}
|
|
184
213
|
}
|
|
185
|
-
|
|
214
|
+
// dependsOn entries are names or { name, condition } (flisk#2983); a stated condition wins over the guess.
|
|
215
|
+
const dependsOn = (Array.isArray(c.dependsOn) ? c.dependsOn : [])
|
|
216
|
+
.map((entry) => typeof entry === "string"
|
|
217
|
+
? { name: entry, condition: undefined }
|
|
218
|
+
: { name: record(entry).name, condition: record(entry).condition })
|
|
219
|
+
.filter((d) => typeof d.name === "string" && known.has(d.name));
|
|
186
220
|
if (dependsOn.length > 0) {
|
|
187
221
|
lines.push(" depends_on:");
|
|
188
222
|
for (const d of dependsOn) {
|
|
189
|
-
|
|
223
|
+
const dep = known.get(d.name);
|
|
224
|
+
if (d.condition !== undefined && !CONDITIONS.has(d.condition)) {
|
|
225
|
+
throw new ExportError(`${name}: dependsOn "${d.name}" condition is not a Compose condition`, ["Use service_started, service_healthy or service_completed_successfully"]);
|
|
226
|
+
}
|
|
227
|
+
// service_healthy needs a healthcheck in this file; without one Compose would refuse to start.
|
|
228
|
+
const condition = d.condition === undefined
|
|
229
|
+
? dependencyCondition(dep)
|
|
230
|
+
: d.condition === "service_healthy" && !hasProbeCommand(dep.config.healthCheck)
|
|
231
|
+
? "service_started"
|
|
232
|
+
: d.condition;
|
|
233
|
+
lines.push(` ${d.name}:`, ` condition: ${condition}`);
|
|
190
234
|
}
|
|
191
235
|
}
|
|
192
236
|
lines.push(...healthcheckLines(name, c.healthCheck));
|
|
@@ -288,6 +332,26 @@ function serviceLines(resource, outDir, services) {
|
|
|
288
332
|
lines.push(` - path: ${q(hostPath(outDir, resolveFrom(resource.path, file)))}`, " required: false");
|
|
289
333
|
}
|
|
290
334
|
}
|
|
335
|
+
// logging (flisk 1.4.28): a driver and string options, checked so nothing can add a key.
|
|
336
|
+
if (c.logging !== undefined) {
|
|
337
|
+
const logging = record(c.logging);
|
|
338
|
+
const options = Object.entries(record(logging.options));
|
|
339
|
+
if (logging.driver !== undefined &&
|
|
340
|
+
(typeof logging.driver !== "string" || !LOG_DRIVER.test(logging.driver))) {
|
|
341
|
+
throw new ExportError(`${name}: logging.driver "${String(logging.driver)}" is not a logging driver`);
|
|
342
|
+
}
|
|
343
|
+
for (const [key, value] of options) {
|
|
344
|
+
if (!LOG_OPTION.test(key) || typeof value !== "string") {
|
|
345
|
+
throw new ExportError(`${name}: logging option "${key}" needs a name of letters, digits, _ . - and a string`);
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
lines.push(" logging:", ...(typeof logging.driver === "string" ? [` driver: ${q(logging.driver)}`] : []), ...(options.length
|
|
349
|
+
? [
|
|
350
|
+
" options:",
|
|
351
|
+
...options.map(([key, value]) => ` ${key}: ${q(value)}`),
|
|
352
|
+
]
|
|
353
|
+
: []));
|
|
354
|
+
}
|
|
291
355
|
const profiles = strings(c.profiles);
|
|
292
356
|
if (profiles.length > 0) {
|
|
293
357
|
for (const p of profiles) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@flisk-sh/flisk-export",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.0",
|
|
4
4
|
"description": "Write a Render Blueprint (render.yaml), a Cloudflare wrangler.toml or a standalone docker-compose.yml from Flisk service.json files. Never deploys.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|