dokku-compose 0.11.1 → 0.13.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 +53 -6
- package/dist/index.js +773 -130
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -53,6 +53,12 @@ var AppSchema = z.object({
|
|
|
53
53
|
logs: z.record(z.string(), z.union([z.string(), z.number()])).optional(),
|
|
54
54
|
registry: z.record(z.string(), z.union([z.string(), z.boolean()])).optional(),
|
|
55
55
|
scheduler: z.string().optional(),
|
|
56
|
+
scale: z.record(z.string(), z.preprocess((v) => typeof v === "string" && /^\s*-?\d+\s*$/.test(v) ? Number(v) : v, z.number().int().min(0))).optional(),
|
|
57
|
+
ps: z.object({
|
|
58
|
+
"procfile-path": z.union([z.string(), z.number()]).optional(),
|
|
59
|
+
"restart-policy": z.union([z.string(), z.number()]).optional(),
|
|
60
|
+
"stop-timeout-seconds": z.union([z.string(), z.number()]).optional()
|
|
61
|
+
}).strict().optional(),
|
|
56
62
|
checks: ChecksSchema.optional(),
|
|
57
63
|
build: z.object({
|
|
58
64
|
dockerfile: z.string().optional(),
|
|
@@ -117,16 +123,29 @@ function parseConfig(raw) {
|
|
|
117
123
|
}
|
|
118
124
|
|
|
119
125
|
// src/core/config.ts
|
|
120
|
-
function
|
|
126
|
+
function interpolateValue(value, missing) {
|
|
127
|
+
if (typeof value === "string") {
|
|
128
|
+
return value.replace(/\$\$\{|\$\{([^}]+)\}/g, (match, name) => {
|
|
129
|
+
if (name === void 0) return "${";
|
|
130
|
+
const v = process.env[name];
|
|
131
|
+
if (v === void 0 || v === "") {
|
|
132
|
+
missing.push(name);
|
|
133
|
+
return "";
|
|
134
|
+
}
|
|
135
|
+
return v;
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
if (Array.isArray(value)) return value.map((v) => interpolateValue(v, missing));
|
|
139
|
+
if (value !== null && typeof value === "object") {
|
|
140
|
+
const out = {};
|
|
141
|
+
for (const [k, v] of Object.entries(value)) out[k] = interpolateValue(v, missing);
|
|
142
|
+
return out;
|
|
143
|
+
}
|
|
144
|
+
return value;
|
|
145
|
+
}
|
|
146
|
+
function interpolateEnvVars(raw) {
|
|
121
147
|
const missing = [];
|
|
122
|
-
const result =
|
|
123
|
-
const value = process.env[name];
|
|
124
|
-
if (value === void 0 || value === "") {
|
|
125
|
-
missing.push(name);
|
|
126
|
-
return "";
|
|
127
|
-
}
|
|
128
|
-
return value;
|
|
129
|
-
});
|
|
148
|
+
const result = interpolateValue(raw, missing);
|
|
130
149
|
if (missing.length > 0) {
|
|
131
150
|
const unique = Array.from(new Set(missing));
|
|
132
151
|
throw new Error(
|
|
@@ -140,7 +159,7 @@ function loadConfig(filePath) {
|
|
|
140
159
|
throw new Error(`Config file not found: ${filePath}`);
|
|
141
160
|
}
|
|
142
161
|
const content = fs.readFileSync(filePath, "utf8");
|
|
143
|
-
const raw = yaml.load(
|
|
162
|
+
const raw = interpolateEnvVars(yaml.load(content));
|
|
144
163
|
return parseConfig(raw);
|
|
145
164
|
}
|
|
146
165
|
|
|
@@ -149,6 +168,95 @@ import { execa } from "execa";
|
|
|
149
168
|
import { createHash } from "crypto";
|
|
150
169
|
import * as os from "os";
|
|
151
170
|
import * as path from "path";
|
|
171
|
+
|
|
172
|
+
// src/core/mask.ts
|
|
173
|
+
var SENSITIVE_RE = /(?:TOKEN|SECRET|PASSWORD|KEY|AUTH|CREDENTIAL)/i;
|
|
174
|
+
function maskValue(value) {
|
|
175
|
+
if (value.length <= 4) return "****";
|
|
176
|
+
return `****${value.slice(-4)}`;
|
|
177
|
+
}
|
|
178
|
+
function maskSensitiveArgs(cmd) {
|
|
179
|
+
let masked = cmd.replace(/([^\s=]*(?:TOKEN|SECRET|PASSWORD|KEY|AUTH|CREDENTIAL)[^\s=]*)=(\S+)/gi, (_, key, value) => {
|
|
180
|
+
return `${key}=${maskValue(value)}`;
|
|
181
|
+
});
|
|
182
|
+
masked = masked.replace(
|
|
183
|
+
/^(registry:login\s+(?:--global\s+)?\S+\s+\S+\s+)(\S+)/,
|
|
184
|
+
(_, prefix, password) => `${prefix}${maskValue(password)}`
|
|
185
|
+
);
|
|
186
|
+
masked = masked.replace(
|
|
187
|
+
/^(\S+:backup-auth\s+\S+\s+)(\S+)(\s+)(\S+)/,
|
|
188
|
+
(_, prefix, accessKeyId, sep, secretAccessKey) => `${prefix}${maskValue(accessKeyId)}${sep}${maskValue(secretAccessKey)}`
|
|
189
|
+
);
|
|
190
|
+
return masked;
|
|
191
|
+
}
|
|
192
|
+
function maskSensitiveArgsList(args) {
|
|
193
|
+
const secrets = [];
|
|
194
|
+
const masked = [...args];
|
|
195
|
+
const mark = (index) => {
|
|
196
|
+
const value = masked[index];
|
|
197
|
+
if (value === void 0 || value === "") return;
|
|
198
|
+
secrets.push(value);
|
|
199
|
+
masked[index] = maskValue(value);
|
|
200
|
+
};
|
|
201
|
+
for (let i = 0; i < masked.length; i++) {
|
|
202
|
+
const m = masked[i].match(/^([^\s=]*(?:TOKEN|SECRET|PASSWORD|KEY|AUTH|CREDENTIAL)[^\s=]*)=(.+)$/i);
|
|
203
|
+
if (m) {
|
|
204
|
+
secrets.push(m[2]);
|
|
205
|
+
masked[i] = `${m[1]}=${maskValue(m[2])}`;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
if (masked[0] === "registry:login") {
|
|
209
|
+
mark(masked.length - 1);
|
|
210
|
+
}
|
|
211
|
+
if (/^\S+:backup-auth$/.test(masked[0] ?? "")) {
|
|
212
|
+
mark(2);
|
|
213
|
+
mark(3);
|
|
214
|
+
}
|
|
215
|
+
return { masked, secrets };
|
|
216
|
+
}
|
|
217
|
+
function maskSecretsInText(text, secrets) {
|
|
218
|
+
let masked = text;
|
|
219
|
+
for (const secret of secrets) {
|
|
220
|
+
if (!secret) continue;
|
|
221
|
+
masked = masked.split(secret).join(maskValue(secret));
|
|
222
|
+
}
|
|
223
|
+
return masked;
|
|
224
|
+
}
|
|
225
|
+
function maskSensitiveData(data) {
|
|
226
|
+
if (data === null || data === void 0) return data;
|
|
227
|
+
if (typeof data === "string") return data;
|
|
228
|
+
if (Array.isArray(data)) return data.map(maskSensitiveData);
|
|
229
|
+
if (typeof data === "object") {
|
|
230
|
+
const result = {};
|
|
231
|
+
for (const [key, value] of Object.entries(data)) {
|
|
232
|
+
if (typeof value === "string" && SENSITIVE_RE.test(key)) {
|
|
233
|
+
result[key] = maskValue(value);
|
|
234
|
+
} else if (typeof value === "object" && value !== null) {
|
|
235
|
+
result[key] = maskSensitiveData(value);
|
|
236
|
+
} else {
|
|
237
|
+
result[key] = value;
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
return result;
|
|
241
|
+
}
|
|
242
|
+
return data;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// src/core/dokku.ts
|
|
246
|
+
var DokkuCommandError = class extends Error {
|
|
247
|
+
constructor(args, exitCode, stderr) {
|
|
248
|
+
const { masked, secrets } = maskSensitiveArgsList(args);
|
|
249
|
+
const maskedCmd = masked.join(" ");
|
|
250
|
+
const stderrTail = maskSecretsInText(stderr.trim(), secrets);
|
|
251
|
+
super(
|
|
252
|
+
`dokku ${maskedCmd} failed (exit ${exitCode})` + (stderrTail ? `: ${stderrTail}` : "")
|
|
253
|
+
);
|
|
254
|
+
this.args = args;
|
|
255
|
+
this.exitCode = exitCode;
|
|
256
|
+
this.stderr = stderr;
|
|
257
|
+
this.name = "DokkuCommandError";
|
|
258
|
+
}
|
|
259
|
+
};
|
|
152
260
|
function createRunner(opts = {}) {
|
|
153
261
|
const log = [];
|
|
154
262
|
const controlPath = opts.host ? path.join(os.tmpdir(), `dc-${createHash("sha1").update(opts.host).digest("hex").slice(0, 16)}.sock`) : null;
|
|
@@ -160,17 +268,17 @@ function createRunner(opts = {}) {
|
|
|
160
268
|
async function execDokku(args) {
|
|
161
269
|
if (opts.host) {
|
|
162
270
|
try {
|
|
163
|
-
const result = await execa("ssh", [...sshControlFlags, `dokku@${opts.host}`, "--", ...args.map(shellQuote)]);
|
|
164
|
-
return { stdout: String(result.stdout ?? ""), ok: true };
|
|
271
|
+
const result = await execa("ssh", [...sshControlFlags, `dokku@${opts.host}`, "--", ...args.map(shellQuote)], { stdin: "ignore" });
|
|
272
|
+
return { stdout: String(result.stdout ?? ""), stderr: String(result.stderr ?? ""), exitCode: result.exitCode ?? 0, ok: true };
|
|
165
273
|
} catch (e) {
|
|
166
|
-
return { stdout: String(e.stdout ?? ""), ok: false };
|
|
274
|
+
return { stdout: String(e.stdout ?? ""), stderr: String(e.stderr ?? ""), exitCode: e.exitCode ?? 1, ok: false };
|
|
167
275
|
}
|
|
168
276
|
} else {
|
|
169
277
|
try {
|
|
170
|
-
const result = await execa("dokku", args);
|
|
171
|
-
return { stdout: String(result.stdout ?? ""), ok: true };
|
|
278
|
+
const result = await execa("dokku", args, { stdin: "ignore" });
|
|
279
|
+
return { stdout: String(result.stdout ?? ""), stderr: String(result.stderr ?? ""), exitCode: result.exitCode ?? 0, ok: true };
|
|
172
280
|
} catch (e) {
|
|
173
|
-
return { stdout: String(e.stdout ?? ""), ok: false };
|
|
281
|
+
return { stdout: String(e.stdout ?? ""), stderr: String(e.stderr ?? ""), exitCode: e.exitCode ?? 1, ok: false };
|
|
174
282
|
}
|
|
175
283
|
}
|
|
176
284
|
}
|
|
@@ -181,12 +289,20 @@ function createRunner(opts = {}) {
|
|
|
181
289
|
log.push(args.map((a) => a.includes(" ") ? shellQuote(a) : a).join(" "));
|
|
182
290
|
return;
|
|
183
291
|
}
|
|
184
|
-
await execDokku(args);
|
|
292
|
+
const { ok, exitCode, stderr } = await execDokku(args);
|
|
293
|
+
if (!ok) {
|
|
294
|
+
throw new DokkuCommandError(args, exitCode, stderr);
|
|
295
|
+
}
|
|
185
296
|
},
|
|
186
297
|
async query(...args) {
|
|
187
298
|
const { stdout } = await execDokku(args);
|
|
188
299
|
return stdout.trim();
|
|
189
300
|
},
|
|
301
|
+
async queryStrict(...args) {
|
|
302
|
+
const { stdout, ok, exitCode, stderr } = await execDokku(args);
|
|
303
|
+
if (!ok) throw new DokkuCommandError(args, exitCode, stderr);
|
|
304
|
+
return stdout.trim();
|
|
305
|
+
},
|
|
190
306
|
async check(...args) {
|
|
191
307
|
const { ok } = await execDokku(args);
|
|
192
308
|
return ok;
|
|
@@ -194,7 +310,7 @@ function createRunner(opts = {}) {
|
|
|
194
310
|
async close() {
|
|
195
311
|
if (!opts.host || !controlPath) return;
|
|
196
312
|
try {
|
|
197
|
-
await execa("ssh", ["-O", "exit", "-o", `ControlPath=${controlPath}`, `dokku@${opts.host}`]);
|
|
313
|
+
await execa("ssh", ["-O", "exit", "-o", `ControlPath=${controlPath}`, `dokku@${opts.host}`], { stdin: "ignore" });
|
|
198
314
|
} catch {
|
|
199
315
|
}
|
|
200
316
|
}
|
|
@@ -215,6 +331,9 @@ function createContext(runner) {
|
|
|
215
331
|
}
|
|
216
332
|
return cache.get(key);
|
|
217
333
|
},
|
|
334
|
+
queryStrict(...args) {
|
|
335
|
+
return runner.queryStrict(...args);
|
|
336
|
+
},
|
|
218
337
|
check(...args) {
|
|
219
338
|
return runner.check(...args);
|
|
220
339
|
},
|
|
@@ -263,6 +382,11 @@ function computeChange(before, after) {
|
|
|
263
382
|
}
|
|
264
383
|
return { before, after, changed: before !== after };
|
|
265
384
|
}
|
|
385
|
+
function computeAdditiveChange(before, after) {
|
|
386
|
+
const change = computeChange(before ?? {}, after);
|
|
387
|
+
change.changed = Object.keys(change.added).length > 0 || Object.keys(change.modified).length > 0;
|
|
388
|
+
return change;
|
|
389
|
+
}
|
|
266
390
|
|
|
267
391
|
// src/core/logger.ts
|
|
268
392
|
import chalk from "chalk";
|
|
@@ -275,21 +399,49 @@ function logDone() {
|
|
|
275
399
|
function logSkip() {
|
|
276
400
|
console.log(`... ${chalk.yellow("already configured")}`);
|
|
277
401
|
}
|
|
402
|
+
function logError(context, message) {
|
|
403
|
+
console.error(chalk.red(`[${context.padEnd(12)}] ERROR: ${message}`));
|
|
404
|
+
}
|
|
278
405
|
function logWarn(context, message) {
|
|
279
406
|
console.warn(chalk.yellow(`[${context.padEnd(12)}] WARN: ${message}`));
|
|
280
407
|
}
|
|
281
408
|
|
|
282
409
|
// src/core/reconcile.ts
|
|
283
|
-
async function reconcile(resource, ctx, target, desired) {
|
|
284
|
-
|
|
410
|
+
async function reconcile(resource, ctx, target, desired, onRemovals, prune = false) {
|
|
411
|
+
const canPrune = prune && !!resource.prune && !!resource.removals;
|
|
412
|
+
const pruneFrom = async (current) => {
|
|
413
|
+
const removed = resource.removals(current, desired);
|
|
414
|
+
if (removed.length === 0) return;
|
|
415
|
+
logAction(target, `${resource.key} (prune)`);
|
|
416
|
+
await resource.prune(ctx, target, removed, current, desired);
|
|
417
|
+
logDone();
|
|
418
|
+
};
|
|
419
|
+
if (desired === void 0) {
|
|
420
|
+
if (canPrune) await pruneFrom(await resource.read(ctx, target));
|
|
421
|
+
return;
|
|
422
|
+
}
|
|
285
423
|
logAction(target, `${resource.key}`);
|
|
286
424
|
if (resource.forceApply) {
|
|
287
425
|
await resource.onChange(ctx, target, { before: void 0, after: desired, changed: true });
|
|
288
426
|
logDone();
|
|
427
|
+
if (canPrune) await pruneFrom(await resource.read(ctx, target));
|
|
289
428
|
return;
|
|
290
429
|
}
|
|
291
430
|
const before = await resource.read(ctx, target);
|
|
292
|
-
|
|
431
|
+
if (onRemovals && resource.removals) onRemovals(resource.removals(before, desired));
|
|
432
|
+
if (canPrune) {
|
|
433
|
+
const change2 = resource.additive ? computeAdditiveChange(before, desired) : computeChange(before, desired);
|
|
434
|
+
const pending = resource.removals(before, desired).length > 0;
|
|
435
|
+
if (change2.changed) {
|
|
436
|
+
await resource.onChange(ctx, target, change2, pending);
|
|
437
|
+
logDone();
|
|
438
|
+
} else {
|
|
439
|
+
logSkip();
|
|
440
|
+
}
|
|
441
|
+
await pruneFrom(before);
|
|
442
|
+
return;
|
|
443
|
+
}
|
|
444
|
+
const change = resource.additive ? computeAdditiveChange(before, desired) : computeChange(before, desired);
|
|
293
445
|
if (!change.changed) {
|
|
294
446
|
logSkip();
|
|
295
447
|
return;
|
|
@@ -421,9 +573,73 @@ var Storage = {
|
|
|
421
573
|
};
|
|
422
574
|
|
|
423
575
|
// src/resources/properties.ts
|
|
576
|
+
async function isAppDeployed(ctx, target) {
|
|
577
|
+
const raw = await ctx.query("ps:report", target, "--deployed");
|
|
578
|
+
return raw.trim() === "true";
|
|
579
|
+
}
|
|
580
|
+
var NGINX_KEYS = [
|
|
581
|
+
"access-log-format",
|
|
582
|
+
"access-log-path",
|
|
583
|
+
"bind-address-ipv4",
|
|
584
|
+
"bind-address-ipv6",
|
|
585
|
+
"client-body-timeout",
|
|
586
|
+
"client-header-timeout",
|
|
587
|
+
"client-max-body-size",
|
|
588
|
+
"disable-custom-config",
|
|
589
|
+
"error-log-path",
|
|
590
|
+
"hsts",
|
|
591
|
+
"hsts-include-subdomains",
|
|
592
|
+
"hsts-max-age",
|
|
593
|
+
"hsts-preload",
|
|
594
|
+
"keepalive-timeout",
|
|
595
|
+
"lingering-timeout",
|
|
596
|
+
"proxy-buffer-size",
|
|
597
|
+
"proxy-buffering",
|
|
598
|
+
"proxy-buffers",
|
|
599
|
+
"proxy-busy-buffers-size",
|
|
600
|
+
"proxy-connect-timeout",
|
|
601
|
+
"proxy-keepalive",
|
|
602
|
+
"proxy-read-timeout",
|
|
603
|
+
"proxy-send-timeout",
|
|
604
|
+
"send-timeout",
|
|
605
|
+
"underscore-in-headers",
|
|
606
|
+
"x-forwarded-for-value",
|
|
607
|
+
"x-forwarded-port-value",
|
|
608
|
+
"x-forwarded-proto-value",
|
|
609
|
+
"x-forwarded-ssl"
|
|
610
|
+
];
|
|
611
|
+
var LOGS_KEYS = ["app-label-alias", "max-size", "vector-cron-sink", "vector-sink"];
|
|
612
|
+
var REGISTRY_KEYS = [
|
|
613
|
+
"auth-servers",
|
|
614
|
+
"image-repo",
|
|
615
|
+
"image-repo-template",
|
|
616
|
+
"push-extra-tags",
|
|
617
|
+
"push-on-release",
|
|
618
|
+
"server"
|
|
619
|
+
];
|
|
620
|
+
function propertyRemovals(allowed) {
|
|
621
|
+
return (current, desired) => Object.keys(current ?? {}).filter((k) => allowed.includes(k) && !(desired && k in desired));
|
|
622
|
+
}
|
|
623
|
+
function propertyPrune(setCmd, afterPrune = []) {
|
|
624
|
+
return async (ctx, target, removed) => {
|
|
625
|
+
for (const key of removed) await ctx.run(setCmd, target, key);
|
|
626
|
+
for (const cmd of afterPrune) {
|
|
627
|
+
try {
|
|
628
|
+
await ctx.run(cmd, target);
|
|
629
|
+
} catch (err) {
|
|
630
|
+
if (await isAppDeployed(ctx, target)) throw err;
|
|
631
|
+
logWarn(target, `${cmd} failed on an undeployed app \u2014 its proxy config will be built on first deploy`);
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
};
|
|
635
|
+
}
|
|
424
636
|
function propertyResource(opts) {
|
|
425
637
|
return {
|
|
426
638
|
key: opts.key,
|
|
639
|
+
// Keys only on the server never make the block "changed"; diff shows them as removals.
|
|
640
|
+
additive: true,
|
|
641
|
+
removals: propertyRemovals(opts.settable),
|
|
642
|
+
prune: propertyPrune(opts.setCmd, opts.afterChange),
|
|
427
643
|
async read(ctx, target) {
|
|
428
644
|
const raw = await ctx.query(`${opts.namespace}:report`, target);
|
|
429
645
|
return parseReport(raw, opts.namespace);
|
|
@@ -432,13 +648,18 @@ function propertyResource(opts) {
|
|
|
432
648
|
const raw = await ctx.query(`${opts.namespace}:report`);
|
|
433
649
|
return parseBulkReport(raw, opts.namespace);
|
|
434
650
|
},
|
|
435
|
-
async onChange(ctx, target, change) {
|
|
651
|
+
async onChange(ctx, target, change, deferFollowUp = false) {
|
|
436
652
|
for (const [key, value] of Object.entries({ ...change.added, ...change.modified })) {
|
|
437
653
|
await ctx.run(opts.setCmd, target, key, String(value));
|
|
438
654
|
}
|
|
439
|
-
if (opts.afterChange) {
|
|
655
|
+
if (opts.afterChange && !deferFollowUp) {
|
|
440
656
|
for (const cmd of opts.afterChange) {
|
|
441
|
-
|
|
657
|
+
try {
|
|
658
|
+
await ctx.run(cmd, target);
|
|
659
|
+
} catch (err) {
|
|
660
|
+
if (await isAppDeployed(ctx, target)) throw err;
|
|
661
|
+
logWarn(target, `${cmd} failed on an undeployed app \u2014 its proxy config will be built on first deploy`);
|
|
662
|
+
}
|
|
442
663
|
}
|
|
443
664
|
}
|
|
444
665
|
}
|
|
@@ -448,20 +669,28 @@ var Nginx = propertyResource({
|
|
|
448
669
|
key: "nginx",
|
|
449
670
|
namespace: "nginx",
|
|
450
671
|
setCmd: "nginx:set",
|
|
672
|
+
settable: NGINX_KEYS,
|
|
451
673
|
afterChange: ["proxy:build-config"]
|
|
452
674
|
});
|
|
453
675
|
var Logs = propertyResource({
|
|
454
676
|
key: "logs",
|
|
455
677
|
namespace: "logs",
|
|
456
|
-
setCmd: "logs:set"
|
|
678
|
+
setCmd: "logs:set",
|
|
679
|
+
settable: LOGS_KEYS
|
|
457
680
|
});
|
|
458
681
|
var Registry = propertyResource({
|
|
459
682
|
key: "registry",
|
|
460
683
|
namespace: "registry",
|
|
461
|
-
setCmd: "registry:set"
|
|
684
|
+
setCmd: "registry:set",
|
|
685
|
+
settable: REGISTRY_KEYS
|
|
462
686
|
});
|
|
463
687
|
var Scheduler = {
|
|
464
688
|
key: "scheduler",
|
|
689
|
+
// A scheduler selected on the app but not declared in the file.
|
|
690
|
+
removals: (current, desired) => desired === void 0 && current ? ["scheduler"] : [],
|
|
691
|
+
prune: async (ctx, target) => {
|
|
692
|
+
await ctx.run("scheduler:set", target, "selected");
|
|
693
|
+
},
|
|
465
694
|
async read(ctx, target) {
|
|
466
695
|
const raw = await ctx.query("scheduler:report", target);
|
|
467
696
|
const report = parseReport(raw, "scheduler");
|
|
@@ -502,21 +731,38 @@ var Proxy = {
|
|
|
502
731
|
}
|
|
503
732
|
};
|
|
504
733
|
|
|
734
|
+
// src/core/config-json.ts
|
|
735
|
+
function parseConfigJson(raw, what) {
|
|
736
|
+
let parsed;
|
|
737
|
+
try {
|
|
738
|
+
parsed = JSON.parse(raw);
|
|
739
|
+
} catch {
|
|
740
|
+
throw new Error(`${what}: config:export did not return JSON`);
|
|
741
|
+
}
|
|
742
|
+
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
743
|
+
throw new Error(`${what}: config:export did not return a JSON object`);
|
|
744
|
+
}
|
|
745
|
+
const result = {};
|
|
746
|
+
for (const [k, v] of Object.entries(parsed)) result[k] = String(v);
|
|
747
|
+
return result;
|
|
748
|
+
}
|
|
749
|
+
|
|
505
750
|
// src/resources/config.ts
|
|
506
751
|
var MANAGED_KEYS_VAR = "DOKKU_COMPOSE_MANAGED_KEYS";
|
|
752
|
+
var MISSING = "\0missing";
|
|
507
753
|
var Config = {
|
|
508
754
|
key: "env",
|
|
755
|
+
removals: (current, desired) => Object.keys(current ?? {}).filter((k) => !(desired && typeof desired === "object" && k in desired)),
|
|
509
756
|
read: async (ctx, target) => {
|
|
510
757
|
const managedRaw = await ctx.query("config:get", target, MANAGED_KEYS_VAR);
|
|
511
758
|
const managedKeys = managedRaw.trim() ? managedRaw.trim().split(",").filter(Boolean) : [];
|
|
512
759
|
const result = {};
|
|
760
|
+
for (const k of managedKeys) result[k] = MISSING;
|
|
513
761
|
if (managedKeys.length > 0) {
|
|
514
|
-
const raw = await ctx.
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
if (
|
|
518
|
-
result[match[1]] = match[2];
|
|
519
|
-
}
|
|
762
|
+
const raw = await ctx.queryStrict("config:export", "--format", "json", target);
|
|
763
|
+
const current = parseConfigJson(raw, target);
|
|
764
|
+
for (const k of managedKeys) {
|
|
765
|
+
if (k in current) result[k] = current[k];
|
|
520
766
|
}
|
|
521
767
|
}
|
|
522
768
|
return result;
|
|
@@ -526,9 +772,12 @@ var Config = {
|
|
|
526
772
|
if (removed.length > 0) {
|
|
527
773
|
await ctx.run("config:unset", "--no-restart", target, ...removed);
|
|
528
774
|
}
|
|
529
|
-
const toSet = { ...added, ...modified };
|
|
530
775
|
const allDesiredKeys = Object.keys(change.after);
|
|
531
|
-
|
|
776
|
+
if (allDesiredKeys.length === 0) {
|
|
777
|
+
await ctx.run("config:unset", "--no-restart", target, MANAGED_KEYS_VAR);
|
|
778
|
+
return;
|
|
779
|
+
}
|
|
780
|
+
const toSet = { ...added, ...modified };
|
|
532
781
|
if (Object.keys(toSet).length > 0 || removed.length > 0) {
|
|
533
782
|
const pairs = Object.entries(change.after).map(([k, v]) => `${k}=${v}`);
|
|
534
783
|
await ctx.run(
|
|
@@ -536,7 +785,7 @@ var Config = {
|
|
|
536
785
|
"--no-restart",
|
|
537
786
|
target,
|
|
538
787
|
...pairs,
|
|
539
|
-
`${MANAGED_KEYS_VAR}=${
|
|
788
|
+
`${MANAGED_KEYS_VAR}=${allDesiredKeys.join(",")}`
|
|
540
789
|
);
|
|
541
790
|
}
|
|
542
791
|
}
|
|
@@ -546,15 +795,17 @@ var Config = {
|
|
|
546
795
|
var Certs = {
|
|
547
796
|
key: "ssl",
|
|
548
797
|
read: async (ctx, target) => {
|
|
549
|
-
const raw = await ctx.query("certs:report", target, "--ssl-enabled");
|
|
550
|
-
|
|
798
|
+
const raw = (await ctx.query("certs:report", target, "--ssl-enabled")).trim();
|
|
799
|
+
if (raw === "") return void 0;
|
|
800
|
+
return raw === "true";
|
|
551
801
|
},
|
|
552
802
|
readAll: async (ctx) => {
|
|
553
803
|
const raw = await ctx.query("certs:report");
|
|
554
804
|
const bulk = parseBulkReport(raw, "ssl");
|
|
555
805
|
const result = /* @__PURE__ */ new Map();
|
|
556
806
|
for (const [app, report] of bulk) {
|
|
557
|
-
|
|
807
|
+
const enabled = report["enabled"];
|
|
808
|
+
result.set(app, enabled === void 0 ? void 0 : enabled === "true");
|
|
558
809
|
}
|
|
559
810
|
return result;
|
|
560
811
|
},
|
|
@@ -569,10 +820,17 @@ var Certs = {
|
|
|
569
820
|
};
|
|
570
821
|
|
|
571
822
|
// src/resources/builder.ts
|
|
823
|
+
function parseBuildOptions(dockerOptsBuild) {
|
|
824
|
+
const out = [];
|
|
825
|
+
for (const raw of dockerOptsBuild.trim().split(/\s+(?=--\S)/)) {
|
|
826
|
+
const m = raw.match(/^--build-arg\s+([^=\s]+)=?(.*)$/s);
|
|
827
|
+
if (m) out.push({ key: m[1], value: m[2], raw });
|
|
828
|
+
}
|
|
829
|
+
return out;
|
|
830
|
+
}
|
|
572
831
|
function parseBuildArgs(dockerOptsBuild) {
|
|
573
832
|
const args = {};
|
|
574
|
-
const
|
|
575
|
-
for (const m of matches) args[m[1]] = m[2];
|
|
833
|
+
for (const o of parseBuildOptions(dockerOptsBuild)) args[o.key] = o.value;
|
|
576
834
|
return args;
|
|
577
835
|
}
|
|
578
836
|
function buildConfigFromReports(builderReport, dockerfileReport, appJsonReport, dockerOptsBuild) {
|
|
@@ -589,6 +847,19 @@ function buildConfigFromReports(builderReport, dockerfileReport, appJsonReport,
|
|
|
589
847
|
}
|
|
590
848
|
var Builder = {
|
|
591
849
|
key: "build",
|
|
850
|
+
additive: true,
|
|
851
|
+
// Build args (`--build-arg KEY=...` build docker options) whose KEY is not in `build.args`.
|
|
852
|
+
removals: (current, desired) => Object.keys(current?.args ?? {}).filter((k) => !(desired?.args && k in desired.args)).map((k) => `args.${k}`),
|
|
853
|
+
// Remove exactly the stale `--build-arg` options; other build options stay.
|
|
854
|
+
prune: async (ctx, target, removed) => {
|
|
855
|
+
const stale = new Set(removed.map((l) => l.slice("args.".length)));
|
|
856
|
+
const report = parseReport(await ctx.query("docker-options:report", target), "docker-options");
|
|
857
|
+
const options = parseBuildOptions(report["build"] ?? "").filter((o) => stale.has(o.key));
|
|
858
|
+
for (const o of options) await ctx.run("docker-options:remove", target, "build", o.raw);
|
|
859
|
+
for (const k of stale) {
|
|
860
|
+
if (!options.some((o) => o.key === k)) logWarn(target, `build arg '${k}' not found as an exact docker option; left as is`);
|
|
861
|
+
}
|
|
862
|
+
},
|
|
592
863
|
async read(ctx, target) {
|
|
593
864
|
const [builderRaw, dockerfileRaw, appJsonRaw, dockerOptsRaw] = await Promise.all([
|
|
594
865
|
ctx.query("builder:report", target),
|
|
@@ -660,6 +931,11 @@ function dockerOptsFromReport(report) {
|
|
|
660
931
|
}
|
|
661
932
|
var DockerOptions = {
|
|
662
933
|
key: "docker_options",
|
|
934
|
+
// Pruned by `up` within a declared block only, so a missing block is not shown.
|
|
935
|
+
removals: (current, desired) => !desired ? [] : PHASES.flatMap((phase) => {
|
|
936
|
+
const wanted = new Set(desired?.[phase] ?? []);
|
|
937
|
+
return (current?.[phase] ?? []).filter((o) => !wanted.has(o)).map((o) => `${phase}: ${o}`);
|
|
938
|
+
}),
|
|
663
939
|
async read(ctx, target) {
|
|
664
940
|
const raw = await ctx.query("docker-options:report", target);
|
|
665
941
|
return dockerOptsFromReport(parseReport(raw, "docker-options"));
|
|
@@ -687,6 +963,68 @@ var DockerOptions = {
|
|
|
687
963
|
}
|
|
688
964
|
};
|
|
689
965
|
|
|
966
|
+
// src/resources/ps.ts
|
|
967
|
+
var PS_KEYS = ["procfile-path", "restart-policy", "stop-timeout-seconds"];
|
|
968
|
+
function parseScale(raw) {
|
|
969
|
+
const result = {};
|
|
970
|
+
let parsed;
|
|
971
|
+
try {
|
|
972
|
+
parsed = JSON.parse(raw);
|
|
973
|
+
} catch {
|
|
974
|
+
return result;
|
|
975
|
+
}
|
|
976
|
+
if (!Array.isArray(parsed)) return result;
|
|
977
|
+
for (const e of parsed) {
|
|
978
|
+
if (e && typeof e.process_type === "string" && e.quantity !== void 0) {
|
|
979
|
+
result[e.process_type] = String(e.quantity);
|
|
980
|
+
}
|
|
981
|
+
}
|
|
982
|
+
return result;
|
|
983
|
+
}
|
|
984
|
+
function parsePsReport(raw) {
|
|
985
|
+
const result = {};
|
|
986
|
+
let parsed;
|
|
987
|
+
try {
|
|
988
|
+
parsed = JSON.parse(raw);
|
|
989
|
+
} catch {
|
|
990
|
+
return result;
|
|
991
|
+
}
|
|
992
|
+
if (!parsed || typeof parsed !== "object") return result;
|
|
993
|
+
for (const key of PS_KEYS) {
|
|
994
|
+
const v = parsed[key];
|
|
995
|
+
if (v !== void 0 && v !== null && String(v) !== "") result[key] = String(v);
|
|
996
|
+
}
|
|
997
|
+
return result;
|
|
998
|
+
}
|
|
999
|
+
var Scale = {
|
|
1000
|
+
key: "scale",
|
|
1001
|
+
additive: true,
|
|
1002
|
+
async read(ctx, target) {
|
|
1003
|
+
return parseScale(await ctx.query("ps:scale", "--format", "json", target));
|
|
1004
|
+
},
|
|
1005
|
+
async onChange(ctx, target, change) {
|
|
1006
|
+
const pairs = Object.entries({ ...change.added, ...change.modified }).map(([proc, count]) => `${proc}=${count}`);
|
|
1007
|
+
if (pairs.length === 0) return;
|
|
1008
|
+
await ctx.run("ps:scale", "--skip-deploy", target, ...pairs);
|
|
1009
|
+
}
|
|
1010
|
+
};
|
|
1011
|
+
var Ps = {
|
|
1012
|
+
key: "ps",
|
|
1013
|
+
additive: true,
|
|
1014
|
+
removals: (current, desired) => PS_KEYS.filter((k) => current?.[k] !== void 0 && !(desired && k in desired)),
|
|
1015
|
+
prune: async (ctx, target, removed) => {
|
|
1016
|
+
for (const key of removed) await ctx.run("ps:set", target, key);
|
|
1017
|
+
},
|
|
1018
|
+
async read(ctx, target) {
|
|
1019
|
+
return parsePsReport(await ctx.queryStrict("ps:report", target, "--format", "json"));
|
|
1020
|
+
},
|
|
1021
|
+
async onChange(ctx, target, change) {
|
|
1022
|
+
for (const [key, value] of Object.entries({ ...change.added, ...change.modified })) {
|
|
1023
|
+
await ctx.run("ps:set", target, key, String(value));
|
|
1024
|
+
}
|
|
1025
|
+
}
|
|
1026
|
+
};
|
|
1027
|
+
|
|
690
1028
|
// src/resources/git.ts
|
|
691
1029
|
var Git = {
|
|
692
1030
|
key: "git",
|
|
@@ -714,7 +1052,33 @@ var Git = {
|
|
|
714
1052
|
var Checks = {
|
|
715
1053
|
key: "checks",
|
|
716
1054
|
forceApply: true,
|
|
717
|
-
|
|
1055
|
+
// Applying never reads (forceApply); the read only finds settings to report as removed.
|
|
1056
|
+
// Settable state: the per-process lists and `wait-to-retire`. A list of "none" is empty.
|
|
1057
|
+
read: async (ctx, target) => {
|
|
1058
|
+
const report = parseReport(await ctx.query("checks:report", target), "checks");
|
|
1059
|
+
const current = {};
|
|
1060
|
+
for (const [reportKey, key] of [["disabled-list", "disabled"], ["skipped-list", "skipped"]]) {
|
|
1061
|
+
const v = report[reportKey];
|
|
1062
|
+
if (v && v !== "none" && v !== "_all_") current[key] = v;
|
|
1063
|
+
}
|
|
1064
|
+
if (report["wait-to-retire"]) current["wait-to-retire"] = report["wait-to-retire"];
|
|
1065
|
+
return current;
|
|
1066
|
+
},
|
|
1067
|
+
removals: (current, desired) => desired === false ? [] : Object.keys(current || {}).filter((k) => !(desired && k in desired)),
|
|
1068
|
+
prune: async (ctx, target, removed, current, desired) => {
|
|
1069
|
+
const cur = current;
|
|
1070
|
+
const split = (v) => (v ?? "").split(",").map((t) => t.trim()).filter(Boolean);
|
|
1071
|
+
const declared = new Set(desired && desired !== false ? [...desired.disabled ?? [], ...desired.skipped ?? []] : []);
|
|
1072
|
+
const stale = new Set(removed.filter((k) => k === "disabled" || k === "skipped").flatMap((k) => split(cur[k])));
|
|
1073
|
+
const enable = [...stale].filter((t) => !declared.has(t));
|
|
1074
|
+
for (const t of stale) {
|
|
1075
|
+
if (declared.has(t)) {
|
|
1076
|
+
logWarn(target, `checks: '${t}' is declared in the file but also set in a list the file does not declare; left as is. Run 'dokku checks:enable ${target} ${t}' and then 'up' to clear it`);
|
|
1077
|
+
}
|
|
1078
|
+
}
|
|
1079
|
+
if (enable.length > 0) await ctx.run("checks:enable", target, ...enable);
|
|
1080
|
+
if (removed.includes("wait-to-retire")) await ctx.run("checks:set", target, "wait-to-retire");
|
|
1081
|
+
},
|
|
718
1082
|
onChange: async (ctx, target, { after }) => {
|
|
719
1083
|
if (after === false) {
|
|
720
1084
|
await ctx.run("checks:disable", target);
|
|
@@ -755,6 +1119,12 @@ var Networks = {
|
|
|
755
1119
|
};
|
|
756
1120
|
var NetworkProps = {
|
|
757
1121
|
key: "network",
|
|
1122
|
+
additive: true,
|
|
1123
|
+
removals: (current, desired) => Object.keys(current ?? {}).filter((k) => !(desired && k in desired)),
|
|
1124
|
+
// Dokku's property names are hyphenated; the schema's are underscored.
|
|
1125
|
+
prune: async (ctx, target, removed) => {
|
|
1126
|
+
for (const key of removed) await ctx.run("network:set", target, key.replaceAll("_", "-"));
|
|
1127
|
+
},
|
|
758
1128
|
read: async (ctx, target) => {
|
|
759
1129
|
const raw = await ctx.query("network:report", target);
|
|
760
1130
|
const report = parseReport(raw, "network");
|
|
@@ -801,19 +1171,90 @@ var NetworkProps = {
|
|
|
801
1171
|
};
|
|
802
1172
|
|
|
803
1173
|
// src/modules/plugins.ts
|
|
1174
|
+
function parsePluginList(raw) {
|
|
1175
|
+
const result = /* @__PURE__ */ new Map();
|
|
1176
|
+
let parsed;
|
|
1177
|
+
try {
|
|
1178
|
+
parsed = JSON.parse(raw);
|
|
1179
|
+
} catch {
|
|
1180
|
+
throw new Error(`plugin:list --format json did not return JSON (does this Dokku support --format json?): ${raw.slice(0, 200)}`);
|
|
1181
|
+
}
|
|
1182
|
+
if (!Array.isArray(parsed)) {
|
|
1183
|
+
throw new Error(`plugin:list --format json did not return a JSON array: ${raw.slice(0, 200)}`);
|
|
1184
|
+
}
|
|
1185
|
+
for (const p of parsed) {
|
|
1186
|
+
if (p && typeof p.name === "string") {
|
|
1187
|
+
result.set(p.name, {
|
|
1188
|
+
version: typeof p.version === "string" ? p.version : "",
|
|
1189
|
+
committish: typeof p.committish === "string" ? p.committish : ""
|
|
1190
|
+
});
|
|
1191
|
+
}
|
|
1192
|
+
}
|
|
1193
|
+
return result;
|
|
1194
|
+
}
|
|
1195
|
+
var SEMVER = /^(\d+)\.(\d+)\.(\d+)$/;
|
|
1196
|
+
function compareVersions(a, b) {
|
|
1197
|
+
const na = a.replace(/^v/, "");
|
|
1198
|
+
const nb = b.replace(/^v/, "");
|
|
1199
|
+
const ma = na.match(SEMVER);
|
|
1200
|
+
const mb = nb.match(SEMVER);
|
|
1201
|
+
if (!ma || !mb) return na === nb ? 0 : void 0;
|
|
1202
|
+
for (let i = 1; i <= 3; i++) {
|
|
1203
|
+
const d = Number(ma[i]) - Number(mb[i]);
|
|
1204
|
+
if (d !== 0) return d < 0 ? -1 : 1;
|
|
1205
|
+
}
|
|
1206
|
+
return 0;
|
|
1207
|
+
}
|
|
1208
|
+
var SHA = /^[0-9a-f]{7,40}$/i;
|
|
1209
|
+
function planPlugins(listOutput, plugins) {
|
|
1210
|
+
const installed = parsePluginList(listOutput);
|
|
1211
|
+
return Object.entries(plugins).map(([name, config]) => {
|
|
1212
|
+
const current = installed.get(name);
|
|
1213
|
+
const desired = config.version;
|
|
1214
|
+
if (current === void 0) return { name, action: "install", desired };
|
|
1215
|
+
const installedVersion = current.version || current.committish.slice(0, 7) || void 0;
|
|
1216
|
+
if (desired === void 0) return { name, action: "ok", installed: installedVersion };
|
|
1217
|
+
if (SHA.test(desired) && current.committish) {
|
|
1218
|
+
if (current.committish.toLowerCase().startsWith(desired.toLowerCase())) {
|
|
1219
|
+
return { name, action: "ok", installed: installedVersion, desired };
|
|
1220
|
+
}
|
|
1221
|
+
}
|
|
1222
|
+
if (!current.version) return { name, action: "unknown", installed: installedVersion, desired };
|
|
1223
|
+
const cmp = compareVersions(current.version, desired);
|
|
1224
|
+
const action = cmp === void 0 ? "unknown" : cmp < 0 ? "update" : cmp > 0 ? "newer" : "ok";
|
|
1225
|
+
return { name, action, installed: installedVersion, desired };
|
|
1226
|
+
});
|
|
1227
|
+
}
|
|
804
1228
|
async function ensurePlugins(ctx, plugins) {
|
|
805
|
-
const
|
|
806
|
-
const
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
1229
|
+
const plans = planPlugins(await ctx.queryStrict("plugin:list", "--format", "json"), plugins);
|
|
1230
|
+
for (const plan of plans) {
|
|
1231
|
+
const config = plugins[plan.name];
|
|
1232
|
+
switch (plan.action) {
|
|
1233
|
+
case "install":
|
|
1234
|
+
logAction("plugins", `Installing ${plan.name}`);
|
|
1235
|
+
await ctx.run(
|
|
1236
|
+
"plugin:install",
|
|
1237
|
+
config.url,
|
|
1238
|
+
...config.version ? ["--committish", config.version] : [],
|
|
1239
|
+
"--name",
|
|
1240
|
+
plan.name
|
|
1241
|
+
);
|
|
1242
|
+
logDone();
|
|
1243
|
+
break;
|
|
1244
|
+
case "update":
|
|
1245
|
+
logAction("plugins", `Updating ${plan.name}`);
|
|
1246
|
+
await ctx.run("plugin:update", plan.name, config.version);
|
|
1247
|
+
logDone();
|
|
1248
|
+
break;
|
|
1249
|
+
case "newer":
|
|
1250
|
+
logWarn("plugins", `${plan.name} is ${plan.installed}, newer than the declared ${plan.desired}; leaving it (never downgrades)`);
|
|
1251
|
+
break;
|
|
1252
|
+
case "unknown":
|
|
1253
|
+
logWarn("plugins", `${plan.name} is ${plan.installed} and the declared ${plan.desired} cannot be compared; leaving it`);
|
|
1254
|
+
break;
|
|
1255
|
+
default:
|
|
1256
|
+
break;
|
|
814
1257
|
}
|
|
815
|
-
await ctx.run("plugin:install", config.url, "--name", name);
|
|
816
|
-
logDone();
|
|
817
1258
|
}
|
|
818
1259
|
}
|
|
819
1260
|
|
|
@@ -1133,7 +1574,64 @@ async function ensureGlobalNginx(ctx, nginx) {
|
|
|
1133
1574
|
}
|
|
1134
1575
|
|
|
1135
1576
|
// src/commands/up.ts
|
|
1136
|
-
|
|
1577
|
+
var UpFailedError = class extends Error {
|
|
1578
|
+
constructor(failures) {
|
|
1579
|
+
super(
|
|
1580
|
+
`up failed for ${failures.length} app(s): ${failures.map((f) => f.app).join(", ")}`
|
|
1581
|
+
);
|
|
1582
|
+
this.failures = failures;
|
|
1583
|
+
this.name = "UpFailedError";
|
|
1584
|
+
}
|
|
1585
|
+
};
|
|
1586
|
+
var AppStepsError = class extends AggregateError {
|
|
1587
|
+
constructor(errors) {
|
|
1588
|
+
super(errors, errors.map((e) => e instanceof Error ? e.message : String(e)).join("; "));
|
|
1589
|
+
this.name = "AppStepsError";
|
|
1590
|
+
}
|
|
1591
|
+
};
|
|
1592
|
+
async function processApp(ctx, config, app, appConfig, failFast = false, prune = false) {
|
|
1593
|
+
const errors = [];
|
|
1594
|
+
let leftOnServer = 0;
|
|
1595
|
+
const count = prune ? void 0 : (removed) => {
|
|
1596
|
+
leftOnServer += removed.length;
|
|
1597
|
+
};
|
|
1598
|
+
const step = async (fn) => {
|
|
1599
|
+
try {
|
|
1600
|
+
await fn();
|
|
1601
|
+
} catch (error) {
|
|
1602
|
+
if (failFast) throw error;
|
|
1603
|
+
logError(app, error instanceof Error ? error.message : String(error));
|
|
1604
|
+
errors.push(error);
|
|
1605
|
+
}
|
|
1606
|
+
};
|
|
1607
|
+
await reconcile(Apps, ctx, app, true);
|
|
1608
|
+
await step(() => reconcile(Domains, ctx, app, appConfig.domains));
|
|
1609
|
+
await step(() => reconcile(Networks, ctx, app, appConfig.networks));
|
|
1610
|
+
await step(() => reconcile(NetworkProps, ctx, app, appConfig.network, count, prune));
|
|
1611
|
+
await step(() => reconcile(Proxy, ctx, app, appConfig.proxy?.enabled));
|
|
1612
|
+
await step(() => reconcile(Ports, ctx, app, appConfig.ports));
|
|
1613
|
+
if (config.postgres || config.redis) {
|
|
1614
|
+
await step(() => ensureAppLinks(ctx, app, appConfig.links ?? [], config));
|
|
1615
|
+
}
|
|
1616
|
+
await step(() => reconcile(Certs, ctx, app, appConfig.ssl));
|
|
1617
|
+
await step(() => reconcile(Storage, ctx, app, appConfig.storage));
|
|
1618
|
+
await step(() => reconcile(Nginx, ctx, app, appConfig.nginx, count, prune));
|
|
1619
|
+
await step(() => reconcile(Checks, ctx, app, appConfig.checks, void 0, prune));
|
|
1620
|
+
await step(() => reconcile(Logs, ctx, app, appConfig.logs, count, prune));
|
|
1621
|
+
await step(() => reconcile(Registry, ctx, app, appConfig.registry, count, prune));
|
|
1622
|
+
await step(() => reconcile(Scheduler, ctx, app, appConfig.scheduler, count, prune));
|
|
1623
|
+
await step(() => reconcile(Ps, ctx, app, appConfig.ps, count, prune));
|
|
1624
|
+
await step(() => reconcile(Scale, ctx, app, appConfig.scale));
|
|
1625
|
+
await step(() => reconcile(Config, ctx, app, appConfig.env || {}));
|
|
1626
|
+
await step(() => reconcile(Builder, ctx, app, appConfig.build, count, prune));
|
|
1627
|
+
await step(() => reconcile(Git, ctx, app, appConfig.git ?? config.git));
|
|
1628
|
+
await step(() => reconcile(DockerOptions, ctx, app, appConfig.docker_options));
|
|
1629
|
+
if (leftOnServer > 0) {
|
|
1630
|
+
logWarn(app, `${leftOnServer} setting(s) no longer in the compose file were left on the server; run 'dokku-compose diff' to see them`);
|
|
1631
|
+
}
|
|
1632
|
+
if (errors.length > 0) throw new AppStepsError(errors);
|
|
1633
|
+
}
|
|
1634
|
+
async function runUp(ctx, config, appFilter, opts = {}) {
|
|
1137
1635
|
const apps = appFilter.length > 0 ? appFilter : Object.keys(config.apps);
|
|
1138
1636
|
await ensureDokkuVersion(ctx, config.dokku?.version);
|
|
1139
1637
|
if (config.plugins) await ensurePlugins(ctx, config.plugins);
|
|
@@ -1146,29 +1644,22 @@ async function runUp(ctx, config, appFilter) {
|
|
|
1146
1644
|
if (config.postgres) await ensurePostgres(ctx, config.postgres);
|
|
1147
1645
|
if (config.redis) await ensureRedis(ctx, config.redis);
|
|
1148
1646
|
if (config.postgres) await ensurePostgresBackups(ctx, config.postgres);
|
|
1647
|
+
const failures = [];
|
|
1149
1648
|
for (const app of apps) {
|
|
1150
1649
|
const appConfig = config.apps[app];
|
|
1151
1650
|
if (!appConfig) continue;
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1651
|
+
try {
|
|
1652
|
+
await processApp(ctx, config, app, appConfig, opts.failFast, opts.prune);
|
|
1653
|
+
} catch (error) {
|
|
1654
|
+
if (opts.failFast) throw error;
|
|
1655
|
+
if (!(error instanceof AppStepsError)) {
|
|
1656
|
+
logError(app, error instanceof Error ? error.message : String(error));
|
|
1657
|
+
}
|
|
1658
|
+
failures.push({ app, error });
|
|
1160
1659
|
}
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
await reconcile(Checks, ctx, app, appConfig.checks);
|
|
1165
|
-
await reconcile(Logs, ctx, app, appConfig.logs);
|
|
1166
|
-
await reconcile(Registry, ctx, app, appConfig.registry);
|
|
1167
|
-
await reconcile(Scheduler, ctx, app, appConfig.scheduler);
|
|
1168
|
-
await reconcile(Config, ctx, app, appConfig.env);
|
|
1169
|
-
await reconcile(Builder, ctx, app, appConfig.build);
|
|
1170
|
-
await reconcile(Git, ctx, app, appConfig.git ?? config.git);
|
|
1171
|
-
await reconcile(DockerOptions, ctx, app, appConfig.docker_options);
|
|
1660
|
+
}
|
|
1661
|
+
if (failures.length > 0) {
|
|
1662
|
+
throw new UpFailedError(failures);
|
|
1172
1663
|
}
|
|
1173
1664
|
}
|
|
1174
1665
|
|
|
@@ -1191,15 +1682,35 @@ async function exportApps(ctx) {
|
|
|
1191
1682
|
}
|
|
1192
1683
|
|
|
1193
1684
|
// src/commands/down.ts
|
|
1685
|
+
var DownFailedError = class extends Error {
|
|
1686
|
+
constructor(failures) {
|
|
1687
|
+
super(
|
|
1688
|
+
`down failed for ${failures.length} app(s): ${failures.map((f) => f.app).join(", ")} \u2014 service and network teardown was skipped because app teardown failed`
|
|
1689
|
+
);
|
|
1690
|
+
this.failures = failures;
|
|
1691
|
+
this.name = "DownFailedError";
|
|
1692
|
+
}
|
|
1693
|
+
};
|
|
1194
1694
|
async function runDown(ctx, config, appFilter, opts) {
|
|
1195
1695
|
const apps = appFilter.length > 0 ? appFilter : Object.keys(config.apps);
|
|
1696
|
+
const failures = [];
|
|
1196
1697
|
for (const app of apps) {
|
|
1197
1698
|
const appConfig = config.apps[app];
|
|
1198
1699
|
if (!appConfig) continue;
|
|
1199
|
-
|
|
1200
|
-
|
|
1700
|
+
try {
|
|
1701
|
+
if (appConfig.links && (config.postgres || config.redis)) {
|
|
1702
|
+
await destroyAppLinks(ctx, app, appConfig.links, config);
|
|
1703
|
+
}
|
|
1704
|
+
await destroyApp(ctx, app);
|
|
1705
|
+
} catch (error) {
|
|
1706
|
+
if (opts.failFast) throw error;
|
|
1707
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1708
|
+
logError(app, message);
|
|
1709
|
+
failures.push({ app, error });
|
|
1201
1710
|
}
|
|
1202
|
-
|
|
1711
|
+
}
|
|
1712
|
+
if (failures.length > 0) {
|
|
1713
|
+
throw new DownFailedError(failures);
|
|
1203
1714
|
}
|
|
1204
1715
|
if (config.postgres) await destroyPostgres(ctx, config.postgres);
|
|
1205
1716
|
if (config.redis) await destroyRedis(ctx, config.redis);
|
|
@@ -1233,6 +1744,8 @@ var CONFIG_RESOURCES = [
|
|
|
1233
1744
|
Logs,
|
|
1234
1745
|
Registry,
|
|
1235
1746
|
Scheduler,
|
|
1747
|
+
Ps,
|
|
1748
|
+
Scale,
|
|
1236
1749
|
Config
|
|
1237
1750
|
];
|
|
1238
1751
|
var BUILD_RESOURCES = [
|
|
@@ -1289,80 +1802,158 @@ async function runExport(ctx, opts) {
|
|
|
1289
1802
|
return config;
|
|
1290
1803
|
}
|
|
1291
1804
|
|
|
1292
|
-
// src/
|
|
1293
|
-
|
|
1294
|
-
function
|
|
1295
|
-
if (
|
|
1296
|
-
|
|
1805
|
+
// src/commands/diff.ts
|
|
1806
|
+
import chalk2 from "chalk";
|
|
1807
|
+
function diffSslStatus(desired, current) {
|
|
1808
|
+
if (typeof desired !== "object" || desired === null) return void 0;
|
|
1809
|
+
if (current === void 0) return void 0;
|
|
1810
|
+
if (current === true) return "in-sync";
|
|
1811
|
+
if (current === false || current === null) return "missing";
|
|
1812
|
+
return void 0;
|
|
1297
1813
|
}
|
|
1298
|
-
function
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
/^(registry:login\s+(?:--global\s+)?\S+\s+\S+\s+)(\S+)/,
|
|
1304
|
-
(_, prefix, password) => `${prefix}${maskValue(password)}`
|
|
1305
|
-
);
|
|
1306
|
-
return masked;
|
|
1814
|
+
function hasAdditionsOrChanges(change) {
|
|
1815
|
+
const c = change;
|
|
1816
|
+
if (c.added === void 0) return c.changed;
|
|
1817
|
+
const added = Array.isArray(c.added) ? c.added.length : Object.keys(c.added).length;
|
|
1818
|
+
return added > 0 || Object.keys(c.modified ?? {}).length > 0;
|
|
1307
1819
|
}
|
|
1308
|
-
function
|
|
1309
|
-
|
|
1310
|
-
if (typeof data === "string") return data;
|
|
1311
|
-
if (Array.isArray(data)) return data.map(maskSensitiveData);
|
|
1312
|
-
if (typeof data === "object") {
|
|
1313
|
-
const result = {};
|
|
1314
|
-
for (const [key, value] of Object.entries(data)) {
|
|
1315
|
-
if (typeof value === "string" && SENSITIVE_RE.test(key)) {
|
|
1316
|
-
result[key] = maskValue(value);
|
|
1317
|
-
} else if (typeof value === "object" && value !== null) {
|
|
1318
|
-
result[key] = maskSensitiveData(value);
|
|
1319
|
-
} else {
|
|
1320
|
-
result[key] = value;
|
|
1321
|
-
}
|
|
1322
|
-
}
|
|
1323
|
-
return result;
|
|
1324
|
-
}
|
|
1325
|
-
return data;
|
|
1820
|
+
function isEmptyValue(value) {
|
|
1821
|
+
return value === null || value === void 0 || Array.isArray(value) && value.length === 0 || typeof value === "object" && Object.keys(value).length === 0;
|
|
1326
1822
|
}
|
|
1327
|
-
|
|
1328
|
-
// src/commands/diff.ts
|
|
1329
|
-
import chalk2 from "chalk";
|
|
1330
1823
|
async function computeDiff(ctx, config) {
|
|
1331
|
-
const result = { apps: {}, services: {}, inSync: true };
|
|
1824
|
+
const result = { apps: {}, services: {}, plugins: {}, inSync: true, readWarnings: [] };
|
|
1825
|
+
const appNames = Object.keys(config.apps);
|
|
1332
1826
|
await ensureDokkuVersion(ctx, config.dokku?.version);
|
|
1827
|
+
const appExists = /* @__PURE__ */ new Map();
|
|
1828
|
+
await Promise.all(
|
|
1829
|
+
appNames.map(async (app) => {
|
|
1830
|
+
appExists.set(app, await ctx.check("apps:exists", app));
|
|
1831
|
+
})
|
|
1832
|
+
);
|
|
1833
|
+
const existingAppNames = appNames.filter((app) => appExists.get(app));
|
|
1333
1834
|
const prefetched = /* @__PURE__ */ new Map();
|
|
1334
1835
|
await Promise.all(
|
|
1335
1836
|
ALL_APP_RESOURCES.filter((r) => !r.forceApply && !r.key.startsWith("_") && r.readAll).map(async (r) => {
|
|
1336
|
-
|
|
1837
|
+
try {
|
|
1838
|
+
const bulk = await r.readAll(ctx);
|
|
1839
|
+
const unreadable = existingAppNames.length > 0 && bulk.size === 0;
|
|
1840
|
+
prefetched.set(r.key, unreadable ? null : bulk);
|
|
1841
|
+
} catch {
|
|
1842
|
+
prefetched.set(r.key, null);
|
|
1843
|
+
}
|
|
1337
1844
|
})
|
|
1338
1845
|
);
|
|
1339
1846
|
for (const [app, appConfig] of Object.entries(config.apps)) {
|
|
1340
1847
|
const appDiff = {};
|
|
1341
1848
|
for (const resource of ALL_APP_RESOURCES) {
|
|
1342
1849
|
if (resource.key.startsWith("_")) continue;
|
|
1343
|
-
if (resource.forceApply) continue;
|
|
1344
1850
|
let desired;
|
|
1345
1851
|
if (resource.key === "proxy") {
|
|
1346
1852
|
desired = appConfig.proxy?.enabled;
|
|
1347
1853
|
} else {
|
|
1348
1854
|
desired = appConfig[resource.key];
|
|
1349
1855
|
}
|
|
1350
|
-
|
|
1856
|
+
const checkRemovalsOnly = desired === void 0;
|
|
1857
|
+
if (checkRemovalsOnly && (!resource.removals || !appExists.get(app))) continue;
|
|
1858
|
+
if (resource.forceApply && !resource.removals) continue;
|
|
1859
|
+
if (!appExists.get(app)) {
|
|
1860
|
+
appDiff[resource.key] = { status: "missing", desired, current: void 0 };
|
|
1861
|
+
result.inSync = false;
|
|
1862
|
+
continue;
|
|
1863
|
+
}
|
|
1351
1864
|
const bulk = prefetched.get(resource.key);
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
if (
|
|
1865
|
+
let current;
|
|
1866
|
+
let bulkUnreadable = false;
|
|
1867
|
+
if (checkRemovalsOnly) {
|
|
1868
|
+
try {
|
|
1869
|
+
current = bulk ? bulk.get(app) : await resource.read(ctx, app);
|
|
1870
|
+
if (bulk === null || current === void 0) current = await resource.read(ctx, app);
|
|
1871
|
+
} catch (err) {
|
|
1872
|
+
result.readWarnings.push(`could not read '${resource.key}' for app '${app}' to look for removed settings: ${err instanceof Error ? err.message : String(err)}`);
|
|
1873
|
+
continue;
|
|
1874
|
+
}
|
|
1875
|
+
const removed2 = resource.removals(current, void 0);
|
|
1876
|
+
if (removed2.length > 0) {
|
|
1877
|
+
appDiff[resource.key] = { status: "extra", current, removed: removed2 };
|
|
1878
|
+
result.inSync = false;
|
|
1879
|
+
}
|
|
1880
|
+
continue;
|
|
1881
|
+
}
|
|
1882
|
+
if (bulk === null) {
|
|
1883
|
+
bulkUnreadable = true;
|
|
1884
|
+
current = await resource.read(ctx, app);
|
|
1885
|
+
} else if (bulk) {
|
|
1886
|
+
current = bulk.get(app);
|
|
1887
|
+
if (resource.key === "ssl" && current === void 0) {
|
|
1888
|
+
current = await resource.read(ctx, app);
|
|
1889
|
+
if (current === void 0) bulkUnreadable = true;
|
|
1890
|
+
}
|
|
1891
|
+
} else {
|
|
1892
|
+
current = await resource.read(ctx, app);
|
|
1893
|
+
}
|
|
1894
|
+
const sslStatus = resource.key === "ssl" ? diffSslStatus(desired, current) : void 0;
|
|
1895
|
+
if (sslStatus) {
|
|
1896
|
+
appDiff[resource.key] = { status: sslStatus, desired, current };
|
|
1897
|
+
if (sslStatus !== "in-sync") result.inSync = false;
|
|
1898
|
+
continue;
|
|
1899
|
+
}
|
|
1900
|
+
let removed = resource.removals ? resource.removals(current, desired) : [];
|
|
1901
|
+
if (resource.forceApply) {
|
|
1902
|
+
if (removed.length > 0) {
|
|
1903
|
+
appDiff[resource.key] = { status: "extra", desired, current, removed };
|
|
1904
|
+
result.inSync = false;
|
|
1905
|
+
}
|
|
1906
|
+
continue;
|
|
1907
|
+
}
|
|
1908
|
+
const change = resource.additive ? computeAdditiveChange(current, desired) : computeChange(current, desired);
|
|
1909
|
+
if (!resource.removals && Array.isArray(current) && "removed" in change) {
|
|
1910
|
+
removed = change.removed;
|
|
1911
|
+
}
|
|
1912
|
+
if (!change.changed && removed.length === 0) {
|
|
1355
1913
|
appDiff[resource.key] = { status: "in-sync", desired, current };
|
|
1356
|
-
} else if (
|
|
1914
|
+
} else if (removed.length > 0 && !hasAdditionsOrChanges(change)) {
|
|
1915
|
+
appDiff[resource.key] = { status: "extra", desired, current, removed };
|
|
1916
|
+
result.inSync = false;
|
|
1917
|
+
} else if (bulkUnreadable && isEmptyValue(current)) {
|
|
1918
|
+
appDiff[resource.key] = { status: "unknown", desired, current };
|
|
1919
|
+
result.inSync = false;
|
|
1920
|
+
const msg = `could not read '${resource.key}' for app '${app}' (bulk report was empty and the per-app read returned nothing)`;
|
|
1921
|
+
result.readWarnings.push(msg);
|
|
1922
|
+
} else if (isEmptyValue(current)) {
|
|
1357
1923
|
appDiff[resource.key] = { status: "missing", desired, current };
|
|
1358
1924
|
result.inSync = false;
|
|
1359
1925
|
} else {
|
|
1360
1926
|
appDiff[resource.key] = { status: "changed", desired, current };
|
|
1361
1927
|
result.inSync = false;
|
|
1362
1928
|
}
|
|
1929
|
+
if (removed.length > 0 && appDiff[resource.key].status !== "extra") {
|
|
1930
|
+
appDiff[resource.key].removed = removed;
|
|
1931
|
+
}
|
|
1932
|
+
}
|
|
1933
|
+
if (appConfig.links === void 0 && appExists.get(app)) {
|
|
1934
|
+
const linked = [];
|
|
1935
|
+
for (const [plugin, services] of [["postgres", config.postgres], ["redis", config.redis]]) {
|
|
1936
|
+
for (const svc of Object.keys(services ?? {})) {
|
|
1937
|
+
if (await ctx.check(`${plugin}:linked`, svc, app)) linked.push(svc);
|
|
1938
|
+
}
|
|
1939
|
+
}
|
|
1940
|
+
if (linked.length > 0) {
|
|
1941
|
+
appDiff["links"] = { status: "extra", current: linked, removed: linked, note: "no links: in the file; up will unlink these" };
|
|
1942
|
+
result.inSync = false;
|
|
1943
|
+
}
|
|
1363
1944
|
}
|
|
1364
1945
|
result.apps[app] = appDiff;
|
|
1365
1946
|
}
|
|
1947
|
+
if (config.plugins) {
|
|
1948
|
+
const plans = planPlugins(await ctx.queryStrict("plugin:list", "--format", "json"), config.plugins);
|
|
1949
|
+
for (const plan of plans) {
|
|
1950
|
+
if (plan.action === "ok") continue;
|
|
1951
|
+
const status = plan.action === "install" ? "missing" : plan.action === "unknown" ? "unknown" : "changed";
|
|
1952
|
+
const note = plan.action === "newer" ? "installed is newer; up warns and leaves it" : plan.action === "unknown" ? "versions cannot be compared; up leaves it" : void 0;
|
|
1953
|
+
result.plugins[plan.name] = { status, desired: plan.desired, current: plan.installed, note };
|
|
1954
|
+
if (plan.action !== "unknown") result.inSync = false;
|
|
1955
|
+
}
|
|
1956
|
+
}
|
|
1366
1957
|
for (const svc of Object.keys(config.postgres ?? {})) {
|
|
1367
1958
|
const exists = await ctx.check("postgres:exists", svc);
|
|
1368
1959
|
result.services[svc] = { status: exists ? "in-sync" : "missing" };
|
|
@@ -1376,14 +1967,16 @@ async function computeDiff(ctx, config) {
|
|
|
1376
1967
|
return result;
|
|
1377
1968
|
}
|
|
1378
1969
|
function maskDiffResult(diff) {
|
|
1379
|
-
const masked = { apps: {}, services: diff.services, inSync: diff.inSync };
|
|
1970
|
+
const masked = { apps: {}, services: diff.services, plugins: diff.plugins, inSync: diff.inSync, readWarnings: diff.readWarnings };
|
|
1380
1971
|
for (const [app, appDiff] of Object.entries(diff.apps)) {
|
|
1381
1972
|
const maskedAppDiff = {};
|
|
1382
1973
|
for (const [feature, d] of Object.entries(appDiff)) {
|
|
1383
1974
|
maskedAppDiff[feature] = {
|
|
1384
1975
|
status: d.status,
|
|
1385
1976
|
desired: maskSensitiveData(d.desired),
|
|
1386
|
-
current: maskSensitiveData(d.current)
|
|
1977
|
+
current: maskSensitiveData(d.current),
|
|
1978
|
+
...d.removed ? { removed: d.removed } : {},
|
|
1979
|
+
...d.note ? { note: d.note } : {}
|
|
1387
1980
|
};
|
|
1388
1981
|
}
|
|
1389
1982
|
masked.apps[app] = maskedAppDiff;
|
|
@@ -1400,18 +1993,29 @@ function formatSummary(diff) {
|
|
|
1400
1993
|
} else {
|
|
1401
1994
|
lines.push(` app: ${chalk2.bold(app)}`);
|
|
1402
1995
|
for (const [feature, d] of changes) {
|
|
1403
|
-
const sym = d.status === "missing" ? chalk2.green("+") : chalk2.yellow("~");
|
|
1996
|
+
const sym = d.status === "missing" ? chalk2.green("+") : d.status === "unknown" ? chalk2.red("?") : d.status === "extra" ? chalk2.red("-") : chalk2.yellow("~");
|
|
1404
1997
|
lines.push(` ${sym} ${feature}: ${formatFeatureSummary(d)}`);
|
|
1405
1998
|
}
|
|
1406
1999
|
}
|
|
1407
2000
|
}
|
|
2001
|
+
for (const [name, d] of Object.entries(diff.plugins)) {
|
|
2002
|
+
const sym = d.status === "missing" ? chalk2.green("+") : d.status === "unknown" ? chalk2.red("?") : chalk2.yellow("~");
|
|
2003
|
+
const detail = d.status === "missing" ? `not installed (want ${d.desired ?? "any"})` : `${d.current} \u2192 ${d.desired}`;
|
|
2004
|
+
lines.push(` plugin: ${name}`);
|
|
2005
|
+
lines.push(` ${sym} version: ${detail}${d.note ? ` (${d.note})` : ""}`);
|
|
2006
|
+
}
|
|
1408
2007
|
for (const [svc, d] of Object.entries(diff.services)) {
|
|
1409
2008
|
if (d.status === "missing") {
|
|
1410
2009
|
lines.push(` services:`);
|
|
1411
2010
|
lines.push(` ${chalk2.green("+")} ${svc}: not provisioned`);
|
|
1412
2011
|
}
|
|
1413
2012
|
}
|
|
1414
|
-
const total = Object.values(diff.apps).flatMap((a) => Object.values(a)).filter((d) => d.status !== "in-sync").length + Object.values(diff.services).filter((d) => d.status !== "in-sync").length;
|
|
2013
|
+
const total = Object.values(diff.apps).flatMap((a) => Object.values(a)).filter((d) => d.status !== "in-sync").length + Object.values(diff.services).filter((d) => d.status !== "in-sync").length + Object.values(diff.plugins).filter((d) => d.status !== "unknown").length;
|
|
2014
|
+
if (diff.readWarnings.length > 0) {
|
|
2015
|
+
lines.push("");
|
|
2016
|
+
lines.push(chalk2.red(" Could not read some server state \u2014 treated as unknown, not missing:"));
|
|
2017
|
+
for (const w of diff.readWarnings) lines.push(chalk2.red(` - ${w}`));
|
|
2018
|
+
}
|
|
1415
2019
|
lines.push("");
|
|
1416
2020
|
if (total === 0) {
|
|
1417
2021
|
lines.push(chalk2.green(" Everything in sync."));
|
|
@@ -1422,11 +2026,21 @@ function formatSummary(diff) {
|
|
|
1422
2026
|
return lines.join("\n");
|
|
1423
2027
|
}
|
|
1424
2028
|
function formatFeatureSummary(d) {
|
|
2029
|
+
const gone = d.removed?.length ? `not in the file: ${d.removed.join(", ")}` : "";
|
|
2030
|
+
if (d.status === "extra") return `${gone}${d.note ? ` (${d.note})` : ""}`;
|
|
2031
|
+
const base = formatChange(d);
|
|
2032
|
+
return gone ? `${base}; ${gone}` : base;
|
|
2033
|
+
}
|
|
2034
|
+
function formatChange(d) {
|
|
2035
|
+
if (d.status === "unknown") return "(could not read server state \u2014 not reported as missing)";
|
|
1425
2036
|
if (d.status === "missing") return "(not set on server)";
|
|
1426
2037
|
if (Array.isArray(d.desired) && Array.isArray(d.current)) {
|
|
1427
2038
|
return `${d.current.length} \u2192 ${d.desired.length} items`;
|
|
1428
2039
|
}
|
|
1429
|
-
return `${JSON.stringify(d.current)} \u2192 ${JSON.stringify(d.desired)}`;
|
|
2040
|
+
return `${JSON.stringify(d.current, showUnset)} \u2192 ${JSON.stringify(d.desired, showUnset)}`;
|
|
2041
|
+
}
|
|
2042
|
+
function showUnset(_key, value) {
|
|
2043
|
+
return value === "\0missing" ? "(unset)" : value;
|
|
1430
2044
|
}
|
|
1431
2045
|
function formatVerbose(diff) {
|
|
1432
2046
|
const lines = [""];
|
|
@@ -1435,18 +2049,38 @@ function formatVerbose(diff) {
|
|
|
1435
2049
|
if (changes.length === 0) continue;
|
|
1436
2050
|
for (const [feature, d] of changes) {
|
|
1437
2051
|
lines.push(`@@ app: ${app} / ${feature} @@`);
|
|
1438
|
-
|
|
1439
|
-
|
|
2052
|
+
if (d.status === "unknown") {
|
|
2053
|
+
lines.push(chalk2.red(` ? could not read server state \u2014 not reported as missing`));
|
|
2054
|
+
continue;
|
|
2055
|
+
}
|
|
2056
|
+
if (d.status === "extra") {
|
|
2057
|
+
for (const r of d.removed ?? []) lines.push(chalk2.red(`- ${r}`));
|
|
2058
|
+
if (d.note) lines.push(` (${d.note})`);
|
|
2059
|
+
continue;
|
|
2060
|
+
}
|
|
2061
|
+
const currentLines = d.current !== void 0 ? JSON.stringify(d.current, showUnset, 2).split("\n") : [];
|
|
2062
|
+
const desiredLines = JSON.stringify(d.desired, showUnset, 2).split("\n");
|
|
1440
2063
|
for (const line of currentLines) lines.push(chalk2.red(`- ${line}`));
|
|
1441
2064
|
for (const line of desiredLines) lines.push(chalk2.green(`+ ${line}`));
|
|
2065
|
+
for (const r of d.removed ?? []) lines.push(chalk2.red(`- ${r} (not in the file)`));
|
|
1442
2066
|
}
|
|
1443
2067
|
}
|
|
2068
|
+
for (const [name, d] of Object.entries(diff.plugins)) {
|
|
2069
|
+
lines.push(`@@ plugin: ${name} @@`);
|
|
2070
|
+
if (d.current !== void 0) lines.push(chalk2.red(`- ${d.current}`));
|
|
2071
|
+
lines.push(chalk2.green(`+ ${d.desired ?? "(any version)"}`) + (d.note ? ` (${d.note})` : ""));
|
|
2072
|
+
}
|
|
1444
2073
|
for (const [svc, d] of Object.entries(diff.services)) {
|
|
1445
2074
|
if (d.status === "missing") {
|
|
1446
2075
|
lines.push(`@@ services @@`);
|
|
1447
2076
|
lines.push(chalk2.green(`+ ${svc}`));
|
|
1448
2077
|
}
|
|
1449
2078
|
}
|
|
2079
|
+
if (diff.readWarnings.length > 0) {
|
|
2080
|
+
lines.push("");
|
|
2081
|
+
lines.push(chalk2.red("Could not read some server state \u2014 treated as unknown, not missing:"));
|
|
2082
|
+
for (const w of diff.readWarnings) lines.push(chalk2.red(` - ${w}`));
|
|
2083
|
+
}
|
|
1450
2084
|
lines.push("");
|
|
1451
2085
|
return lines.join("\n");
|
|
1452
2086
|
}
|
|
@@ -1502,6 +2136,7 @@ function validate(filePath) {
|
|
|
1502
2136
|
}
|
|
1503
2137
|
|
|
1504
2138
|
// src/index.ts
|
|
2139
|
+
import chalk3 from "chalk";
|
|
1505
2140
|
var require2 = createRequire(import.meta.url);
|
|
1506
2141
|
var { version } = require2("../package.json");
|
|
1507
2142
|
var program = new Command().name("dokku-compose").version(version);
|
|
@@ -1511,12 +2146,12 @@ function makeRunner(opts) {
|
|
|
1511
2146
|
dryRun: opts.dryRun ?? false
|
|
1512
2147
|
});
|
|
1513
2148
|
}
|
|
1514
|
-
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("--sensitive", "Show sensitive values in dry-run output (masked by default)").option("--fail-fast", "Stop on first error").action(async (apps, opts) => {
|
|
2149
|
+
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("--sensitive", "Show sensitive values in dry-run output (masked by default)").option("--fail-fast", "Stop on first error").option("--prune", "Also reset settings the file no longer declares (read `diff` first)").action(async (apps, opts) => {
|
|
1515
2150
|
const config = loadConfig(opts.file);
|
|
1516
2151
|
const runner = makeRunner(opts);
|
|
1517
2152
|
const ctx = createContext(runner);
|
|
1518
2153
|
try {
|
|
1519
|
-
await runUp(ctx, config, apps);
|
|
2154
|
+
await runUp(ctx, config, apps, { failFast: opts.failFast ?? false, prune: opts.prune ?? false });
|
|
1520
2155
|
if (opts.dryRun) {
|
|
1521
2156
|
console.log("\n# Commands that would run:");
|
|
1522
2157
|
for (const cmd of runner.dryRunLog) {
|
|
@@ -1524,11 +2159,15 @@ program.command("up [apps...]").description("Create/update apps and services to
|
|
|
1524
2159
|
console.log(`dokku ${line}`);
|
|
1525
2160
|
}
|
|
1526
2161
|
}
|
|
2162
|
+
} catch (error) {
|
|
2163
|
+
console.error(chalk3.red(`
|
|
2164
|
+
${error.message}`));
|
|
2165
|
+
process.exitCode = 1;
|
|
1527
2166
|
} finally {
|
|
1528
2167
|
await ctx.close();
|
|
1529
2168
|
}
|
|
1530
2169
|
});
|
|
1531
|
-
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) => {
|
|
2170
|
+
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").option("--fail-fast", "Stop on first error").action(async (apps, opts) => {
|
|
1532
2171
|
if (!opts.force) {
|
|
1533
2172
|
console.error("--force required");
|
|
1534
2173
|
process.exit(1);
|
|
@@ -1537,7 +2176,11 @@ program.command("down [apps...]").description("Destroy apps and services (requir
|
|
|
1537
2176
|
const runner = makeRunner({});
|
|
1538
2177
|
const ctx = createContext(runner);
|
|
1539
2178
|
try {
|
|
1540
|
-
await runDown(ctx, config, apps, { force: true });
|
|
2179
|
+
await runDown(ctx, config, apps, { force: true, failFast: opts.failFast ?? false });
|
|
2180
|
+
} catch (error) {
|
|
2181
|
+
console.error(chalk3.red(`
|
|
2182
|
+
${error.message}`));
|
|
2183
|
+
process.exitCode = 1;
|
|
1541
2184
|
} finally {
|
|
1542
2185
|
await ctx.close();
|
|
1543
2186
|
}
|
|
@@ -1586,7 +2229,7 @@ program.command("diff").description("Show what is out of sync between config and
|
|
|
1586
2229
|
const diff = opts.sensitive ? rawDiff : maskDiffResult(rawDiff);
|
|
1587
2230
|
const output = opts.verbose ? formatVerbose(diff) : formatSummary(diff);
|
|
1588
2231
|
process.stdout.write(output);
|
|
1589
|
-
process.
|
|
2232
|
+
process.exitCode = rawDiff.inSync ? 0 : 1;
|
|
1590
2233
|
} finally {
|
|
1591
2234
|
await ctx.close();
|
|
1592
2235
|
}
|