dokku-compose 0.11.1 → 0.12.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 +11 -0
- package/dist/index.js +271 -81
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -104,6 +104,17 @@ Running `up` again produces no changes — every step checks current state befor
|
|
|
104
104
|
| `--remove-orphans` | Destroy services and networks not in config |
|
|
105
105
|
| `--verbose` | Show git-style +/- diff (diff command only) |
|
|
106
106
|
|
|
107
|
+
### Errors
|
|
108
|
+
|
|
109
|
+
A failing Dokku command (mutation) is never reported as a success. On `up` and `down`, a
|
|
110
|
+
global-phase failure (plugins, docker auth, global config, networks, services) aborts the whole
|
|
111
|
+
run immediately. A per-app failure is printed with the command and Dokku's error output, and by
|
|
112
|
+
default the run continues to the next app, exiting non-zero at the end with a summary of which
|
|
113
|
+
apps failed; pass `--fail-fast` to stop at the first failure instead. `diff` never reports a
|
|
114
|
+
resource as missing when it could not actually read server state — it falls back to a per-app
|
|
115
|
+
read and, if that's unreadable too, marks the resource `unknown` and exits non-zero with a
|
|
116
|
+
warning naming what it couldn't read.
|
|
117
|
+
|
|
107
118
|
## Features
|
|
108
119
|
|
|
109
120
|
All features are idempotent — running `up` twice produces no changes.
|
package/dist/index.js
CHANGED
|
@@ -149,6 +149,95 @@ import { execa } from "execa";
|
|
|
149
149
|
import { createHash } from "crypto";
|
|
150
150
|
import * as os from "os";
|
|
151
151
|
import * as path from "path";
|
|
152
|
+
|
|
153
|
+
// src/core/mask.ts
|
|
154
|
+
var SENSITIVE_RE = /(?:TOKEN|SECRET|PASSWORD|KEY|AUTH|CREDENTIAL)/i;
|
|
155
|
+
function maskValue(value) {
|
|
156
|
+
if (value.length <= 4) return "****";
|
|
157
|
+
return `****${value.slice(-4)}`;
|
|
158
|
+
}
|
|
159
|
+
function maskSensitiveArgs(cmd) {
|
|
160
|
+
let masked = cmd.replace(/([^\s=]*(?:TOKEN|SECRET|PASSWORD|KEY|AUTH|CREDENTIAL)[^\s=]*)=(\S+)/gi, (_, key, value) => {
|
|
161
|
+
return `${key}=${maskValue(value)}`;
|
|
162
|
+
});
|
|
163
|
+
masked = masked.replace(
|
|
164
|
+
/^(registry:login\s+(?:--global\s+)?\S+\s+\S+\s+)(\S+)/,
|
|
165
|
+
(_, prefix, password) => `${prefix}${maskValue(password)}`
|
|
166
|
+
);
|
|
167
|
+
masked = masked.replace(
|
|
168
|
+
/^(\S+:backup-auth\s+\S+\s+)(\S+)(\s+)(\S+)/,
|
|
169
|
+
(_, prefix, accessKeyId, sep, secretAccessKey) => `${prefix}${maskValue(accessKeyId)}${sep}${maskValue(secretAccessKey)}`
|
|
170
|
+
);
|
|
171
|
+
return masked;
|
|
172
|
+
}
|
|
173
|
+
function maskSensitiveArgsList(args) {
|
|
174
|
+
const secrets = [];
|
|
175
|
+
const masked = [...args];
|
|
176
|
+
const mark = (index) => {
|
|
177
|
+
const value = masked[index];
|
|
178
|
+
if (value === void 0 || value === "") return;
|
|
179
|
+
secrets.push(value);
|
|
180
|
+
masked[index] = maskValue(value);
|
|
181
|
+
};
|
|
182
|
+
for (let i = 0; i < masked.length; i++) {
|
|
183
|
+
const m = masked[i].match(/^([^\s=]*(?:TOKEN|SECRET|PASSWORD|KEY|AUTH|CREDENTIAL)[^\s=]*)=(.+)$/i);
|
|
184
|
+
if (m) {
|
|
185
|
+
secrets.push(m[2]);
|
|
186
|
+
masked[i] = `${m[1]}=${maskValue(m[2])}`;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
if (masked[0] === "registry:login") {
|
|
190
|
+
mark(masked.length - 1);
|
|
191
|
+
}
|
|
192
|
+
if (/^\S+:backup-auth$/.test(masked[0] ?? "")) {
|
|
193
|
+
mark(2);
|
|
194
|
+
mark(3);
|
|
195
|
+
}
|
|
196
|
+
return { masked, secrets };
|
|
197
|
+
}
|
|
198
|
+
function maskSecretsInText(text, secrets) {
|
|
199
|
+
let masked = text;
|
|
200
|
+
for (const secret of secrets) {
|
|
201
|
+
if (!secret) continue;
|
|
202
|
+
masked = masked.split(secret).join(maskValue(secret));
|
|
203
|
+
}
|
|
204
|
+
return masked;
|
|
205
|
+
}
|
|
206
|
+
function maskSensitiveData(data) {
|
|
207
|
+
if (data === null || data === void 0) return data;
|
|
208
|
+
if (typeof data === "string") return data;
|
|
209
|
+
if (Array.isArray(data)) return data.map(maskSensitiveData);
|
|
210
|
+
if (typeof data === "object") {
|
|
211
|
+
const result = {};
|
|
212
|
+
for (const [key, value] of Object.entries(data)) {
|
|
213
|
+
if (typeof value === "string" && SENSITIVE_RE.test(key)) {
|
|
214
|
+
result[key] = maskValue(value);
|
|
215
|
+
} else if (typeof value === "object" && value !== null) {
|
|
216
|
+
result[key] = maskSensitiveData(value);
|
|
217
|
+
} else {
|
|
218
|
+
result[key] = value;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
return result;
|
|
222
|
+
}
|
|
223
|
+
return data;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// src/core/dokku.ts
|
|
227
|
+
var DokkuCommandError = class extends Error {
|
|
228
|
+
constructor(args, exitCode, stderr) {
|
|
229
|
+
const { masked, secrets } = maskSensitiveArgsList(args);
|
|
230
|
+
const maskedCmd = masked.join(" ");
|
|
231
|
+
const stderrTail = maskSecretsInText(stderr.trim(), secrets);
|
|
232
|
+
super(
|
|
233
|
+
`dokku ${maskedCmd} failed (exit ${exitCode})` + (stderrTail ? `: ${stderrTail}` : "")
|
|
234
|
+
);
|
|
235
|
+
this.args = args;
|
|
236
|
+
this.exitCode = exitCode;
|
|
237
|
+
this.stderr = stderr;
|
|
238
|
+
this.name = "DokkuCommandError";
|
|
239
|
+
}
|
|
240
|
+
};
|
|
152
241
|
function createRunner(opts = {}) {
|
|
153
242
|
const log = [];
|
|
154
243
|
const controlPath = opts.host ? path.join(os.tmpdir(), `dc-${createHash("sha1").update(opts.host).digest("hex").slice(0, 16)}.sock`) : null;
|
|
@@ -160,17 +249,17 @@ function createRunner(opts = {}) {
|
|
|
160
249
|
async function execDokku(args) {
|
|
161
250
|
if (opts.host) {
|
|
162
251
|
try {
|
|
163
|
-
const result = await execa("ssh", [...sshControlFlags, `dokku@${opts.host}`, "--", ...args.map(shellQuote)]);
|
|
164
|
-
return { stdout: String(result.stdout ?? ""), ok: true };
|
|
252
|
+
const result = await execa("ssh", [...sshControlFlags, `dokku@${opts.host}`, "--", ...args.map(shellQuote)], { stdin: "ignore" });
|
|
253
|
+
return { stdout: String(result.stdout ?? ""), stderr: String(result.stderr ?? ""), exitCode: result.exitCode ?? 0, ok: true };
|
|
165
254
|
} catch (e) {
|
|
166
|
-
return { stdout: String(e.stdout ?? ""), ok: false };
|
|
255
|
+
return { stdout: String(e.stdout ?? ""), stderr: String(e.stderr ?? ""), exitCode: e.exitCode ?? 1, ok: false };
|
|
167
256
|
}
|
|
168
257
|
} else {
|
|
169
258
|
try {
|
|
170
|
-
const result = await execa("dokku", args);
|
|
171
|
-
return { stdout: String(result.stdout ?? ""), ok: true };
|
|
259
|
+
const result = await execa("dokku", args, { stdin: "ignore" });
|
|
260
|
+
return { stdout: String(result.stdout ?? ""), stderr: String(result.stderr ?? ""), exitCode: result.exitCode ?? 0, ok: true };
|
|
172
261
|
} catch (e) {
|
|
173
|
-
return { stdout: String(e.stdout ?? ""), ok: false };
|
|
262
|
+
return { stdout: String(e.stdout ?? ""), stderr: String(e.stderr ?? ""), exitCode: e.exitCode ?? 1, ok: false };
|
|
174
263
|
}
|
|
175
264
|
}
|
|
176
265
|
}
|
|
@@ -181,7 +270,10 @@ function createRunner(opts = {}) {
|
|
|
181
270
|
log.push(args.map((a) => a.includes(" ") ? shellQuote(a) : a).join(" "));
|
|
182
271
|
return;
|
|
183
272
|
}
|
|
184
|
-
await execDokku(args);
|
|
273
|
+
const { ok, exitCode, stderr } = await execDokku(args);
|
|
274
|
+
if (!ok) {
|
|
275
|
+
throw new DokkuCommandError(args, exitCode, stderr);
|
|
276
|
+
}
|
|
185
277
|
},
|
|
186
278
|
async query(...args) {
|
|
187
279
|
const { stdout } = await execDokku(args);
|
|
@@ -194,7 +286,7 @@ function createRunner(opts = {}) {
|
|
|
194
286
|
async close() {
|
|
195
287
|
if (!opts.host || !controlPath) return;
|
|
196
288
|
try {
|
|
197
|
-
await execa("ssh", ["-O", "exit", "-o", `ControlPath=${controlPath}`, `dokku@${opts.host}`]);
|
|
289
|
+
await execa("ssh", ["-O", "exit", "-o", `ControlPath=${controlPath}`, `dokku@${opts.host}`], { stdin: "ignore" });
|
|
198
290
|
} catch {
|
|
199
291
|
}
|
|
200
292
|
}
|
|
@@ -275,6 +367,9 @@ function logDone() {
|
|
|
275
367
|
function logSkip() {
|
|
276
368
|
console.log(`... ${chalk.yellow("already configured")}`);
|
|
277
369
|
}
|
|
370
|
+
function logError(context, message) {
|
|
371
|
+
console.error(chalk.red(`[${context.padEnd(12)}] ERROR: ${message}`));
|
|
372
|
+
}
|
|
278
373
|
function logWarn(context, message) {
|
|
279
374
|
console.warn(chalk.yellow(`[${context.padEnd(12)}] WARN: ${message}`));
|
|
280
375
|
}
|
|
@@ -421,6 +516,10 @@ var Storage = {
|
|
|
421
516
|
};
|
|
422
517
|
|
|
423
518
|
// src/resources/properties.ts
|
|
519
|
+
async function isAppDeployed(ctx, target) {
|
|
520
|
+
const raw = await ctx.query("ps:report", target, "--deployed");
|
|
521
|
+
return raw.trim() === "true";
|
|
522
|
+
}
|
|
424
523
|
function propertyResource(opts) {
|
|
425
524
|
return {
|
|
426
525
|
key: opts.key,
|
|
@@ -438,7 +537,12 @@ function propertyResource(opts) {
|
|
|
438
537
|
}
|
|
439
538
|
if (opts.afterChange) {
|
|
440
539
|
for (const cmd of opts.afterChange) {
|
|
441
|
-
|
|
540
|
+
try {
|
|
541
|
+
await ctx.run(cmd, target);
|
|
542
|
+
} catch (err) {
|
|
543
|
+
if (await isAppDeployed(ctx, target)) throw err;
|
|
544
|
+
logWarn(target, `${cmd} failed on an undeployed app \u2014 its proxy config will be built on first deploy`);
|
|
545
|
+
}
|
|
442
546
|
}
|
|
443
547
|
}
|
|
444
548
|
}
|
|
@@ -546,15 +650,17 @@ var Config = {
|
|
|
546
650
|
var Certs = {
|
|
547
651
|
key: "ssl",
|
|
548
652
|
read: async (ctx, target) => {
|
|
549
|
-
const raw = await ctx.query("certs:report", target, "--ssl-enabled");
|
|
550
|
-
|
|
653
|
+
const raw = (await ctx.query("certs:report", target, "--ssl-enabled")).trim();
|
|
654
|
+
if (raw === "") return void 0;
|
|
655
|
+
return raw === "true";
|
|
551
656
|
},
|
|
552
657
|
readAll: async (ctx) => {
|
|
553
658
|
const raw = await ctx.query("certs:report");
|
|
554
659
|
const bulk = parseBulkReport(raw, "ssl");
|
|
555
660
|
const result = /* @__PURE__ */ new Map();
|
|
556
661
|
for (const [app, report] of bulk) {
|
|
557
|
-
|
|
662
|
+
const enabled = report["enabled"];
|
|
663
|
+
result.set(app, enabled === void 0 ? void 0 : enabled === "true");
|
|
558
664
|
}
|
|
559
665
|
return result;
|
|
560
666
|
},
|
|
@@ -1133,7 +1239,38 @@ async function ensureGlobalNginx(ctx, nginx) {
|
|
|
1133
1239
|
}
|
|
1134
1240
|
|
|
1135
1241
|
// src/commands/up.ts
|
|
1136
|
-
|
|
1242
|
+
var UpFailedError = class extends Error {
|
|
1243
|
+
constructor(failures) {
|
|
1244
|
+
super(
|
|
1245
|
+
`up failed for ${failures.length} app(s): ${failures.map((f) => f.app).join(", ")}`
|
|
1246
|
+
);
|
|
1247
|
+
this.failures = failures;
|
|
1248
|
+
this.name = "UpFailedError";
|
|
1249
|
+
}
|
|
1250
|
+
};
|
|
1251
|
+
async function processApp(ctx, config, app, appConfig) {
|
|
1252
|
+
await reconcile(Apps, ctx, app, true);
|
|
1253
|
+
await reconcile(Domains, ctx, app, appConfig.domains);
|
|
1254
|
+
await reconcile(Networks, ctx, app, appConfig.networks);
|
|
1255
|
+
await reconcile(NetworkProps, ctx, app, appConfig.network);
|
|
1256
|
+
await reconcile(Proxy, ctx, app, appConfig.proxy?.enabled);
|
|
1257
|
+
await reconcile(Ports, ctx, app, appConfig.ports);
|
|
1258
|
+
if (config.postgres || config.redis) {
|
|
1259
|
+
await ensureAppLinks(ctx, app, appConfig.links ?? [], config);
|
|
1260
|
+
}
|
|
1261
|
+
await reconcile(Certs, ctx, app, appConfig.ssl);
|
|
1262
|
+
await reconcile(Storage, ctx, app, appConfig.storage);
|
|
1263
|
+
await reconcile(Nginx, ctx, app, appConfig.nginx);
|
|
1264
|
+
await reconcile(Checks, ctx, app, appConfig.checks);
|
|
1265
|
+
await reconcile(Logs, ctx, app, appConfig.logs);
|
|
1266
|
+
await reconcile(Registry, ctx, app, appConfig.registry);
|
|
1267
|
+
await reconcile(Scheduler, ctx, app, appConfig.scheduler);
|
|
1268
|
+
await reconcile(Config, ctx, app, appConfig.env);
|
|
1269
|
+
await reconcile(Builder, ctx, app, appConfig.build);
|
|
1270
|
+
await reconcile(Git, ctx, app, appConfig.git ?? config.git);
|
|
1271
|
+
await reconcile(DockerOptions, ctx, app, appConfig.docker_options);
|
|
1272
|
+
}
|
|
1273
|
+
async function runUp(ctx, config, appFilter, opts = {}) {
|
|
1137
1274
|
const apps = appFilter.length > 0 ? appFilter : Object.keys(config.apps);
|
|
1138
1275
|
await ensureDokkuVersion(ctx, config.dokku?.version);
|
|
1139
1276
|
if (config.plugins) await ensurePlugins(ctx, config.plugins);
|
|
@@ -1146,29 +1283,21 @@ async function runUp(ctx, config, appFilter) {
|
|
|
1146
1283
|
if (config.postgres) await ensurePostgres(ctx, config.postgres);
|
|
1147
1284
|
if (config.redis) await ensureRedis(ctx, config.redis);
|
|
1148
1285
|
if (config.postgres) await ensurePostgresBackups(ctx, config.postgres);
|
|
1286
|
+
const failures = [];
|
|
1149
1287
|
for (const app of apps) {
|
|
1150
1288
|
const appConfig = config.apps[app];
|
|
1151
1289
|
if (!appConfig) continue;
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
await ensureAppLinks(ctx, app, appConfig.links ?? [], config);
|
|
1290
|
+
try {
|
|
1291
|
+
await processApp(ctx, config, app, appConfig);
|
|
1292
|
+
} catch (error) {
|
|
1293
|
+
if (opts.failFast) throw error;
|
|
1294
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1295
|
+
logError(app, message);
|
|
1296
|
+
failures.push({ app, error });
|
|
1160
1297
|
}
|
|
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);
|
|
1298
|
+
}
|
|
1299
|
+
if (failures.length > 0) {
|
|
1300
|
+
throw new UpFailedError(failures);
|
|
1172
1301
|
}
|
|
1173
1302
|
}
|
|
1174
1303
|
|
|
@@ -1191,15 +1320,35 @@ async function exportApps(ctx) {
|
|
|
1191
1320
|
}
|
|
1192
1321
|
|
|
1193
1322
|
// src/commands/down.ts
|
|
1323
|
+
var DownFailedError = class extends Error {
|
|
1324
|
+
constructor(failures) {
|
|
1325
|
+
super(
|
|
1326
|
+
`down failed for ${failures.length} app(s): ${failures.map((f) => f.app).join(", ")} \u2014 service and network teardown was skipped because app teardown failed`
|
|
1327
|
+
);
|
|
1328
|
+
this.failures = failures;
|
|
1329
|
+
this.name = "DownFailedError";
|
|
1330
|
+
}
|
|
1331
|
+
};
|
|
1194
1332
|
async function runDown(ctx, config, appFilter, opts) {
|
|
1195
1333
|
const apps = appFilter.length > 0 ? appFilter : Object.keys(config.apps);
|
|
1334
|
+
const failures = [];
|
|
1196
1335
|
for (const app of apps) {
|
|
1197
1336
|
const appConfig = config.apps[app];
|
|
1198
1337
|
if (!appConfig) continue;
|
|
1199
|
-
|
|
1200
|
-
|
|
1338
|
+
try {
|
|
1339
|
+
if (appConfig.links && (config.postgres || config.redis)) {
|
|
1340
|
+
await destroyAppLinks(ctx, app, appConfig.links, config);
|
|
1341
|
+
}
|
|
1342
|
+
await destroyApp(ctx, app);
|
|
1343
|
+
} catch (error) {
|
|
1344
|
+
if (opts.failFast) throw error;
|
|
1345
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1346
|
+
logError(app, message);
|
|
1347
|
+
failures.push({ app, error });
|
|
1201
1348
|
}
|
|
1202
|
-
|
|
1349
|
+
}
|
|
1350
|
+
if (failures.length > 0) {
|
|
1351
|
+
throw new DownFailedError(failures);
|
|
1203
1352
|
}
|
|
1204
1353
|
if (config.postgres) await destroyPostgres(ctx, config.postgres);
|
|
1205
1354
|
if (config.redis) await destroyRedis(ctx, config.redis);
|
|
@@ -1289,51 +1438,39 @@ async function runExport(ctx, opts) {
|
|
|
1289
1438
|
return config;
|
|
1290
1439
|
}
|
|
1291
1440
|
|
|
1292
|
-
// src/core/mask.ts
|
|
1293
|
-
var SENSITIVE_RE = /(?:TOKEN|SECRET|PASSWORD|KEY|AUTH|CREDENTIAL)/i;
|
|
1294
|
-
function maskValue(value) {
|
|
1295
|
-
if (value.length <= 4) return "****";
|
|
1296
|
-
return `****${value.slice(-4)}`;
|
|
1297
|
-
}
|
|
1298
|
-
function maskSensitiveArgs(cmd) {
|
|
1299
|
-
let masked = cmd.replace(/([^\s=]*(?:TOKEN|SECRET|PASSWORD|KEY|AUTH|CREDENTIAL)[^\s=]*)=(\S+)/gi, (_, key, value) => {
|
|
1300
|
-
return `${key}=${maskValue(value)}`;
|
|
1301
|
-
});
|
|
1302
|
-
masked = masked.replace(
|
|
1303
|
-
/^(registry:login\s+(?:--global\s+)?\S+\s+\S+\s+)(\S+)/,
|
|
1304
|
-
(_, prefix, password) => `${prefix}${maskValue(password)}`
|
|
1305
|
-
);
|
|
1306
|
-
return masked;
|
|
1307
|
-
}
|
|
1308
|
-
function maskSensitiveData(data) {
|
|
1309
|
-
if (data === null || data === void 0) return data;
|
|
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;
|
|
1326
|
-
}
|
|
1327
|
-
|
|
1328
1441
|
// src/commands/diff.ts
|
|
1329
1442
|
import chalk2 from "chalk";
|
|
1443
|
+
function diffSslStatus(desired, current) {
|
|
1444
|
+
if (typeof desired !== "object" || desired === null) return void 0;
|
|
1445
|
+
if (current === void 0) return void 0;
|
|
1446
|
+
if (current === true) return "in-sync";
|
|
1447
|
+
if (current === false || current === null) return "missing";
|
|
1448
|
+
return void 0;
|
|
1449
|
+
}
|
|
1450
|
+
function isEmptyValue(value) {
|
|
1451
|
+
return value === null || value === void 0 || Array.isArray(value) && value.length === 0 || typeof value === "object" && Object.keys(value).length === 0;
|
|
1452
|
+
}
|
|
1330
1453
|
async function computeDiff(ctx, config) {
|
|
1331
|
-
const result = { apps: {}, services: {}, inSync: true };
|
|
1454
|
+
const result = { apps: {}, services: {}, inSync: true, readWarnings: [] };
|
|
1455
|
+
const appNames = Object.keys(config.apps);
|
|
1332
1456
|
await ensureDokkuVersion(ctx, config.dokku?.version);
|
|
1457
|
+
const appExists = /* @__PURE__ */ new Map();
|
|
1458
|
+
await Promise.all(
|
|
1459
|
+
appNames.map(async (app) => {
|
|
1460
|
+
appExists.set(app, await ctx.check("apps:exists", app));
|
|
1461
|
+
})
|
|
1462
|
+
);
|
|
1463
|
+
const existingAppNames = appNames.filter((app) => appExists.get(app));
|
|
1333
1464
|
const prefetched = /* @__PURE__ */ new Map();
|
|
1334
1465
|
await Promise.all(
|
|
1335
1466
|
ALL_APP_RESOURCES.filter((r) => !r.forceApply && !r.key.startsWith("_") && r.readAll).map(async (r) => {
|
|
1336
|
-
|
|
1467
|
+
try {
|
|
1468
|
+
const bulk = await r.readAll(ctx);
|
|
1469
|
+
const unreadable = existingAppNames.length > 0 && bulk.size === 0;
|
|
1470
|
+
prefetched.set(r.key, unreadable ? null : bulk);
|
|
1471
|
+
} catch {
|
|
1472
|
+
prefetched.set(r.key, null);
|
|
1473
|
+
}
|
|
1337
1474
|
})
|
|
1338
1475
|
);
|
|
1339
1476
|
for (const [app, appConfig] of Object.entries(config.apps)) {
|
|
@@ -1348,12 +1485,41 @@ async function computeDiff(ctx, config) {
|
|
|
1348
1485
|
desired = appConfig[resource.key];
|
|
1349
1486
|
}
|
|
1350
1487
|
if (desired === void 0) continue;
|
|
1488
|
+
if (!appExists.get(app)) {
|
|
1489
|
+
appDiff[resource.key] = { status: "missing", desired, current: void 0 };
|
|
1490
|
+
result.inSync = false;
|
|
1491
|
+
continue;
|
|
1492
|
+
}
|
|
1351
1493
|
const bulk = prefetched.get(resource.key);
|
|
1352
|
-
|
|
1494
|
+
let current;
|
|
1495
|
+
let bulkUnreadable = false;
|
|
1496
|
+
if (bulk === null) {
|
|
1497
|
+
bulkUnreadable = true;
|
|
1498
|
+
current = await resource.read(ctx, app);
|
|
1499
|
+
} else if (bulk) {
|
|
1500
|
+
current = bulk.get(app);
|
|
1501
|
+
if (resource.key === "ssl" && current === void 0) {
|
|
1502
|
+
current = await resource.read(ctx, app);
|
|
1503
|
+
if (current === void 0) bulkUnreadable = true;
|
|
1504
|
+
}
|
|
1505
|
+
} else {
|
|
1506
|
+
current = await resource.read(ctx, app);
|
|
1507
|
+
}
|
|
1508
|
+
const sslStatus = resource.key === "ssl" ? diffSslStatus(desired, current) : void 0;
|
|
1509
|
+
if (sslStatus) {
|
|
1510
|
+
appDiff[resource.key] = { status: sslStatus, desired, current };
|
|
1511
|
+
if (sslStatus !== "in-sync") result.inSync = false;
|
|
1512
|
+
continue;
|
|
1513
|
+
}
|
|
1353
1514
|
const change = computeChange(current, desired);
|
|
1354
1515
|
if (!change.changed) {
|
|
1355
1516
|
appDiff[resource.key] = { status: "in-sync", desired, current };
|
|
1356
|
-
} else if (
|
|
1517
|
+
} else if (bulkUnreadable && isEmptyValue(current)) {
|
|
1518
|
+
appDiff[resource.key] = { status: "unknown", desired, current };
|
|
1519
|
+
result.inSync = false;
|
|
1520
|
+
const msg = `could not read '${resource.key}' for app '${app}' (bulk report was empty and the per-app read returned nothing)`;
|
|
1521
|
+
result.readWarnings.push(msg);
|
|
1522
|
+
} else if (isEmptyValue(current)) {
|
|
1357
1523
|
appDiff[resource.key] = { status: "missing", desired, current };
|
|
1358
1524
|
result.inSync = false;
|
|
1359
1525
|
} else {
|
|
@@ -1376,7 +1542,7 @@ async function computeDiff(ctx, config) {
|
|
|
1376
1542
|
return result;
|
|
1377
1543
|
}
|
|
1378
1544
|
function maskDiffResult(diff) {
|
|
1379
|
-
const masked = { apps: {}, services: diff.services, inSync: diff.inSync };
|
|
1545
|
+
const masked = { apps: {}, services: diff.services, inSync: diff.inSync, readWarnings: diff.readWarnings };
|
|
1380
1546
|
for (const [app, appDiff] of Object.entries(diff.apps)) {
|
|
1381
1547
|
const maskedAppDiff = {};
|
|
1382
1548
|
for (const [feature, d] of Object.entries(appDiff)) {
|
|
@@ -1400,7 +1566,7 @@ function formatSummary(diff) {
|
|
|
1400
1566
|
} else {
|
|
1401
1567
|
lines.push(` app: ${chalk2.bold(app)}`);
|
|
1402
1568
|
for (const [feature, d] of changes) {
|
|
1403
|
-
const sym = d.status === "missing" ? chalk2.green("+") : chalk2.yellow("~");
|
|
1569
|
+
const sym = d.status === "missing" ? chalk2.green("+") : d.status === "unknown" ? chalk2.red("?") : chalk2.yellow("~");
|
|
1404
1570
|
lines.push(` ${sym} ${feature}: ${formatFeatureSummary(d)}`);
|
|
1405
1571
|
}
|
|
1406
1572
|
}
|
|
@@ -1412,6 +1578,11 @@ function formatSummary(diff) {
|
|
|
1412
1578
|
}
|
|
1413
1579
|
}
|
|
1414
1580
|
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;
|
|
1581
|
+
if (diff.readWarnings.length > 0) {
|
|
1582
|
+
lines.push("");
|
|
1583
|
+
lines.push(chalk2.red(" Could not read some server state \u2014 treated as unknown, not missing:"));
|
|
1584
|
+
for (const w of diff.readWarnings) lines.push(chalk2.red(` - ${w}`));
|
|
1585
|
+
}
|
|
1415
1586
|
lines.push("");
|
|
1416
1587
|
if (total === 0) {
|
|
1417
1588
|
lines.push(chalk2.green(" Everything in sync."));
|
|
@@ -1422,6 +1593,7 @@ function formatSummary(diff) {
|
|
|
1422
1593
|
return lines.join("\n");
|
|
1423
1594
|
}
|
|
1424
1595
|
function formatFeatureSummary(d) {
|
|
1596
|
+
if (d.status === "unknown") return "(could not read server state \u2014 not reported as missing)";
|
|
1425
1597
|
if (d.status === "missing") return "(not set on server)";
|
|
1426
1598
|
if (Array.isArray(d.desired) && Array.isArray(d.current)) {
|
|
1427
1599
|
return `${d.current.length} \u2192 ${d.desired.length} items`;
|
|
@@ -1435,6 +1607,10 @@ function formatVerbose(diff) {
|
|
|
1435
1607
|
if (changes.length === 0) continue;
|
|
1436
1608
|
for (const [feature, d] of changes) {
|
|
1437
1609
|
lines.push(`@@ app: ${app} / ${feature} @@`);
|
|
1610
|
+
if (d.status === "unknown") {
|
|
1611
|
+
lines.push(chalk2.red(` ? could not read server state \u2014 not reported as missing`));
|
|
1612
|
+
continue;
|
|
1613
|
+
}
|
|
1438
1614
|
const currentLines = d.current !== void 0 ? JSON.stringify(d.current, null, 2).split("\n") : [];
|
|
1439
1615
|
const desiredLines = JSON.stringify(d.desired, null, 2).split("\n");
|
|
1440
1616
|
for (const line of currentLines) lines.push(chalk2.red(`- ${line}`));
|
|
@@ -1447,6 +1623,11 @@ function formatVerbose(diff) {
|
|
|
1447
1623
|
lines.push(chalk2.green(`+ ${svc}`));
|
|
1448
1624
|
}
|
|
1449
1625
|
}
|
|
1626
|
+
if (diff.readWarnings.length > 0) {
|
|
1627
|
+
lines.push("");
|
|
1628
|
+
lines.push(chalk2.red("Could not read some server state \u2014 treated as unknown, not missing:"));
|
|
1629
|
+
for (const w of diff.readWarnings) lines.push(chalk2.red(` - ${w}`));
|
|
1630
|
+
}
|
|
1450
1631
|
lines.push("");
|
|
1451
1632
|
return lines.join("\n");
|
|
1452
1633
|
}
|
|
@@ -1502,6 +1683,7 @@ function validate(filePath) {
|
|
|
1502
1683
|
}
|
|
1503
1684
|
|
|
1504
1685
|
// src/index.ts
|
|
1686
|
+
import chalk3 from "chalk";
|
|
1505
1687
|
var require2 = createRequire(import.meta.url);
|
|
1506
1688
|
var { version } = require2("../package.json");
|
|
1507
1689
|
var program = new Command().name("dokku-compose").version(version);
|
|
@@ -1516,7 +1698,7 @@ program.command("up [apps...]").description("Create/update apps and services to
|
|
|
1516
1698
|
const runner = makeRunner(opts);
|
|
1517
1699
|
const ctx = createContext(runner);
|
|
1518
1700
|
try {
|
|
1519
|
-
await runUp(ctx, config, apps);
|
|
1701
|
+
await runUp(ctx, config, apps, { failFast: opts.failFast ?? false });
|
|
1520
1702
|
if (opts.dryRun) {
|
|
1521
1703
|
console.log("\n# Commands that would run:");
|
|
1522
1704
|
for (const cmd of runner.dryRunLog) {
|
|
@@ -1524,11 +1706,15 @@ program.command("up [apps...]").description("Create/update apps and services to
|
|
|
1524
1706
|
console.log(`dokku ${line}`);
|
|
1525
1707
|
}
|
|
1526
1708
|
}
|
|
1709
|
+
} catch (error) {
|
|
1710
|
+
console.error(chalk3.red(`
|
|
1711
|
+
${error.message}`));
|
|
1712
|
+
process.exitCode = 1;
|
|
1527
1713
|
} finally {
|
|
1528
1714
|
await ctx.close();
|
|
1529
1715
|
}
|
|
1530
1716
|
});
|
|
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) => {
|
|
1717
|
+
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
1718
|
if (!opts.force) {
|
|
1533
1719
|
console.error("--force required");
|
|
1534
1720
|
process.exit(1);
|
|
@@ -1537,7 +1723,11 @@ program.command("down [apps...]").description("Destroy apps and services (requir
|
|
|
1537
1723
|
const runner = makeRunner({});
|
|
1538
1724
|
const ctx = createContext(runner);
|
|
1539
1725
|
try {
|
|
1540
|
-
await runDown(ctx, config, apps, { force: true });
|
|
1726
|
+
await runDown(ctx, config, apps, { force: true, failFast: opts.failFast ?? false });
|
|
1727
|
+
} catch (error) {
|
|
1728
|
+
console.error(chalk3.red(`
|
|
1729
|
+
${error.message}`));
|
|
1730
|
+
process.exitCode = 1;
|
|
1541
1731
|
} finally {
|
|
1542
1732
|
await ctx.close();
|
|
1543
1733
|
}
|
|
@@ -1586,7 +1776,7 @@ program.command("diff").description("Show what is out of sync between config and
|
|
|
1586
1776
|
const diff = opts.sensitive ? rawDiff : maskDiffResult(rawDiff);
|
|
1587
1777
|
const output = opts.verbose ? formatVerbose(diff) : formatSummary(diff);
|
|
1588
1778
|
process.stdout.write(output);
|
|
1589
|
-
process.
|
|
1779
|
+
process.exitCode = rawDiff.inSync ? 0 : 1;
|
|
1590
1780
|
} finally {
|
|
1591
1781
|
await ctx.close();
|
|
1592
1782
|
}
|