@flisk-sh/flisk-export 0.2.1 → 0.4.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 +2 -1
- package/dist/compose.js +60 -7
- 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
|
-
`secrets.required`, `volumes`, `extraPorts`, `dependsOn`, `healthCheck`, `sidecarOf`, `container`). It runs with plain `docker compose up` and no Flisk: no external
|
|
48
|
+
`secrets.required`, `models`, `volumes`, `extraPorts`, `dependsOn`, `healthCheck`, `sidecarOf`, `container`). It runs with plain `docker compose up` and no Flisk: no external
|
|
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
|
+
A volume's `name` (the Docker volume an install already has) is kept, and the project is named after its stack, so `docker compose up` finds the same data as before.
|
|
50
51
|
`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`.
|
|
51
52
|
Other resource types and `command` (native) services are not exported. The file is generated: change `service.json` and run it
|
|
52
53
|
again with `--force`. It never starts a container or calls an API.
|
package/dist/compose.js
CHANGED
|
@@ -13,6 +13,7 @@ const RESTART_POLICIES = new Set(["no", "on-failure", "unless-stopped", "always"
|
|
|
13
13
|
const SERVICE_NAME = /^[a-z0-9][a-z0-9_.-]*$/i;
|
|
14
14
|
const ENV_NAME = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
15
15
|
const SHM_SIZE = /^\d+(b|k|kb|m|mb|g|gb)?$/i;
|
|
16
|
+
const DOCKER_NAME = /^[A-Za-z0-9][A-Za-z0-9_.-]*$/;
|
|
16
17
|
export function isComposeExportable(resource) {
|
|
17
18
|
return resource.type === COMPOSE_TYPE && typeof resource.config.command !== "string";
|
|
18
19
|
}
|
|
@@ -50,7 +51,8 @@ function serviceLines(resource, outDir, services) {
|
|
|
50
51
|
]);
|
|
51
52
|
}
|
|
52
53
|
const lines = [` ${name}:`];
|
|
53
|
-
const namedVolumes = new
|
|
54
|
+
const namedVolumes = new Map();
|
|
55
|
+
const models = new Map();
|
|
54
56
|
const image = typeof c.image === "string" && c.image ? c.image : undefined;
|
|
55
57
|
const dockerfile = typeof c.dockerfile === "string" && c.dockerfile ? c.dockerfile : undefined;
|
|
56
58
|
const buildContext = typeof c.buildContext === "string" ? c.buildContext : undefined;
|
|
@@ -104,7 +106,11 @@ function serviceLines(resource, outDir, services) {
|
|
|
104
106
|
}
|
|
105
107
|
let source = v.source;
|
|
106
108
|
if (isNamedVolume(source)) {
|
|
107
|
-
|
|
109
|
+
const dockerName = typeof v.name === "string" ? v.name : undefined;
|
|
110
|
+
if (dockerName !== undefined && !DOCKER_NAME.test(dockerName)) {
|
|
111
|
+
throw new ExportError(`${name}: volume name "${dockerName}" is not a Docker volume name`);
|
|
112
|
+
}
|
|
113
|
+
namedVolumes.set(source, dockerName ?? namedVolumes.get(source));
|
|
108
114
|
}
|
|
109
115
|
else {
|
|
110
116
|
source = hostPath(outDir, resolveFrom(resource.path, source));
|
|
@@ -138,6 +144,21 @@ function serviceLines(resource, outDir, services) {
|
|
|
138
144
|
}
|
|
139
145
|
}
|
|
140
146
|
lines.push(...healthcheckLines(name, c.healthCheck));
|
|
147
|
+
// Docker Model Runner: the service lists model names, the top-level `models` block says which model each is.
|
|
148
|
+
const modelRefs = (Array.isArray(c.models) ? c.models : []).map(record);
|
|
149
|
+
if (modelRefs.length > 0) {
|
|
150
|
+
lines.push(" models:");
|
|
151
|
+
for (const ref of modelRefs) {
|
|
152
|
+
if (typeof ref.name !== "string" || !ENV_NAME.test(ref.name.replace(/[-.]/g, "_"))) {
|
|
153
|
+
throw new ExportError(`${name}: a model needs a name of letters, digits, dashes and dots`);
|
|
154
|
+
}
|
|
155
|
+
if (typeof ref.model !== "string" || !ref.model) {
|
|
156
|
+
throw new ExportError(`${name}: model "${ref.name}" needs a model reference`);
|
|
157
|
+
}
|
|
158
|
+
lines.push(` - ${q(ref.name)}`);
|
|
159
|
+
models.set(ref.name, ref.model);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
141
162
|
const capAdd = strings(container.capAdd);
|
|
142
163
|
const capDrop = strings(container.capDrop);
|
|
143
164
|
const securityOpt = strings(container.securityOpt);
|
|
@@ -158,7 +179,7 @@ function serviceLines(resource, outDir, services) {
|
|
|
158
179
|
}
|
|
159
180
|
lines.push(` shm_size: ${q(container.shmSize)}`);
|
|
160
181
|
}
|
|
161
|
-
return { lines, namedVolumes };
|
|
182
|
+
return { lines, namedVolumes, models };
|
|
162
183
|
}
|
|
163
184
|
/** What Compose waits for before starting a dependent: a passing check, a finished one-shot job, or the start. */
|
|
164
185
|
function dependencyCondition(dep) {
|
|
@@ -224,13 +245,24 @@ export function buildCompose(outDir, resources) {
|
|
|
224
245
|
]);
|
|
225
246
|
}
|
|
226
247
|
const known = new Map(services.map((s) => [s.name, s]));
|
|
248
|
+
const stacks = new Set(services
|
|
249
|
+
.map((s) => s.config.stack)
|
|
250
|
+
.filter((v) => typeof v === "string" && /^[a-z0-9][a-z0-9_-]*$/.test(v)));
|
|
227
251
|
const body = [];
|
|
228
|
-
const volumes = new
|
|
252
|
+
const volumes = new Map();
|
|
253
|
+
const models = new Map();
|
|
229
254
|
for (const service of services) {
|
|
230
255
|
const built = serviceLines(service, outDir, known);
|
|
231
256
|
body.push(...built.lines, "");
|
|
232
|
-
for (const v of built.namedVolumes)
|
|
233
|
-
volumes.
|
|
257
|
+
for (const [v, dockerName] of built.namedVolumes) {
|
|
258
|
+
const known = volumes.get(v);
|
|
259
|
+
if (known && dockerName && known !== dockerName) {
|
|
260
|
+
throw new ExportError(`volume "${v}" is given two names, "${known}" and "${dockerName}"`);
|
|
261
|
+
}
|
|
262
|
+
volumes.set(v, dockerName ?? known);
|
|
263
|
+
}
|
|
264
|
+
for (const [n, m] of built.models)
|
|
265
|
+
models.set(n, m);
|
|
234
266
|
}
|
|
235
267
|
const header = [
|
|
236
268
|
"###############################################################################",
|
|
@@ -240,8 +272,29 @@ export function buildCompose(outDir, resources) {
|
|
|
240
272
|
"# Runs with plain `docker compose up`; Flisk is not needed to start it.",
|
|
241
273
|
"###############################################################################",
|
|
242
274
|
"",
|
|
275
|
+
// Without a name Compose takes the folder's (autogenerated), so containers and unnamed volumes would not match the
|
|
276
|
+
// project the app had before. One stack gives the project its name.
|
|
277
|
+
...(stacks.size === 1 ? [`name: ${q([...stacks][0])}`, ""] : []),
|
|
243
278
|
"services:",
|
|
244
279
|
];
|
|
245
|
-
const tail =
|
|
280
|
+
const tail = [
|
|
281
|
+
...(models.size > 0
|
|
282
|
+
? [
|
|
283
|
+
"models:",
|
|
284
|
+
...[...models].sort().flatMap(([n, m]) => [` ${n}:`, ` model: ${q(m)}`]),
|
|
285
|
+
"",
|
|
286
|
+
]
|
|
287
|
+
: []),
|
|
288
|
+
// A volume with a name keeps the Docker volume an existing install has, so its data is used as it is.
|
|
289
|
+
...(volumes.size > 0
|
|
290
|
+
? [
|
|
291
|
+
"volumes:",
|
|
292
|
+
...[...volumes]
|
|
293
|
+
.sort(([a], [b]) => a.localeCompare(b))
|
|
294
|
+
.flatMap(([v, dockerName]) => dockerName ? [` ${v}:`, ` name: ${q(dockerName)}`] : [` ${v}:`]),
|
|
295
|
+
"",
|
|
296
|
+
]
|
|
297
|
+
: []),
|
|
298
|
+
];
|
|
246
299
|
return [...header, ...body, ...tail].join("\n").replace(/\n{3,}/g, "\n\n");
|
|
247
300
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@flisk-sh/flisk-export",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.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",
|