dokku-compose 0.3.6 → 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/dist/index.js +607 -487
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -27,6 +27,9 @@ var ChecksSchema = z.union([
|
|
|
27
27
|
skipped: z.array(z.string()).optional()
|
|
28
28
|
}).catchall(z.union([z.string(), z.number(), z.boolean()]))
|
|
29
29
|
]);
|
|
30
|
+
var GitSchema = z.object({
|
|
31
|
+
deploy_branch: z.string().optional()
|
|
32
|
+
});
|
|
30
33
|
var AppSchema = z.object({
|
|
31
34
|
domains: z.union([z.array(z.string()), z.literal(false)]).optional(),
|
|
32
35
|
links: z.array(z.string()).optional(),
|
|
@@ -61,12 +64,26 @@ var AppSchema = z.object({
|
|
|
61
64
|
build: z.array(z.string()).optional(),
|
|
62
65
|
deploy: z.array(z.string()).optional(),
|
|
63
66
|
run: z.array(z.string()).optional()
|
|
64
|
-
}).optional()
|
|
67
|
+
}).optional(),
|
|
68
|
+
git: GitSchema.optional()
|
|
69
|
+
});
|
|
70
|
+
var ServiceBackupAuthSchema = z.object({
|
|
71
|
+
access_key_id: z.string(),
|
|
72
|
+
secret_access_key: z.string(),
|
|
73
|
+
region: z.string(),
|
|
74
|
+
signature_version: z.string(),
|
|
75
|
+
endpoint: z.string()
|
|
76
|
+
});
|
|
77
|
+
var ServiceBackupSchema = z.object({
|
|
78
|
+
schedule: z.string(),
|
|
79
|
+
bucket: z.string(),
|
|
80
|
+
auth: ServiceBackupAuthSchema
|
|
65
81
|
});
|
|
66
82
|
var ServiceSchema = z.object({
|
|
67
83
|
plugin: z.string(),
|
|
68
84
|
version: z.string().optional(),
|
|
69
|
-
image: z.string().optional()
|
|
85
|
+
image: z.string().optional(),
|
|
86
|
+
backup: ServiceBackupSchema.optional()
|
|
70
87
|
});
|
|
71
88
|
var PluginSchema = z.object({
|
|
72
89
|
url: z.string().url(),
|
|
@@ -83,18 +100,23 @@ var ConfigSchema = z.object({
|
|
|
83
100
|
domains: z.union([z.array(z.string()), z.literal(false)]).optional(),
|
|
84
101
|
env: EnvMapSchema.optional(),
|
|
85
102
|
nginx: z.record(z.string(), z.union([z.string(), z.number()])).optional(),
|
|
86
|
-
logs: z.record(z.string(), z.union([z.string(), z.number()])).optional()
|
|
103
|
+
logs: z.record(z.string(), z.union([z.string(), z.number()])).optional(),
|
|
104
|
+
git: GitSchema.optional()
|
|
87
105
|
});
|
|
88
106
|
function parseConfig(raw) {
|
|
89
107
|
return ConfigSchema.parse(raw);
|
|
90
108
|
}
|
|
91
109
|
|
|
92
110
|
// src/core/config.ts
|
|
111
|
+
function interpolateEnvVars(content) {
|
|
112
|
+
return content.replace(/\$\{([^}]+)\}/g, (_, name) => process.env[name] ?? "");
|
|
113
|
+
}
|
|
93
114
|
function loadConfig(filePath) {
|
|
94
115
|
if (!fs.existsSync(filePath)) {
|
|
95
116
|
throw new Error(`Config file not found: ${filePath}`);
|
|
96
117
|
}
|
|
97
|
-
const
|
|
118
|
+
const content = fs.readFileSync(filePath, "utf8");
|
|
119
|
+
const raw = yaml.load(interpolateEnvVars(content));
|
|
98
120
|
return parseConfig(raw);
|
|
99
121
|
}
|
|
100
122
|
|
|
@@ -153,6 +175,69 @@ function createRunner(opts = {}) {
|
|
|
153
175
|
};
|
|
154
176
|
}
|
|
155
177
|
|
|
178
|
+
// src/core/context.ts
|
|
179
|
+
function createContext(runner) {
|
|
180
|
+
const cache = /* @__PURE__ */ new Map();
|
|
181
|
+
const commands = [];
|
|
182
|
+
return {
|
|
183
|
+
commands,
|
|
184
|
+
runner,
|
|
185
|
+
query(...args) {
|
|
186
|
+
const key = args.join("\0");
|
|
187
|
+
if (!cache.has(key)) {
|
|
188
|
+
cache.set(key, runner.query(...args));
|
|
189
|
+
}
|
|
190
|
+
return cache.get(key);
|
|
191
|
+
},
|
|
192
|
+
check(...args) {
|
|
193
|
+
return runner.check(...args);
|
|
194
|
+
},
|
|
195
|
+
async run(...args) {
|
|
196
|
+
commands.push(args);
|
|
197
|
+
await runner.run(...args);
|
|
198
|
+
},
|
|
199
|
+
close() {
|
|
200
|
+
return runner.close();
|
|
201
|
+
}
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// src/core/change.ts
|
|
206
|
+
function computeChange(before, after) {
|
|
207
|
+
if (before === null || before === void 0 || after === null || after === void 0) {
|
|
208
|
+
return { before, after, changed: before !== after };
|
|
209
|
+
}
|
|
210
|
+
if (Array.isArray(before) && Array.isArray(after)) {
|
|
211
|
+
const beforeSet = new Set(before);
|
|
212
|
+
const afterSet = new Set(after);
|
|
213
|
+
const added = after.filter((x) => !beforeSet.has(x));
|
|
214
|
+
const removed = before.filter((x) => !afterSet.has(x));
|
|
215
|
+
return {
|
|
216
|
+
before,
|
|
217
|
+
after,
|
|
218
|
+
changed: added.length > 0 || removed.length > 0,
|
|
219
|
+
added,
|
|
220
|
+
removed
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
if (typeof before === "object" && typeof after === "object") {
|
|
224
|
+
const b = before;
|
|
225
|
+
const a = after;
|
|
226
|
+
const allKeys = /* @__PURE__ */ new Set([...Object.keys(b), ...Object.keys(a)]);
|
|
227
|
+
const added = {};
|
|
228
|
+
const removed = [];
|
|
229
|
+
const modified = {};
|
|
230
|
+
for (const key of allKeys) {
|
|
231
|
+
if (!(key in b)) added[key] = a[key];
|
|
232
|
+
else if (!(key in a)) removed.push(key);
|
|
233
|
+
else if (String(b[key]) !== String(a[key])) modified[key] = a[key];
|
|
234
|
+
}
|
|
235
|
+
const changed = Object.keys(added).length > 0 || removed.length > 0 || Object.keys(modified).length > 0;
|
|
236
|
+
return { before, after, changed, added, removed, modified };
|
|
237
|
+
}
|
|
238
|
+
return { before, after, changed: before !== after };
|
|
239
|
+
}
|
|
240
|
+
|
|
156
241
|
// src/core/logger.ts
|
|
157
242
|
import chalk from "chalk";
|
|
158
243
|
function logAction(context, message) {
|
|
@@ -165,68 +250,326 @@ function logSkip() {
|
|
|
165
250
|
console.log(`... ${chalk.yellow("already configured")}`);
|
|
166
251
|
}
|
|
167
252
|
|
|
168
|
-
// src/
|
|
169
|
-
async function
|
|
170
|
-
|
|
171
|
-
logAction(
|
|
172
|
-
if (
|
|
173
|
-
|
|
253
|
+
// src/core/reconcile.ts
|
|
254
|
+
async function reconcile(resource, ctx, target, desired) {
|
|
255
|
+
if (desired === void 0) return;
|
|
256
|
+
logAction(target, `${resource.key}`);
|
|
257
|
+
if (resource.forceApply) {
|
|
258
|
+
await resource.onChange(ctx, target, { before: void 0, after: desired, changed: true });
|
|
259
|
+
logDone();
|
|
174
260
|
return;
|
|
175
261
|
}
|
|
176
|
-
await
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
async function destroyApp(runner, app) {
|
|
180
|
-
const exists = await runner.check("apps:exists", app);
|
|
181
|
-
logAction(app, "Destroying app");
|
|
182
|
-
if (!exists) {
|
|
262
|
+
const before = await resource.read(ctx, target);
|
|
263
|
+
const change = computeChange(before, desired);
|
|
264
|
+
if (!change.changed) {
|
|
183
265
|
logSkip();
|
|
184
266
|
return;
|
|
185
267
|
}
|
|
186
|
-
await
|
|
268
|
+
await resource.onChange(ctx, target, change);
|
|
187
269
|
logDone();
|
|
188
270
|
}
|
|
189
|
-
async function exportApps(runner) {
|
|
190
|
-
const output = await runner.query("apps:list");
|
|
191
|
-
return output.split("\n").map((s) => s.trim()).filter(
|
|
192
|
-
(s) => s && !s.startsWith("=====>")
|
|
193
|
-
);
|
|
194
|
-
}
|
|
195
271
|
|
|
196
|
-
// src/
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
272
|
+
// src/resources/lifecycle.ts
|
|
273
|
+
var Apps = {
|
|
274
|
+
key: "_app",
|
|
275
|
+
read: async (ctx, target) => {
|
|
276
|
+
return ctx.check("apps:exists", target);
|
|
277
|
+
},
|
|
278
|
+
onChange: async (ctx, target, { after }) => {
|
|
279
|
+
if (after) {
|
|
280
|
+
await ctx.run("apps:create", target);
|
|
281
|
+
} else {
|
|
282
|
+
await ctx.run("apps:destroy", target, "--force");
|
|
283
|
+
}
|
|
206
284
|
}
|
|
207
|
-
|
|
285
|
+
};
|
|
286
|
+
|
|
287
|
+
// src/resources/lists.ts
|
|
288
|
+
function splitWords(raw) {
|
|
289
|
+
return raw.split(/\s+/).map((s) => s.trim()).filter(Boolean);
|
|
208
290
|
}
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
291
|
+
function splitLines(raw) {
|
|
292
|
+
return raw.split("\n").map((s) => s.trim()).filter(Boolean);
|
|
293
|
+
}
|
|
294
|
+
var Ports = {
|
|
295
|
+
key: "ports",
|
|
296
|
+
read: async (ctx, target) => {
|
|
297
|
+
const raw = await ctx.query("ports:report", target, "--ports-map");
|
|
298
|
+
return splitWords(raw);
|
|
299
|
+
},
|
|
300
|
+
onChange: async (ctx, target, change) => {
|
|
301
|
+
await ctx.run("ports:set", target, ...change.after);
|
|
302
|
+
}
|
|
303
|
+
};
|
|
304
|
+
var Domains = {
|
|
305
|
+
key: "domains",
|
|
306
|
+
read: async (ctx, target) => {
|
|
307
|
+
const raw = await ctx.query("domains:report", target, "--domains-app-vhosts");
|
|
308
|
+
return splitLines(raw);
|
|
309
|
+
},
|
|
310
|
+
onChange: async (ctx, target, { added, removed }) => {
|
|
311
|
+
for (const d of removed) await ctx.run("domains:remove", target, d);
|
|
312
|
+
for (const d of added) await ctx.run("domains:add", target, d);
|
|
313
|
+
}
|
|
314
|
+
};
|
|
315
|
+
var Storage = {
|
|
316
|
+
key: "storage",
|
|
317
|
+
read: async (ctx, target) => {
|
|
318
|
+
const raw = await ctx.query("storage:report", target, "--storage-mounts");
|
|
319
|
+
return splitLines(raw);
|
|
320
|
+
},
|
|
321
|
+
onChange: async (ctx, target, { added, removed }) => {
|
|
322
|
+
for (const m of removed) await ctx.run("storage:unmount", target, m);
|
|
323
|
+
for (const m of added) await ctx.run("storage:mount", target, m);
|
|
324
|
+
}
|
|
325
|
+
};
|
|
326
|
+
|
|
327
|
+
// src/resources/parsers.ts
|
|
328
|
+
function parseReport(raw, namespace) {
|
|
329
|
+
const result = {};
|
|
330
|
+
const prefix = new RegExp(`^${namespace}\\s+`, "i");
|
|
331
|
+
for (const line of raw.split("\n")) {
|
|
332
|
+
if (line.trimStart().startsWith("=====>")) continue;
|
|
333
|
+
const colonIdx = line.indexOf(":");
|
|
334
|
+
if (colonIdx === -1) continue;
|
|
335
|
+
const rawKey = line.slice(0, colonIdx).trim();
|
|
336
|
+
if (!rawKey) continue;
|
|
337
|
+
const value = line.slice(colonIdx + 1).trim();
|
|
338
|
+
const stripped = rawKey.replace(prefix, "");
|
|
339
|
+
const key = stripped.toLowerCase().replace(/\s+/g, "-");
|
|
340
|
+
if (key.startsWith("computed-") || key.startsWith("global-") || key === "last-visited-at") continue;
|
|
341
|
+
if (!value) continue;
|
|
342
|
+
result[key] = value;
|
|
215
343
|
}
|
|
216
|
-
|
|
344
|
+
return result;
|
|
217
345
|
}
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
346
|
+
|
|
347
|
+
// src/resources/properties.ts
|
|
348
|
+
function propertyResource(opts) {
|
|
349
|
+
return {
|
|
350
|
+
key: opts.key,
|
|
351
|
+
async read(ctx, target) {
|
|
352
|
+
const raw = await ctx.query(`${opts.namespace}:report`, target);
|
|
353
|
+
return parseReport(raw, opts.namespace);
|
|
354
|
+
},
|
|
355
|
+
async onChange(ctx, target, change) {
|
|
356
|
+
for (const [key, value] of Object.entries({ ...change.added, ...change.modified })) {
|
|
357
|
+
await ctx.run(opts.setCmd, target, key, String(value));
|
|
358
|
+
}
|
|
359
|
+
if (opts.afterChange) {
|
|
360
|
+
for (const cmd of opts.afterChange) {
|
|
361
|
+
await ctx.run(cmd, target);
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
};
|
|
225
366
|
}
|
|
367
|
+
var Nginx = propertyResource({
|
|
368
|
+
key: "nginx",
|
|
369
|
+
namespace: "nginx",
|
|
370
|
+
setCmd: "nginx:set",
|
|
371
|
+
afterChange: ["proxy:build-config"]
|
|
372
|
+
});
|
|
373
|
+
var Logs = propertyResource({
|
|
374
|
+
key: "logs",
|
|
375
|
+
namespace: "logs",
|
|
376
|
+
setCmd: "logs:set"
|
|
377
|
+
});
|
|
378
|
+
var Registry = propertyResource({
|
|
379
|
+
key: "registry",
|
|
380
|
+
namespace: "registry",
|
|
381
|
+
setCmd: "registry:set"
|
|
382
|
+
});
|
|
383
|
+
var Scheduler = {
|
|
384
|
+
key: "scheduler",
|
|
385
|
+
async read(ctx, target) {
|
|
386
|
+
const raw = await ctx.query("scheduler:report", target);
|
|
387
|
+
const report = parseReport(raw, "scheduler");
|
|
388
|
+
return report["selected"] ?? "";
|
|
389
|
+
},
|
|
390
|
+
async onChange(ctx, target, change) {
|
|
391
|
+
await ctx.run("scheduler:set", target, "selected", change.after);
|
|
392
|
+
}
|
|
393
|
+
};
|
|
394
|
+
|
|
395
|
+
// src/resources/toggle.ts
|
|
396
|
+
var Proxy = {
|
|
397
|
+
key: "proxy",
|
|
398
|
+
read: async (ctx, target) => {
|
|
399
|
+
const raw = await ctx.query("proxy:report", target, "--proxy-enabled");
|
|
400
|
+
return raw.trim() === "true";
|
|
401
|
+
},
|
|
402
|
+
onChange: async (ctx, target, { after }) => {
|
|
403
|
+
await ctx.run(after ? "proxy:enable" : "proxy:disable", target);
|
|
404
|
+
}
|
|
405
|
+
};
|
|
406
|
+
|
|
407
|
+
// src/resources/config.ts
|
|
408
|
+
var MANAGED_KEYS_VAR = "DOKKU_COMPOSE_MANAGED_KEYS";
|
|
409
|
+
var Config = {
|
|
410
|
+
key: "env",
|
|
411
|
+
read: async (ctx, target) => {
|
|
412
|
+
const managedRaw = await ctx.query("config:get", target, MANAGED_KEYS_VAR);
|
|
413
|
+
const managedKeys = managedRaw.trim() ? managedRaw.trim().split(",").filter(Boolean) : [];
|
|
414
|
+
const result = {};
|
|
415
|
+
if (managedKeys.length > 0) {
|
|
416
|
+
const raw = await ctx.query("config:export", target, "--format", "shell");
|
|
417
|
+
for (const line of raw.split("\n")) {
|
|
418
|
+
const match = line.match(/^export\s+(\w+)=['"]?(.*?)['"]?$/);
|
|
419
|
+
if (match && managedKeys.includes(match[1])) {
|
|
420
|
+
result[match[1]] = match[2];
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
return result;
|
|
425
|
+
},
|
|
426
|
+
onChange: async (ctx, target, change) => {
|
|
427
|
+
const { added, removed, modified } = change;
|
|
428
|
+
if (removed.length > 0) {
|
|
429
|
+
await ctx.run("config:unset", "--no-restart", target, ...removed);
|
|
430
|
+
}
|
|
431
|
+
const toSet = { ...added, ...modified };
|
|
432
|
+
const allDesiredKeys = Object.keys(change.after);
|
|
433
|
+
const managedValue = allDesiredKeys.join(",");
|
|
434
|
+
if (Object.keys(toSet).length > 0 || removed.length > 0) {
|
|
435
|
+
const pairs = Object.entries(change.after).map(([k, v]) => `${k}=${v}`);
|
|
436
|
+
await ctx.run(
|
|
437
|
+
"config:set",
|
|
438
|
+
"--no-restart",
|
|
439
|
+
target,
|
|
440
|
+
...pairs,
|
|
441
|
+
`${MANAGED_KEYS_VAR}=${managedValue}`
|
|
442
|
+
);
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
};
|
|
446
|
+
|
|
447
|
+
// src/resources/certs.ts
|
|
448
|
+
var Certs = {
|
|
449
|
+
key: "ssl",
|
|
450
|
+
read: async (ctx, target) => {
|
|
451
|
+
const raw = await ctx.query("certs:report", target, "--ssl-enabled");
|
|
452
|
+
return raw.trim() === "true";
|
|
453
|
+
},
|
|
454
|
+
onChange: async (ctx, target, { before, after }) => {
|
|
455
|
+
if (after === false && before) {
|
|
456
|
+
await ctx.run("certs:remove", target);
|
|
457
|
+
}
|
|
458
|
+
if (after && typeof after === "object") {
|
|
459
|
+
await ctx.run("certs:add", target, after.certfile, after.keyfile);
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
};
|
|
463
|
+
|
|
464
|
+
// src/resources/builder.ts
|
|
465
|
+
var Builder = {
|
|
466
|
+
key: "build",
|
|
467
|
+
forceApply: true,
|
|
468
|
+
read: async () => ({}),
|
|
469
|
+
onChange: async (ctx, target, { after }) => {
|
|
470
|
+
if (after.dockerfile)
|
|
471
|
+
await ctx.run("builder-dockerfile:set", target, "dockerfile-path", after.dockerfile);
|
|
472
|
+
if (after.app_json)
|
|
473
|
+
await ctx.run("app-json:set", target, "appjson-path", after.app_json);
|
|
474
|
+
if (after.context)
|
|
475
|
+
await ctx.run("builder:set", target, "build-dir", after.context);
|
|
476
|
+
if (after.args) {
|
|
477
|
+
for (const [key, value] of Object.entries(after.args)) {
|
|
478
|
+
await ctx.run("docker-options:add", target, "build", `--build-arg ${key}=${value}`);
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
};
|
|
483
|
+
|
|
484
|
+
// src/resources/docker-options.ts
|
|
485
|
+
var DockerOptions = {
|
|
486
|
+
key: "docker_options",
|
|
487
|
+
forceApply: true,
|
|
488
|
+
read: async () => ({}),
|
|
489
|
+
onChange: async (ctx, target, { after }) => {
|
|
490
|
+
for (const phase of ["build", "deploy", "run"]) {
|
|
491
|
+
const opts = after[phase];
|
|
492
|
+
if (!opts || opts.length === 0) continue;
|
|
493
|
+
await ctx.run("docker-options:clear", target, phase);
|
|
494
|
+
for (const opt of opts) {
|
|
495
|
+
await ctx.run("docker-options:add", target, phase, opt);
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
};
|
|
500
|
+
|
|
501
|
+
// src/resources/git.ts
|
|
502
|
+
var Git = {
|
|
503
|
+
key: "git",
|
|
504
|
+
read: async (ctx, target) => {
|
|
505
|
+
const report = await ctx.query("git:report", target, "--git-deploy-branch");
|
|
506
|
+
return { deploy_branch: report.trim() || void 0 };
|
|
507
|
+
},
|
|
508
|
+
onChange: async (ctx, target, { after }) => {
|
|
509
|
+
if (after.deploy_branch) {
|
|
510
|
+
await ctx.run("git:set", target, "deploy-branch", after.deploy_branch);
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
};
|
|
514
|
+
|
|
515
|
+
// src/resources/checks.ts
|
|
516
|
+
var Checks = {
|
|
517
|
+
key: "checks",
|
|
518
|
+
forceApply: true,
|
|
519
|
+
read: async () => ({}),
|
|
520
|
+
onChange: async (ctx, target, { after }) => {
|
|
521
|
+
if (after === false) {
|
|
522
|
+
await ctx.run("checks:disable", target);
|
|
523
|
+
return;
|
|
524
|
+
}
|
|
525
|
+
if (after.disabled && after.disabled.length > 0) {
|
|
526
|
+
await ctx.run("checks:disable", target, ...after.disabled);
|
|
527
|
+
}
|
|
528
|
+
if (after.skipped && after.skipped.length > 0) {
|
|
529
|
+
await ctx.run("checks:skip", target, ...after.skipped);
|
|
530
|
+
}
|
|
531
|
+
for (const [key, value] of Object.entries(after)) {
|
|
532
|
+
if (key === "disabled" || key === "skipped") continue;
|
|
533
|
+
await ctx.run("checks:set", target, key, String(value));
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
};
|
|
537
|
+
|
|
538
|
+
// src/resources/network.ts
|
|
539
|
+
var Networks = {
|
|
540
|
+
key: "networks",
|
|
541
|
+
read: async (ctx, target) => {
|
|
542
|
+
const raw = await ctx.query("network:report", target, "--network-attach-post-deploy");
|
|
543
|
+
return raw.trim() ? raw.trim().split(/\s+/) : [];
|
|
544
|
+
},
|
|
545
|
+
onChange: async (ctx, target, { after }) => {
|
|
546
|
+
await ctx.run("network:set", target, "attach-post-deploy", ...after);
|
|
547
|
+
}
|
|
548
|
+
};
|
|
549
|
+
var NetworkProps = {
|
|
550
|
+
key: "network",
|
|
551
|
+
forceApply: true,
|
|
552
|
+
read: async () => ({}),
|
|
553
|
+
onChange: async (ctx, target, { after }) => {
|
|
554
|
+
if (after.attach_post_create !== void 0 && after.attach_post_create !== false) {
|
|
555
|
+
const nets = Array.isArray(after.attach_post_create) ? after.attach_post_create : [after.attach_post_create];
|
|
556
|
+
await ctx.run("network:set", target, "attach-post-create", ...nets);
|
|
557
|
+
}
|
|
558
|
+
if (after.initial_network !== void 0 && after.initial_network !== false) {
|
|
559
|
+
await ctx.run("network:set", target, "initial-network", after.initial_network);
|
|
560
|
+
}
|
|
561
|
+
if (after.bind_all_interfaces !== void 0) {
|
|
562
|
+
await ctx.run("network:set", target, "bind-all-interfaces", String(after.bind_all_interfaces));
|
|
563
|
+
}
|
|
564
|
+
if (after.tld !== void 0 && after.tld !== false) {
|
|
565
|
+
await ctx.run("network:set", target, "tld", after.tld);
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
};
|
|
226
569
|
|
|
227
570
|
// src/modules/plugins.ts
|
|
228
|
-
async function ensurePlugins(
|
|
229
|
-
const listOutput = await
|
|
571
|
+
async function ensurePlugins(ctx, plugins) {
|
|
572
|
+
const listOutput = await ctx.query("plugin:list");
|
|
230
573
|
const installedNames = new Set(
|
|
231
574
|
listOutput.split("\n").map((line) => line.trim().split(/\s+/)[0]).filter(Boolean)
|
|
232
575
|
);
|
|
@@ -236,514 +579,304 @@ async function ensurePlugins(runner, plugins) {
|
|
|
236
579
|
logSkip();
|
|
237
580
|
continue;
|
|
238
581
|
}
|
|
239
|
-
await
|
|
582
|
+
await ctx.run("plugin:install", config.url, "--name", name);
|
|
240
583
|
logDone();
|
|
241
584
|
}
|
|
242
585
|
}
|
|
243
586
|
|
|
244
587
|
// src/modules/network.ts
|
|
245
|
-
async function ensureNetworks(
|
|
588
|
+
async function ensureNetworks(ctx, networks) {
|
|
246
589
|
for (const net of networks) {
|
|
247
590
|
logAction("network", `Creating ${net}`);
|
|
248
|
-
const exists = await
|
|
591
|
+
const exists = await ctx.check("network:exists", net);
|
|
249
592
|
if (exists) {
|
|
250
593
|
logSkip();
|
|
251
594
|
continue;
|
|
252
595
|
}
|
|
253
|
-
await
|
|
596
|
+
await ctx.run("network:create", net);
|
|
254
597
|
logDone();
|
|
255
598
|
}
|
|
256
599
|
}
|
|
257
|
-
async function ensureAppNetworks(runner, app, networks) {
|
|
258
|
-
if (!networks || networks.length === 0) return;
|
|
259
|
-
logAction(app, "Setting networks");
|
|
260
|
-
await runner.run("network:set", app, "attach-post-deploy", ...networks);
|
|
261
|
-
logDone();
|
|
262
|
-
}
|
|
263
|
-
async function ensureAppNetwork(runner, app, network) {
|
|
264
|
-
if (!network) return;
|
|
265
|
-
if (network.attach_post_create !== void 0 && network.attach_post_create !== false) {
|
|
266
|
-
const nets = Array.isArray(network.attach_post_create) ? network.attach_post_create : [network.attach_post_create];
|
|
267
|
-
await runner.run("network:set", app, "attach-post-create", ...nets);
|
|
268
|
-
}
|
|
269
|
-
if (network.initial_network !== void 0 && network.initial_network !== false) {
|
|
270
|
-
await runner.run("network:set", app, "initial-network", network.initial_network);
|
|
271
|
-
}
|
|
272
|
-
if (network.bind_all_interfaces !== void 0) {
|
|
273
|
-
await runner.run("network:set", app, "bind-all-interfaces", String(network.bind_all_interfaces));
|
|
274
|
-
}
|
|
275
|
-
if (network.tld !== void 0 && network.tld !== false) {
|
|
276
|
-
await runner.run("network:set", app, "tld", network.tld);
|
|
277
|
-
}
|
|
278
|
-
}
|
|
279
600
|
var DOCKER_BUILTIN_NETWORKS = /* @__PURE__ */ new Set(["bridge", "host", "none"]);
|
|
280
|
-
async function exportNetworks(
|
|
281
|
-
const output = await
|
|
601
|
+
async function exportNetworks(ctx) {
|
|
602
|
+
const output = await ctx.query("network:list");
|
|
282
603
|
return output.split("\n").map((s) => s.trim()).filter((s) => s && !s.startsWith("=====>") && !DOCKER_BUILTIN_NETWORKS.has(s));
|
|
283
604
|
}
|
|
284
|
-
async function exportAppNetwork(runner, app) {
|
|
285
|
-
const output = await runner.query("network:report", app);
|
|
286
|
-
if (!output) return void 0;
|
|
287
|
-
const result = {};
|
|
288
|
-
const lines = output.split("\n");
|
|
289
|
-
for (const line of lines) {
|
|
290
|
-
const [key, ...valueParts] = line.split(":").map((s) => s.trim());
|
|
291
|
-
const value = valueParts.join(":").trim();
|
|
292
|
-
if (!key || !value) continue;
|
|
293
|
-
if (key === "Network attach post deploy") {
|
|
294
|
-
result.networks = value.split(" ").filter(Boolean);
|
|
295
|
-
}
|
|
296
|
-
}
|
|
297
|
-
return Object.keys(result).length > 0 ? result : void 0;
|
|
298
|
-
}
|
|
299
605
|
|
|
300
606
|
// src/modules/services.ts
|
|
301
|
-
|
|
607
|
+
import { createHash as createHash2 } from "crypto";
|
|
608
|
+
function backupHashKey(serviceName) {
|
|
609
|
+
return "DOKKU_COMPOSE_BACKUP_HASH_" + serviceName.toUpperCase().replace(/-/g, "_");
|
|
610
|
+
}
|
|
611
|
+
function computeBackupHash(backup) {
|
|
612
|
+
return createHash2("sha256").update(JSON.stringify(backup)).digest("hex");
|
|
613
|
+
}
|
|
614
|
+
async function ensureServices(ctx, services) {
|
|
302
615
|
for (const [name, config] of Object.entries(services)) {
|
|
303
616
|
logAction("services", `Ensuring ${name}`);
|
|
304
|
-
const exists = await
|
|
617
|
+
const exists = await ctx.check(`${config.plugin}:exists`, name);
|
|
305
618
|
if (exists) {
|
|
306
619
|
logSkip();
|
|
307
620
|
continue;
|
|
308
621
|
}
|
|
309
|
-
|
|
622
|
+
const args = [`${config.plugin}:create`, name];
|
|
623
|
+
if (config.image) args.push("--image", config.image);
|
|
624
|
+
if (config.version) args.push("--image-version", config.version);
|
|
625
|
+
await ctx.run(...args);
|
|
310
626
|
logDone();
|
|
311
627
|
}
|
|
312
628
|
}
|
|
313
|
-
async function
|
|
629
|
+
async function ensureServiceBackups(ctx, services) {
|
|
630
|
+
for (const [name, config] of Object.entries(services)) {
|
|
631
|
+
if (!config.backup) continue;
|
|
632
|
+
logAction("services", `Configuring backup for ${name}`);
|
|
633
|
+
const hashKey = backupHashKey(name);
|
|
634
|
+
const desiredHash = computeBackupHash(config.backup);
|
|
635
|
+
const storedHash = await ctx.query("config:get", "--global", hashKey);
|
|
636
|
+
if (storedHash === desiredHash) {
|
|
637
|
+
logSkip();
|
|
638
|
+
continue;
|
|
639
|
+
}
|
|
640
|
+
const { schedule, bucket, auth } = config.backup;
|
|
641
|
+
await ctx.run(`${config.plugin}:backup-deauth`, name);
|
|
642
|
+
await ctx.run(
|
|
643
|
+
`${config.plugin}:backup-auth`,
|
|
644
|
+
name,
|
|
645
|
+
auth.access_key_id,
|
|
646
|
+
auth.secret_access_key,
|
|
647
|
+
auth.region,
|
|
648
|
+
auth.signature_version,
|
|
649
|
+
auth.endpoint
|
|
650
|
+
);
|
|
651
|
+
await ctx.run(`${config.plugin}:backup-schedule`, name, schedule, bucket);
|
|
652
|
+
await ctx.run("config:set", "--global", `${hashKey}=${desiredHash}`);
|
|
653
|
+
logDone();
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
async function ensureAppLinks(ctx, app, desiredLinks, allServices) {
|
|
314
657
|
const desiredSet = new Set(desiredLinks);
|
|
315
658
|
for (const [serviceName, serviceConfig] of Object.entries(allServices)) {
|
|
316
|
-
const isLinked = await
|
|
659
|
+
const isLinked = await ctx.check(`${serviceConfig.plugin}:linked`, serviceName, app);
|
|
317
660
|
const isDesired = desiredSet.has(serviceName);
|
|
318
661
|
if (isDesired && !isLinked) {
|
|
319
662
|
logAction(app, `Linking ${serviceName}`);
|
|
320
|
-
await
|
|
663
|
+
await ctx.run(`${serviceConfig.plugin}:link`, serviceName, app, "--no-restart");
|
|
321
664
|
logDone();
|
|
322
665
|
} else if (!isDesired && isLinked) {
|
|
323
666
|
logAction(app, `Unlinking ${serviceName}`);
|
|
324
|
-
await
|
|
667
|
+
await ctx.run(`${serviceConfig.plugin}:unlink`, serviceName, app, "--no-restart");
|
|
325
668
|
logDone();
|
|
326
669
|
}
|
|
327
670
|
}
|
|
328
671
|
}
|
|
329
|
-
async function destroyAppLinks(
|
|
672
|
+
async function destroyAppLinks(ctx, app, links, allServices) {
|
|
330
673
|
for (const serviceName of links) {
|
|
331
674
|
const config = allServices[serviceName];
|
|
332
675
|
if (!config) continue;
|
|
333
|
-
const isLinked = await
|
|
676
|
+
const isLinked = await ctx.check(`${config.plugin}:linked`, serviceName, app);
|
|
334
677
|
if (isLinked) {
|
|
335
|
-
await
|
|
678
|
+
await ctx.run(`${config.plugin}:unlink`, serviceName, app, "--no-restart");
|
|
336
679
|
}
|
|
337
680
|
}
|
|
338
681
|
}
|
|
339
|
-
async function destroyServices(
|
|
682
|
+
async function destroyServices(ctx, services) {
|
|
340
683
|
for (const [name, config] of Object.entries(services)) {
|
|
341
684
|
logAction("services", `Destroying ${name}`);
|
|
342
|
-
const exists = await
|
|
685
|
+
const exists = await ctx.check(`${config.plugin}:exists`, name);
|
|
343
686
|
if (!exists) {
|
|
344
687
|
logSkip();
|
|
345
688
|
continue;
|
|
346
689
|
}
|
|
347
|
-
await
|
|
690
|
+
await ctx.run(`${config.plugin}:destroy`, name, "--force");
|
|
348
691
|
logDone();
|
|
349
692
|
}
|
|
350
693
|
}
|
|
351
|
-
async function exportServices(
|
|
694
|
+
async function exportServices(_ctx) {
|
|
352
695
|
return {};
|
|
353
696
|
}
|
|
354
|
-
async function exportAppLinks(
|
|
697
|
+
async function exportAppLinks(ctx, app, services) {
|
|
355
698
|
const linked = [];
|
|
356
699
|
for (const [serviceName, config] of Object.entries(services)) {
|
|
357
|
-
const isLinked = await
|
|
700
|
+
const isLinked = await ctx.check(`${config.plugin}:linked`, serviceName, app);
|
|
358
701
|
if (isLinked) linked.push(serviceName);
|
|
359
702
|
}
|
|
360
703
|
return linked;
|
|
361
704
|
}
|
|
362
705
|
|
|
363
|
-
// src/modules/
|
|
364
|
-
async function
|
|
365
|
-
logAction(
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
if (current === enabled) {
|
|
369
|
-
logSkip();
|
|
370
|
-
return;
|
|
371
|
-
}
|
|
372
|
-
if (enabled) {
|
|
373
|
-
await runner.run("proxy:enable", app);
|
|
706
|
+
// src/modules/domains.ts
|
|
707
|
+
async function ensureGlobalDomains(ctx, domains) {
|
|
708
|
+
logAction("global", "Configuring domains");
|
|
709
|
+
if (domains === false) {
|
|
710
|
+
await ctx.run("domains:clear-global");
|
|
374
711
|
} else {
|
|
375
|
-
await
|
|
712
|
+
await ctx.run("domains:set-global", ...domains);
|
|
376
713
|
}
|
|
377
714
|
logDone();
|
|
378
715
|
}
|
|
379
|
-
async function exportAppProxy(runner, app) {
|
|
380
|
-
const raw = await runner.query("proxy:report", app, "--proxy-enabled");
|
|
381
|
-
const enabled = raw.trim() === "true";
|
|
382
|
-
if (enabled) return void 0;
|
|
383
|
-
return { enabled };
|
|
384
|
-
}
|
|
385
716
|
|
|
386
|
-
// src/modules/
|
|
387
|
-
async function
|
|
388
|
-
|
|
389
|
-
const
|
|
390
|
-
|
|
391
|
-
const desired = [...ports].sort();
|
|
392
|
-
if (JSON.stringify(current) === JSON.stringify(desired)) {
|
|
393
|
-
logSkip();
|
|
394
|
-
return;
|
|
395
|
-
}
|
|
396
|
-
await runner.run("ports:set", app, ...ports);
|
|
397
|
-
logDone();
|
|
398
|
-
}
|
|
399
|
-
async function exportAppPorts(runner, app) {
|
|
400
|
-
const raw = await runner.query("ports:report", app, "--ports-map");
|
|
401
|
-
const ports = raw.split(/\s+/).map((s) => s.trim()).filter(Boolean);
|
|
402
|
-
if (ports.length === 0) return void 0;
|
|
403
|
-
return ports;
|
|
717
|
+
// src/modules/config.ts
|
|
718
|
+
async function ensureGlobalConfig(ctx, env) {
|
|
719
|
+
if (env === false) return;
|
|
720
|
+
const pairs = Object.entries(env).map(([k, v]) => `${k}=${v}`);
|
|
721
|
+
await ctx.run("config:set", "--global", ...pairs);
|
|
404
722
|
}
|
|
405
723
|
|
|
406
|
-
// src/modules/
|
|
407
|
-
async function
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
const enabled = enabledRaw.trim() === "true";
|
|
411
|
-
if (ssl === false) {
|
|
412
|
-
if (!enabled) {
|
|
413
|
-
logSkip();
|
|
414
|
-
return;
|
|
415
|
-
}
|
|
416
|
-
await runner.run("certs:remove", app);
|
|
417
|
-
logDone();
|
|
418
|
-
return;
|
|
419
|
-
}
|
|
420
|
-
if (ssl === true) {
|
|
421
|
-
if (enabled) {
|
|
422
|
-
logSkip();
|
|
423
|
-
return;
|
|
424
|
-
}
|
|
425
|
-
logSkip();
|
|
426
|
-
return;
|
|
427
|
-
}
|
|
428
|
-
if (enabled) {
|
|
429
|
-
logSkip();
|
|
430
|
-
return;
|
|
431
|
-
}
|
|
432
|
-
await runner.run("certs:add", app, ssl.certfile, ssl.keyfile);
|
|
433
|
-
logDone();
|
|
434
|
-
}
|
|
435
|
-
async function exportAppCerts(runner, app) {
|
|
436
|
-
const raw = await runner.query("certs:report", app, "--ssl-enabled");
|
|
437
|
-
const enabled = raw.trim() === "true";
|
|
438
|
-
if (!enabled) return void 0;
|
|
439
|
-
return true;
|
|
440
|
-
}
|
|
441
|
-
|
|
442
|
-
// src/modules/storage.ts
|
|
443
|
-
async function ensureAppStorage(runner, app, storage) {
|
|
444
|
-
logAction(app, "Configuring storage");
|
|
445
|
-
const currentRaw = await runner.query("storage:report", app, "--storage-mounts");
|
|
446
|
-
const current = currentRaw.split("\n").map((s) => s.trim()).filter(Boolean);
|
|
447
|
-
const desired = new Set(storage);
|
|
448
|
-
const currentSet = new Set(current);
|
|
449
|
-
const toUnmount = current.filter((m) => !desired.has(m));
|
|
450
|
-
const toMount = storage.filter((m) => !currentSet.has(m));
|
|
451
|
-
if (toUnmount.length === 0 && toMount.length === 0) {
|
|
452
|
-
logSkip();
|
|
453
|
-
return;
|
|
454
|
-
}
|
|
455
|
-
for (const mount of toUnmount) {
|
|
456
|
-
await runner.run("storage:unmount", app, mount);
|
|
457
|
-
}
|
|
458
|
-
for (const mount of toMount) {
|
|
459
|
-
await runner.run("storage:mount", app, mount);
|
|
724
|
+
// src/modules/logs.ts
|
|
725
|
+
async function ensureGlobalLogs(ctx, logs) {
|
|
726
|
+
for (const [key, value] of Object.entries(logs)) {
|
|
727
|
+
await ctx.run("logs:set", "--global", key, String(value));
|
|
460
728
|
}
|
|
461
|
-
logDone();
|
|
462
|
-
}
|
|
463
|
-
async function exportAppStorage(runner, app) {
|
|
464
|
-
const raw = await runner.query("storage:report", app, "--storage-mounts");
|
|
465
|
-
const mounts = raw.split("\n").map((s) => s.trim()).filter(Boolean);
|
|
466
|
-
if (mounts.length === 0) return void 0;
|
|
467
|
-
return mounts;
|
|
468
729
|
}
|
|
469
730
|
|
|
470
731
|
// src/modules/nginx.ts
|
|
471
|
-
async function
|
|
732
|
+
async function ensureGlobalNginx(ctx, nginx) {
|
|
472
733
|
for (const [key, value] of Object.entries(nginx)) {
|
|
473
|
-
await
|
|
734
|
+
await ctx.run("nginx:set", "--global", key, String(value));
|
|
474
735
|
}
|
|
475
736
|
}
|
|
476
|
-
async function ensureGlobalNginx(runner, nginx) {
|
|
477
|
-
for (const [key, value] of Object.entries(nginx)) {
|
|
478
|
-
await runner.run("nginx:set", "--global", key, String(value));
|
|
479
|
-
}
|
|
480
|
-
}
|
|
481
|
-
async function exportAppNginx(runner, app) {
|
|
482
|
-
const raw = await runner.query("nginx:report", app);
|
|
483
|
-
if (!raw) return void 0;
|
|
484
|
-
const result = {};
|
|
485
|
-
for (const line of raw.split("\n")) {
|
|
486
|
-
const match = line.match(/^\s*Nginx\s+(.+?):\s*(.+?)\s*$/);
|
|
487
|
-
if (match) {
|
|
488
|
-
const key = match[1].toLowerCase().replace(/\s+/g, "-");
|
|
489
|
-
if (key.startsWith("computed-") || key.startsWith("global-") || key === "last-visited-at") continue;
|
|
490
|
-
const value = match[2].trim();
|
|
491
|
-
if (!value) continue;
|
|
492
|
-
result[key] = value;
|
|
493
|
-
}
|
|
494
|
-
}
|
|
495
|
-
return Object.keys(result).length > 0 ? result : void 0;
|
|
496
|
-
}
|
|
497
737
|
|
|
498
|
-
// src/
|
|
499
|
-
async function
|
|
500
|
-
|
|
501
|
-
if (
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
if (
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
await
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
await
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
await runner.run("logs:set", "--global", key, String(value));
|
|
533
|
-
}
|
|
534
|
-
}
|
|
535
|
-
async function exportAppLogs(runner, app) {
|
|
536
|
-
const raw = await runner.query("logs:report", app);
|
|
537
|
-
if (!raw) return void 0;
|
|
538
|
-
return void 0;
|
|
539
|
-
}
|
|
540
|
-
|
|
541
|
-
// src/modules/registry.ts
|
|
542
|
-
async function ensureAppRegistry(runner, app, registry) {
|
|
543
|
-
for (const [key, value] of Object.entries(registry)) {
|
|
544
|
-
await runner.run("registry:set", app, key, String(value));
|
|
738
|
+
// src/commands/up.ts
|
|
739
|
+
async function runUp(ctx, config, appFilter) {
|
|
740
|
+
const apps = appFilter.length > 0 ? appFilter : Object.keys(config.apps);
|
|
741
|
+
if (config.plugins) await ensurePlugins(ctx, config.plugins);
|
|
742
|
+
if (config.domains !== void 0) await ensureGlobalDomains(ctx, config.domains);
|
|
743
|
+
if (config.env !== void 0) await ensureGlobalConfig(ctx, config.env);
|
|
744
|
+
if (config.logs !== void 0) await ensureGlobalLogs(ctx, config.logs);
|
|
745
|
+
if (config.nginx !== void 0) await ensureGlobalNginx(ctx, config.nginx);
|
|
746
|
+
if (config.networks) await ensureNetworks(ctx, config.networks);
|
|
747
|
+
if (config.services) await ensureServices(ctx, config.services);
|
|
748
|
+
if (config.services) await ensureServiceBackups(ctx, config.services);
|
|
749
|
+
for (const app of apps) {
|
|
750
|
+
const appConfig = config.apps[app];
|
|
751
|
+
if (!appConfig) continue;
|
|
752
|
+
await reconcile(Apps, ctx, app, true);
|
|
753
|
+
await reconcile(Domains, ctx, app, appConfig.domains);
|
|
754
|
+
await reconcile(Networks, ctx, app, appConfig.networks);
|
|
755
|
+
await reconcile(NetworkProps, ctx, app, appConfig.network);
|
|
756
|
+
await reconcile(Proxy, ctx, app, appConfig.proxy?.enabled);
|
|
757
|
+
await reconcile(Ports, ctx, app, appConfig.ports);
|
|
758
|
+
if (config.services) {
|
|
759
|
+
await ensureAppLinks(ctx, app, appConfig.links ?? [], config.services);
|
|
760
|
+
}
|
|
761
|
+
await reconcile(Certs, ctx, app, appConfig.ssl);
|
|
762
|
+
await reconcile(Storage, ctx, app, appConfig.storage);
|
|
763
|
+
await reconcile(Nginx, ctx, app, appConfig.nginx);
|
|
764
|
+
await reconcile(Checks, ctx, app, appConfig.checks);
|
|
765
|
+
await reconcile(Logs, ctx, app, appConfig.logs);
|
|
766
|
+
await reconcile(Registry, ctx, app, appConfig.registry);
|
|
767
|
+
await reconcile(Scheduler, ctx, app, appConfig.scheduler);
|
|
768
|
+
await reconcile(Config, ctx, app, appConfig.env);
|
|
769
|
+
await reconcile(Builder, ctx, app, appConfig.build);
|
|
770
|
+
await reconcile(Git, ctx, app, appConfig.git ?? config.git);
|
|
771
|
+
await reconcile(DockerOptions, ctx, app, appConfig.docker_options);
|
|
545
772
|
}
|
|
546
773
|
}
|
|
547
|
-
async function exportAppRegistry(runner, app) {
|
|
548
|
-
const raw = await runner.query("registry:report", app);
|
|
549
|
-
if (!raw) return void 0;
|
|
550
|
-
return void 0;
|
|
551
|
-
}
|
|
552
774
|
|
|
553
|
-
// src/modules/
|
|
554
|
-
async function
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
if (
|
|
775
|
+
// src/modules/apps.ts
|
|
776
|
+
async function destroyApp(ctx, app) {
|
|
777
|
+
const exists = await ctx.check("apps:exists", app);
|
|
778
|
+
logAction(app, "Destroying app");
|
|
779
|
+
if (!exists) {
|
|
558
780
|
logSkip();
|
|
559
781
|
return;
|
|
560
782
|
}
|
|
561
|
-
await
|
|
783
|
+
await ctx.run("apps:destroy", app, "--force");
|
|
562
784
|
logDone();
|
|
563
785
|
}
|
|
564
|
-
async function
|
|
565
|
-
const
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
return scheduler;
|
|
569
|
-
}
|
|
570
|
-
|
|
571
|
-
// src/modules/config.ts
|
|
572
|
-
var MANAGED_KEYS_VAR = "DOKKU_COMPOSE_MANAGED_KEYS";
|
|
573
|
-
async function ensureAppConfig(runner, app, env) {
|
|
574
|
-
logAction(app, "Configuring env vars");
|
|
575
|
-
if (env === false) {
|
|
576
|
-
logSkip();
|
|
577
|
-
return;
|
|
578
|
-
}
|
|
579
|
-
const prevManagedRaw = await runner.query("config:get", app, MANAGED_KEYS_VAR);
|
|
580
|
-
const prevManaged = prevManagedRaw.trim() ? prevManagedRaw.trim().split(",").filter(Boolean) : [];
|
|
581
|
-
const desiredKeys = Object.keys(env);
|
|
582
|
-
const toUnset = prevManaged.filter((k) => !desiredKeys.includes(k));
|
|
583
|
-
if (toUnset.length > 0) {
|
|
584
|
-
await runner.run("config:unset", "--no-restart", app, ...toUnset);
|
|
585
|
-
}
|
|
586
|
-
const pairs = Object.entries(env).map(([k, v]) => `${k}=${v}`);
|
|
587
|
-
const newManagedKeys = desiredKeys.join(",");
|
|
588
|
-
await runner.run(
|
|
589
|
-
"config:set",
|
|
590
|
-
"--no-restart",
|
|
591
|
-
app,
|
|
592
|
-
...pairs,
|
|
593
|
-
`${MANAGED_KEYS_VAR}=${newManagedKeys}`
|
|
786
|
+
async function exportApps(ctx) {
|
|
787
|
+
const output = await ctx.query("apps:list");
|
|
788
|
+
return output.split("\n").map((s) => s.trim()).filter(
|
|
789
|
+
(s) => s && !s.startsWith("=====>")
|
|
594
790
|
);
|
|
595
|
-
logDone();
|
|
596
|
-
}
|
|
597
|
-
async function ensureGlobalConfig(runner, env) {
|
|
598
|
-
if (env === false) return;
|
|
599
|
-
const pairs = Object.entries(env).map(([k, v]) => `${k}=${v}`);
|
|
600
|
-
await runner.run("config:set", "--global", ...pairs);
|
|
601
|
-
}
|
|
602
|
-
async function exportAppConfig(runner, app) {
|
|
603
|
-
const raw = await runner.query("config:export", app, "--format", "shell");
|
|
604
|
-
if (!raw) return void 0;
|
|
605
|
-
const result = {};
|
|
606
|
-
for (const line of raw.split("\n")) {
|
|
607
|
-
const match = line.match(/^export\s+(\w+)=['"]?(.*?)['"]?$/);
|
|
608
|
-
if (match && match[1] !== MANAGED_KEYS_VAR) {
|
|
609
|
-
result[match[1]] = match[2];
|
|
610
|
-
}
|
|
611
|
-
}
|
|
612
|
-
return Object.keys(result).length > 0 ? result : void 0;
|
|
613
|
-
}
|
|
614
|
-
|
|
615
|
-
// src/modules/builder.ts
|
|
616
|
-
async function ensureAppBuilder(runner, app, build) {
|
|
617
|
-
if (build.dockerfile) {
|
|
618
|
-
await runner.run("builder-dockerfile:set", app, "dockerfile-path", build.dockerfile);
|
|
619
|
-
}
|
|
620
|
-
if (build.app_json) {
|
|
621
|
-
await runner.run("app-json:set", app, "appjson-path", build.app_json);
|
|
622
|
-
}
|
|
623
|
-
if (build.context) {
|
|
624
|
-
await runner.run("builder:set", app, "build-dir", build.context);
|
|
625
|
-
}
|
|
626
|
-
if (build.args && Object.keys(build.args).length > 0) {
|
|
627
|
-
for (const [key, value] of Object.entries(build.args)) {
|
|
628
|
-
await runner.run("docker-options:add", app, "build", `--build-arg ${key}=${value}`);
|
|
629
|
-
}
|
|
630
|
-
}
|
|
631
|
-
}
|
|
632
|
-
|
|
633
|
-
// src/modules/docker-options.ts
|
|
634
|
-
async function ensureAppDockerOptions(runner, app, options) {
|
|
635
|
-
const phases = ["build", "deploy", "run"];
|
|
636
|
-
for (const phase of phases) {
|
|
637
|
-
const opts = options[phase];
|
|
638
|
-
if (!opts || opts.length === 0) continue;
|
|
639
|
-
await runner.run("docker-options:clear", app, phase);
|
|
640
|
-
for (const opt of opts) {
|
|
641
|
-
await runner.run("docker-options:add", app, phase, opt);
|
|
642
|
-
}
|
|
643
|
-
}
|
|
644
|
-
}
|
|
645
|
-
|
|
646
|
-
// src/commands/up.ts
|
|
647
|
-
async function runUp(runner, config, appFilter) {
|
|
648
|
-
const apps = appFilter.length > 0 ? appFilter : Object.keys(config.apps);
|
|
649
|
-
if (config.plugins) await ensurePlugins(runner, config.plugins);
|
|
650
|
-
if (config.domains !== void 0) await ensureGlobalDomains(runner, config.domains);
|
|
651
|
-
if (config.env !== void 0) await ensureGlobalConfig(runner, config.env);
|
|
652
|
-
if (config.logs !== void 0) await ensureGlobalLogs(runner, config.logs);
|
|
653
|
-
if (config.nginx !== void 0) await ensureGlobalNginx(runner, config.nginx);
|
|
654
|
-
if (config.networks) await ensureNetworks(runner, config.networks);
|
|
655
|
-
if (config.services) await ensureServices(runner, config.services);
|
|
656
|
-
for (const app of apps) {
|
|
657
|
-
const appConfig = config.apps[app];
|
|
658
|
-
if (!appConfig) continue;
|
|
659
|
-
await ensureApp(runner, app);
|
|
660
|
-
await ensureAppDomains(runner, app, appConfig.domains);
|
|
661
|
-
if (config.services) await ensureAppLinks(runner, app, appConfig.links ?? [], config.services);
|
|
662
|
-
await ensureAppNetworks(runner, app, appConfig.networks);
|
|
663
|
-
await ensureAppNetwork(runner, app, appConfig.network);
|
|
664
|
-
if (appConfig.proxy) await ensureAppProxy(runner, app, appConfig.proxy.enabled);
|
|
665
|
-
if (appConfig.ports) await ensureAppPorts(runner, app, appConfig.ports);
|
|
666
|
-
if (appConfig.ssl !== void 0) await ensureAppCerts(runner, app, appConfig.ssl);
|
|
667
|
-
if (appConfig.storage) await ensureAppStorage(runner, app, appConfig.storage);
|
|
668
|
-
if (appConfig.nginx) await ensureAppNginx(runner, app, appConfig.nginx);
|
|
669
|
-
if (appConfig.checks !== void 0) await ensureAppChecks(runner, app, appConfig.checks);
|
|
670
|
-
if (appConfig.logs) await ensureAppLogs(runner, app, appConfig.logs);
|
|
671
|
-
if (appConfig.registry) await ensureAppRegistry(runner, app, appConfig.registry);
|
|
672
|
-
if (appConfig.scheduler) await ensureAppScheduler(runner, app, appConfig.scheduler);
|
|
673
|
-
if (appConfig.env !== void 0) await ensureAppConfig(runner, app, appConfig.env);
|
|
674
|
-
if (appConfig.build) await ensureAppBuilder(runner, app, appConfig.build);
|
|
675
|
-
if (appConfig.docker_options) await ensureAppDockerOptions(runner, app, appConfig.docker_options);
|
|
676
|
-
}
|
|
677
791
|
}
|
|
678
792
|
|
|
679
793
|
// src/commands/down.ts
|
|
680
|
-
async function runDown(
|
|
794
|
+
async function runDown(ctx, config, appFilter, opts) {
|
|
681
795
|
const apps = appFilter.length > 0 ? appFilter : Object.keys(config.apps);
|
|
682
796
|
for (const app of apps) {
|
|
683
797
|
const appConfig = config.apps[app];
|
|
684
798
|
if (!appConfig) continue;
|
|
685
799
|
if (config.services && appConfig.links) {
|
|
686
|
-
await destroyAppLinks(
|
|
800
|
+
await destroyAppLinks(ctx, app, appConfig.links, config.services);
|
|
687
801
|
}
|
|
688
|
-
await destroyApp(
|
|
802
|
+
await destroyApp(ctx, app);
|
|
689
803
|
}
|
|
690
804
|
if (config.services) {
|
|
691
|
-
await destroyServices(
|
|
805
|
+
await destroyServices(ctx, config.services);
|
|
692
806
|
}
|
|
693
807
|
if (config.networks) {
|
|
694
808
|
for (const net of config.networks) {
|
|
695
809
|
logAction("network", `Destroying ${net}`);
|
|
696
|
-
const exists = await
|
|
810
|
+
const exists = await ctx.check("network:exists", net);
|
|
697
811
|
if (!exists) {
|
|
698
812
|
logSkip();
|
|
699
813
|
continue;
|
|
700
814
|
}
|
|
701
|
-
await
|
|
815
|
+
await ctx.run("network:destroy", net, "--force");
|
|
702
816
|
logDone();
|
|
703
817
|
}
|
|
704
818
|
}
|
|
705
819
|
}
|
|
706
820
|
|
|
821
|
+
// src/resources/index.ts
|
|
822
|
+
var NETWORKING_RESOURCES = [
|
|
823
|
+
Domains,
|
|
824
|
+
Networks,
|
|
825
|
+
NetworkProps,
|
|
826
|
+
Proxy,
|
|
827
|
+
Ports
|
|
828
|
+
];
|
|
829
|
+
var CONFIG_RESOURCES = [
|
|
830
|
+
Certs,
|
|
831
|
+
Storage,
|
|
832
|
+
Nginx,
|
|
833
|
+
Checks,
|
|
834
|
+
Logs,
|
|
835
|
+
Registry,
|
|
836
|
+
Scheduler,
|
|
837
|
+
Config
|
|
838
|
+
];
|
|
839
|
+
var BUILD_RESOURCES = [
|
|
840
|
+
Builder,
|
|
841
|
+
Git,
|
|
842
|
+
DockerOptions
|
|
843
|
+
];
|
|
844
|
+
var ALL_APP_RESOURCES = [
|
|
845
|
+
...NETWORKING_RESOURCES,
|
|
846
|
+
...CONFIG_RESOURCES,
|
|
847
|
+
...BUILD_RESOURCES
|
|
848
|
+
];
|
|
849
|
+
|
|
707
850
|
// src/commands/export.ts
|
|
708
|
-
async function runExport(
|
|
851
|
+
async function runExport(ctx, opts) {
|
|
709
852
|
const config = { apps: {} };
|
|
710
|
-
const versionOutput = await
|
|
853
|
+
const versionOutput = await ctx.query("version");
|
|
711
854
|
const versionMatch = versionOutput.match(/(\d+\.\d+\.\d+)/);
|
|
712
855
|
if (versionMatch) config.dokku = { version: versionMatch[1] };
|
|
713
|
-
const apps = opts.appFilter?.length ? opts.appFilter : await exportApps(
|
|
714
|
-
const networks = await exportNetworks(
|
|
856
|
+
const apps = opts.appFilter?.length ? opts.appFilter : await exportApps(ctx);
|
|
857
|
+
const networks = await exportNetworks(ctx);
|
|
715
858
|
if (networks.length > 0) config.networks = networks;
|
|
716
|
-
const services = await exportServices(
|
|
859
|
+
const services = await exportServices(ctx);
|
|
717
860
|
if (Object.keys(services).length > 0) config.services = services;
|
|
718
861
|
for (const app of apps) {
|
|
719
862
|
const appConfig = {};
|
|
720
|
-
const
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
if (
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
if (logs && Object.keys(logs).length) appConfig.logs = logs;
|
|
738
|
-
const registry = await exportAppRegistry(runner, app);
|
|
739
|
-
if (registry && Object.keys(registry).length) appConfig.registry = registry;
|
|
740
|
-
const scheduler = await exportAppScheduler(runner, app);
|
|
741
|
-
if (scheduler) appConfig.scheduler = scheduler;
|
|
742
|
-
const networkCfg = await exportAppNetwork(runner, app);
|
|
743
|
-
if (networkCfg?.networks?.length) appConfig.networks = networkCfg.networks;
|
|
744
|
-
if (networkCfg?.network) appConfig.network = networkCfg.network;
|
|
745
|
-
const env = await exportAppConfig(runner, app);
|
|
746
|
-
if (env && Object.keys(env).length) appConfig.env = env;
|
|
863
|
+
for (const resource of ALL_APP_RESOURCES) {
|
|
864
|
+
if (resource.key.startsWith("_")) continue;
|
|
865
|
+
if (resource.forceApply) continue;
|
|
866
|
+
const value = await resource.read(ctx, app);
|
|
867
|
+
if (value === void 0 || value === null || value === "") continue;
|
|
868
|
+
if (Array.isArray(value) && value.length === 0) continue;
|
|
869
|
+
if (typeof value === "object" && !Array.isArray(value) && Object.keys(value).length === 0) continue;
|
|
870
|
+
if (resource.key === "proxy") {
|
|
871
|
+
appConfig.proxy = { enabled: value };
|
|
872
|
+
} else {
|
|
873
|
+
appConfig[resource.key] = value;
|
|
874
|
+
}
|
|
875
|
+
}
|
|
876
|
+
if (Object.keys(services).length > 0) {
|
|
877
|
+
const links = await exportAppLinks(ctx, app, services);
|
|
878
|
+
if (links.length > 0) appConfig.links = links;
|
|
879
|
+
}
|
|
747
880
|
config.apps[app] = appConfig;
|
|
748
881
|
}
|
|
749
882
|
return config;
|
|
@@ -751,52 +884,38 @@ async function runExport(runner, opts) {
|
|
|
751
884
|
|
|
752
885
|
// src/commands/diff.ts
|
|
753
886
|
import chalk2 from "chalk";
|
|
754
|
-
function computeDiff(
|
|
887
|
+
async function computeDiff(ctx, config) {
|
|
755
888
|
const result = { apps: {}, services: {}, inSync: true };
|
|
756
|
-
for (const [app,
|
|
757
|
-
const currentApp = current.apps[app] ?? {};
|
|
889
|
+
for (const [app, appConfig] of Object.entries(config.apps)) {
|
|
758
890
|
const appDiff = {};
|
|
759
|
-
const
|
|
760
|
-
"
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
"
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
const d = desiredApp[feature];
|
|
776
|
-
const c = currentApp[feature];
|
|
777
|
-
if (d === void 0) continue;
|
|
778
|
-
const dStr = JSON.stringify(d);
|
|
779
|
-
const cStr = JSON.stringify(c);
|
|
780
|
-
if (c === void 0) {
|
|
781
|
-
appDiff[feature] = { status: "missing", desired: d, current: void 0 };
|
|
782
|
-
result.inSync = false;
|
|
783
|
-
} else if (dStr !== cStr) {
|
|
784
|
-
appDiff[feature] = { status: "changed", desired: d, current: c };
|
|
891
|
+
for (const resource of ALL_APP_RESOURCES) {
|
|
892
|
+
if (resource.key.startsWith("_")) continue;
|
|
893
|
+
if (resource.forceApply) continue;
|
|
894
|
+
let desired;
|
|
895
|
+
if (resource.key === "proxy") {
|
|
896
|
+
desired = appConfig.proxy?.enabled;
|
|
897
|
+
} else {
|
|
898
|
+
desired = appConfig[resource.key];
|
|
899
|
+
}
|
|
900
|
+
if (desired === void 0) continue;
|
|
901
|
+
const current = await resource.read(ctx, app);
|
|
902
|
+
const change = computeChange(current, desired);
|
|
903
|
+
if (!change.changed) {
|
|
904
|
+
appDiff[resource.key] = { status: "in-sync", desired, current };
|
|
905
|
+
} else if (current === null || current === void 0 || Array.isArray(current) && current.length === 0 || typeof current === "object" && Object.keys(current).length === 0) {
|
|
906
|
+
appDiff[resource.key] = { status: "missing", desired, current };
|
|
785
907
|
result.inSync = false;
|
|
786
908
|
} else {
|
|
787
|
-
appDiff[
|
|
909
|
+
appDiff[resource.key] = { status: "changed", desired, current };
|
|
910
|
+
result.inSync = false;
|
|
788
911
|
}
|
|
789
912
|
}
|
|
790
913
|
result.apps[app] = appDiff;
|
|
791
914
|
}
|
|
792
|
-
for (const [svc] of Object.entries(
|
|
793
|
-
const exists =
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
result.inSync = false;
|
|
797
|
-
} else {
|
|
798
|
-
result.services[svc] = { status: "in-sync" };
|
|
799
|
-
}
|
|
915
|
+
for (const [svc, svcConfig] of Object.entries(config.services ?? {})) {
|
|
916
|
+
const exists = await ctx.check(`${svcConfig.plugin}:exists`, svc);
|
|
917
|
+
result.services[svc] = { status: exists ? "in-sync" : "missing" };
|
|
918
|
+
if (!exists) result.inSync = false;
|
|
800
919
|
}
|
|
801
920
|
return result;
|
|
802
921
|
}
|
|
@@ -921,14 +1040,15 @@ function makeRunner(opts) {
|
|
|
921
1040
|
program.command("up [apps...]").description("Create/update apps and services to match config").option("-f, --file <path>", "Config file", "dokku-compose.yml").option("--dry-run", "Print commands without executing").option("--fail-fast", "Stop on first error").action(async (apps, opts) => {
|
|
922
1041
|
const config = loadConfig(opts.file);
|
|
923
1042
|
const runner = makeRunner(opts);
|
|
1043
|
+
const ctx = createContext(runner);
|
|
924
1044
|
try {
|
|
925
|
-
await runUp(
|
|
1045
|
+
await runUp(ctx, config, apps);
|
|
926
1046
|
if (opts.dryRun) {
|
|
927
1047
|
console.log("\n# Commands that would run:");
|
|
928
1048
|
for (const cmd of runner.dryRunLog) console.log(`dokku ${cmd}`);
|
|
929
1049
|
}
|
|
930
1050
|
} finally {
|
|
931
|
-
await
|
|
1051
|
+
await ctx.close();
|
|
932
1052
|
}
|
|
933
1053
|
});
|
|
934
1054
|
program.command("down [apps...]").description("Destroy apps and services (requires --force)").option("-f, --file <path>", "Config file", "dokku-compose.yml").option("--force", "Required to destroy apps").action(async (apps, opts) => {
|
|
@@ -938,10 +1058,11 @@ program.command("down [apps...]").description("Destroy apps and services (requir
|
|
|
938
1058
|
}
|
|
939
1059
|
const config = loadConfig(opts.file);
|
|
940
1060
|
const runner = makeRunner({});
|
|
1061
|
+
const ctx = createContext(runner);
|
|
941
1062
|
try {
|
|
942
|
-
await runDown(
|
|
1063
|
+
await runDown(ctx, config, apps, { force: true });
|
|
943
1064
|
} finally {
|
|
944
|
-
await
|
|
1065
|
+
await ctx.close();
|
|
945
1066
|
}
|
|
946
1067
|
});
|
|
947
1068
|
program.command("validate [file]").description("Validate dokku-compose.yml without touching the server").action((file = "dokku-compose.yml") => {
|
|
@@ -962,8 +1083,9 @@ ${result.errors.length} error(s), ${result.warnings.length} warning(s)`);
|
|
|
962
1083
|
});
|
|
963
1084
|
program.command("export").description("Export server state to dokku-compose.yml format").option("--app <app>", "Export only a specific app").option("-o, --output <path>", "Write to file instead of stdout").action(async (opts) => {
|
|
964
1085
|
const runner = makeRunner({});
|
|
1086
|
+
const ctx = createContext(runner);
|
|
965
1087
|
try {
|
|
966
|
-
const result = await runExport(
|
|
1088
|
+
const result = await runExport(ctx, {
|
|
967
1089
|
appFilter: opts.app ? [opts.app] : void 0
|
|
968
1090
|
});
|
|
969
1091
|
const out = yaml3.dump(result, { lineWidth: 120 });
|
|
@@ -974,22 +1096,20 @@ program.command("export").description("Export server state to dokku-compose.yml
|
|
|
974
1096
|
process.stdout.write(out);
|
|
975
1097
|
}
|
|
976
1098
|
} finally {
|
|
977
|
-
await
|
|
1099
|
+
await ctx.close();
|
|
978
1100
|
}
|
|
979
1101
|
});
|
|
980
1102
|
program.command("diff").description("Show what is out of sync between config and server").option("-f, --file <path>", "Config file", "dokku-compose.yml").option("--verbose", "Show git-style +/- diff").action(async (opts) => {
|
|
981
1103
|
const desired = loadConfig(opts.file);
|
|
982
1104
|
const runner = makeRunner({});
|
|
1105
|
+
const ctx = createContext(runner);
|
|
983
1106
|
try {
|
|
984
|
-
const
|
|
985
|
-
appFilter: Object.keys(desired.apps)
|
|
986
|
-
});
|
|
987
|
-
const diff = computeDiff(desired, current);
|
|
1107
|
+
const diff = await computeDiff(ctx, desired);
|
|
988
1108
|
const output = opts.verbose ? formatVerbose(diff) : formatSummary(diff);
|
|
989
1109
|
process.stdout.write(output);
|
|
990
1110
|
process.exit(diff.inSync ? 0 : 1);
|
|
991
1111
|
} finally {
|
|
992
|
-
await
|
|
1112
|
+
await ctx.close();
|
|
993
1113
|
}
|
|
994
1114
|
});
|
|
995
1115
|
program.command("ps [apps...]").description("Show status of configured apps").option("-f, --file <path>", "Config file", "dokku-compose.yml").action(async (apps, opts) => {
|