@flisk-sh/flisk-export 0.2.0 → 0.3.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/compose.js +85 -9
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -45,8 +45,9 @@ flisk-export compose --check # exit 1 when that file is out of date (for CI)
|
|
|
45
45
|
```
|
|
46
46
|
|
|
47
47
|
Writes a **standalone** `docker-compose.yml` from the bring-your-own `service.json` files (`image` or `dockerfile`, `env`,
|
|
48
|
-
`volumes`, `extraPorts`, `dependsOn`, `sidecarOf`, `container`). It runs with plain `docker compose up` and no Flisk: no external
|
|
49
|
-
networks, no `FLISK_*` variables, no generated secrets. `${VAR}` references in `env` pass through for Compose to read from `.env`.
|
|
48
|
+
`secrets.required`, `models`, `volumes`, `extraPorts`, `dependsOn`, `healthCheck`, `sidecarOf`, `container`). It runs with plain `docker compose up` and no Flisk: no external
|
|
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
|
+
`healthCheck.command` becomes a Compose `healthcheck`; a service that lists a checked service in `dependsOn` waits for `service_healthy`, and one that lists a `restart: "no"` job waits for `service_completed_successfully`.
|
|
50
51
|
Other resource types and `command` (native) services are not exported. The file is generated: change `service.json` and run it
|
|
51
52
|
again with `--force`. It never starts a container or calls an API.
|
|
52
53
|
|
package/dist/compose.js
CHANGED
|
@@ -40,7 +40,8 @@ function hostPath(outDir, absolute) {
|
|
|
40
40
|
function isNamedVolume(source) {
|
|
41
41
|
return !/[\\/]/.test(source) && !source.startsWith(".") && !source.startsWith("~");
|
|
42
42
|
}
|
|
43
|
-
function serviceLines(resource, outDir,
|
|
43
|
+
function serviceLines(resource, outDir, services) {
|
|
44
|
+
const known = services;
|
|
44
45
|
const c = resource.config;
|
|
45
46
|
const name = resource.name;
|
|
46
47
|
if (!SERVICE_NAME.test(name)) {
|
|
@@ -50,6 +51,7 @@ function serviceLines(resource, outDir, known) {
|
|
|
50
51
|
}
|
|
51
52
|
const lines = [` ${name}:`];
|
|
52
53
|
const namedVolumes = new Set();
|
|
54
|
+
const models = new Map();
|
|
53
55
|
const image = typeof c.image === "string" && c.image ? c.image : undefined;
|
|
54
56
|
const dockerfile = typeof c.dockerfile === "string" && c.dockerfile ? c.dockerfile : undefined;
|
|
55
57
|
const buildContext = typeof c.buildContext === "string" ? c.buildContext : undefined;
|
|
@@ -81,13 +83,17 @@ function serviceLines(resource, outDir, known) {
|
|
|
81
83
|
lines.push(` network_mode: ${q(`service:${sidecarOf}`)}`);
|
|
82
84
|
}
|
|
83
85
|
const env = record(c.env);
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
86
|
+
// Secrets are listed by name only in service.json; Compose reads their values from the project .env.
|
|
87
|
+
const values = new Map();
|
|
88
|
+
for (const key of strings(record(c.secrets).required))
|
|
89
|
+
values.set(key, `\${${key}}`);
|
|
90
|
+
for (const [key, value] of Object.entries(env))
|
|
91
|
+
values.set(key, String(value));
|
|
92
|
+
const envNames = [...values.keys()].filter((k) => ENV_NAME.test(k)).sort();
|
|
87
93
|
if (envNames.length > 0) {
|
|
88
94
|
lines.push(" environment:");
|
|
89
95
|
for (const key of envNames)
|
|
90
|
-
lines.push(` ${key}: ${q(
|
|
96
|
+
lines.push(` ${key}: ${q(values.get(key) ?? "")}`);
|
|
91
97
|
}
|
|
92
98
|
const volumes = Array.isArray(c.volumes) ? c.volumes : [];
|
|
93
99
|
if (volumes.length > 0) {
|
|
@@ -127,7 +133,26 @@ function serviceLines(resource, outDir, known) {
|
|
|
127
133
|
}
|
|
128
134
|
const dependsOn = strings(c.dependsOn).filter((d) => known.has(d));
|
|
129
135
|
if (dependsOn.length > 0) {
|
|
130
|
-
lines.push(" depends_on:"
|
|
136
|
+
lines.push(" depends_on:");
|
|
137
|
+
for (const d of dependsOn) {
|
|
138
|
+
lines.push(` ${d}:`, ` condition: ${dependencyCondition(known.get(d))}`);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
lines.push(...healthcheckLines(name, c.healthCheck));
|
|
142
|
+
// Docker Model Runner: the service lists model names, the top-level `models` block says which model each is.
|
|
143
|
+
const modelRefs = (Array.isArray(c.models) ? c.models : []).map(record);
|
|
144
|
+
if (modelRefs.length > 0) {
|
|
145
|
+
lines.push(" models:");
|
|
146
|
+
for (const ref of modelRefs) {
|
|
147
|
+
if (typeof ref.name !== "string" || !ENV_NAME.test(ref.name.replace(/[-.]/g, "_"))) {
|
|
148
|
+
throw new ExportError(`${name}: a model needs a name of letters, digits, dashes and dots`);
|
|
149
|
+
}
|
|
150
|
+
if (typeof ref.model !== "string" || !ref.model) {
|
|
151
|
+
throw new ExportError(`${name}: model "${ref.name}" needs a model reference`);
|
|
152
|
+
}
|
|
153
|
+
lines.push(` - ${q(ref.name)}`);
|
|
154
|
+
models.set(ref.name, ref.model);
|
|
155
|
+
}
|
|
131
156
|
}
|
|
132
157
|
const capAdd = strings(container.capAdd);
|
|
133
158
|
const capDrop = strings(container.capDrop);
|
|
@@ -149,7 +174,46 @@ function serviceLines(resource, outDir, known) {
|
|
|
149
174
|
}
|
|
150
175
|
lines.push(` shm_size: ${q(container.shmSize)}`);
|
|
151
176
|
}
|
|
152
|
-
return { lines, namedVolumes };
|
|
177
|
+
return { lines, namedVolumes, models };
|
|
178
|
+
}
|
|
179
|
+
/** What Compose waits for before starting a dependent: a passing check, a finished one-shot job, or the start. */
|
|
180
|
+
function dependencyCondition(dep) {
|
|
181
|
+
if (hasProbeCommand(dep.config.healthCheck))
|
|
182
|
+
return "service_healthy";
|
|
183
|
+
if (dep.config.restart === "no")
|
|
184
|
+
return "service_completed_successfully";
|
|
185
|
+
return "service_started";
|
|
186
|
+
}
|
|
187
|
+
function hasProbeCommand(value) {
|
|
188
|
+
return strings(record(value).command).length > 0;
|
|
189
|
+
}
|
|
190
|
+
/** A Compose `healthcheck` from `healthCheck.command` (exit 0 is ready); `healthCheck: false` turns it off. */
|
|
191
|
+
function healthcheckLines(name, value) {
|
|
192
|
+
if (value === false || record(value).enabled === false) {
|
|
193
|
+
return [" healthcheck:", " disable: true"];
|
|
194
|
+
}
|
|
195
|
+
const h = record(value);
|
|
196
|
+
const command = strings(h.command);
|
|
197
|
+
if (command.length === 0)
|
|
198
|
+
return [];
|
|
199
|
+
const lines = [" healthcheck:", ` test: [${['"CMD"', ...command.map(q)].join(", ")}]`];
|
|
200
|
+
for (const key of ["interval", "timeout"]) {
|
|
201
|
+
if (h[key] === undefined)
|
|
202
|
+
continue;
|
|
203
|
+
const seconds = Number(h[key]);
|
|
204
|
+
if (!Number.isFinite(seconds) || seconds <= 0) {
|
|
205
|
+
throw new ExportError(`${name}: healthCheck.${key} must be a number of seconds above 0`);
|
|
206
|
+
}
|
|
207
|
+
lines.push(` ${key}: ${q(`${seconds}s`)}`);
|
|
208
|
+
}
|
|
209
|
+
if (h.retries !== undefined) {
|
|
210
|
+
const retries = Number(h.retries);
|
|
211
|
+
if (!Number.isInteger(retries) || retries < 1) {
|
|
212
|
+
throw new ExportError(`${name}: healthCheck.retries must be a whole number above 0`);
|
|
213
|
+
}
|
|
214
|
+
lines.push(` retries: ${retries}`);
|
|
215
|
+
}
|
|
216
|
+
return lines;
|
|
153
217
|
}
|
|
154
218
|
function resolveFrom(base, rel) {
|
|
155
219
|
return rel.startsWith("/") ? rel : joinPosix(base, rel);
|
|
@@ -175,14 +239,17 @@ export function buildCompose(outDir, resources) {
|
|
|
175
239
|
`Exported type: ${COMPOSE_TYPE} (a service.json with an image or a Dockerfile, and no command)`,
|
|
176
240
|
]);
|
|
177
241
|
}
|
|
178
|
-
const known = new
|
|
242
|
+
const known = new Map(services.map((s) => [s.name, s]));
|
|
179
243
|
const body = [];
|
|
180
244
|
const volumes = new Set();
|
|
245
|
+
const models = new Map();
|
|
181
246
|
for (const service of services) {
|
|
182
247
|
const built = serviceLines(service, outDir, known);
|
|
183
248
|
body.push(...built.lines, "");
|
|
184
249
|
for (const v of built.namedVolumes)
|
|
185
250
|
volumes.add(v);
|
|
251
|
+
for (const [n, m] of built.models)
|
|
252
|
+
models.set(n, m);
|
|
186
253
|
}
|
|
187
254
|
const header = [
|
|
188
255
|
"###############################################################################",
|
|
@@ -194,6 +261,15 @@ export function buildCompose(outDir, resources) {
|
|
|
194
261
|
"",
|
|
195
262
|
"services:",
|
|
196
263
|
];
|
|
197
|
-
const tail =
|
|
264
|
+
const tail = [
|
|
265
|
+
...(models.size > 0
|
|
266
|
+
? [
|
|
267
|
+
"models:",
|
|
268
|
+
...[...models].sort().flatMap(([n, m]) => [` ${n}:`, ` model: ${q(m)}`]),
|
|
269
|
+
"",
|
|
270
|
+
]
|
|
271
|
+
: []),
|
|
272
|
+
...(volumes.size > 0 ? ["volumes:", ...[...volumes].sort().map((v) => ` ${v}:`), ""] : []),
|
|
273
|
+
];
|
|
198
274
|
return [...header, ...body, ...tail].join("\n").replace(/\n{3,}/g, "\n\n");
|
|
199
275
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@flisk-sh/flisk-export",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.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",
|