dokku-compose 0.13.0 → 0.14.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 +38 -10
- package/dist/chunk-WM244P42.js +89 -0
- package/dist/index.js +1559 -476
- package/dist/{ps-33II4UU3.js → ps-QQAVQTEV.js} +5 -0
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1,8 +1,18 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
assertDokkuSupported,
|
|
4
|
+
compareToFile,
|
|
5
|
+
enforceFileVersion,
|
|
6
|
+
logAction,
|
|
7
|
+
logDone,
|
|
8
|
+
logError,
|
|
9
|
+
logSkip,
|
|
10
|
+
logWarn
|
|
11
|
+
} from "./chunk-WM244P42.js";
|
|
2
12
|
|
|
3
13
|
// src/index.ts
|
|
4
14
|
import { Command } from "commander";
|
|
5
|
-
import * as
|
|
15
|
+
import * as fs4 from "fs";
|
|
6
16
|
import * as yaml3 from "js-yaml";
|
|
7
17
|
import { createRequire } from "module";
|
|
8
18
|
|
|
@@ -12,6 +22,241 @@ import * as yaml from "js-yaml";
|
|
|
12
22
|
|
|
13
23
|
// src/core/schema.ts
|
|
14
24
|
import { z } from "zod";
|
|
25
|
+
|
|
26
|
+
// src/resources/parsers.ts
|
|
27
|
+
function parseReport(raw, namespace) {
|
|
28
|
+
const result = {};
|
|
29
|
+
const prefixPattern = namespace.replace(/-/g, "[\\s-]");
|
|
30
|
+
const prefix = new RegExp(`^${prefixPattern}\\s+`, "i");
|
|
31
|
+
for (const line of raw.split("\n")) {
|
|
32
|
+
if (line.trimStart().startsWith("=====>")) continue;
|
|
33
|
+
const colonIdx = line.indexOf(":");
|
|
34
|
+
if (colonIdx === -1) continue;
|
|
35
|
+
const rawKey = line.slice(0, colonIdx).trim();
|
|
36
|
+
if (!rawKey) continue;
|
|
37
|
+
const value = line.slice(colonIdx + 1).trim();
|
|
38
|
+
const stripped = rawKey.replace(prefix, "");
|
|
39
|
+
const key = stripped.toLowerCase().replace(/\s+/g, "-");
|
|
40
|
+
if (key.startsWith("computed-") || key.startsWith("global-") || key === "last-visited-at") continue;
|
|
41
|
+
if (!value) continue;
|
|
42
|
+
result[key] = value;
|
|
43
|
+
}
|
|
44
|
+
return result;
|
|
45
|
+
}
|
|
46
|
+
function parseBulkReport(raw, namespace, headerNamespace) {
|
|
47
|
+
const result = /* @__PURE__ */ new Map();
|
|
48
|
+
const sections = raw.split(/(?=^=====> )/m).filter((s) => s.trim());
|
|
49
|
+
const nsPattern = (headerNamespace ?? namespace).replace(/-/g, "[\\s-]");
|
|
50
|
+
const headerRe = new RegExp(`^=====> (.+?)\\s+${nsPattern}\\s+information`, "i");
|
|
51
|
+
for (const section of sections) {
|
|
52
|
+
const headerEnd = section.indexOf("\n");
|
|
53
|
+
if (headerEnd === -1) continue;
|
|
54
|
+
const header = section.slice(0, headerEnd);
|
|
55
|
+
const match = header.match(headerRe);
|
|
56
|
+
if (!match) continue;
|
|
57
|
+
const app = match[1];
|
|
58
|
+
result.set(app, parseReport(section, namespace));
|
|
59
|
+
}
|
|
60
|
+
return result;
|
|
61
|
+
}
|
|
62
|
+
function parseJsonReport(raw, scope) {
|
|
63
|
+
let parsed;
|
|
64
|
+
try {
|
|
65
|
+
parsed = JSON.parse(raw);
|
|
66
|
+
} catch {
|
|
67
|
+
throw new Error(`report --format json did not return JSON: ${raw.slice(0, 200)}`);
|
|
68
|
+
}
|
|
69
|
+
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
70
|
+
throw new Error(`report --format json did not return a JSON object: ${raw.slice(0, 200)}`);
|
|
71
|
+
}
|
|
72
|
+
const result = {};
|
|
73
|
+
for (const [key, value] of Object.entries(parsed)) {
|
|
74
|
+
if (value === null || value === void 0 || String(value) === "") continue;
|
|
75
|
+
if (scope === "global") {
|
|
76
|
+
if (key.startsWith("global-")) result[key.slice("global-".length)] = String(value);
|
|
77
|
+
} else if (scope.includes(key)) {
|
|
78
|
+
result[key] = String(value);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
return result;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// src/resources/properties.ts
|
|
85
|
+
async function isAppDeployed(ctx, target) {
|
|
86
|
+
const raw = await ctx.query("ps:report", target, "--deployed");
|
|
87
|
+
return raw.trim() === "true";
|
|
88
|
+
}
|
|
89
|
+
var NGINX_KEYS = [
|
|
90
|
+
"access-log-format",
|
|
91
|
+
"access-log-path",
|
|
92
|
+
"bind-address-ipv4",
|
|
93
|
+
"bind-address-ipv6",
|
|
94
|
+
"client-body-timeout",
|
|
95
|
+
"client-header-timeout",
|
|
96
|
+
"client-max-body-size",
|
|
97
|
+
"disable-custom-config",
|
|
98
|
+
"error-log-path",
|
|
99
|
+
"hsts",
|
|
100
|
+
"hsts-include-subdomains",
|
|
101
|
+
"hsts-max-age",
|
|
102
|
+
"hsts-preload",
|
|
103
|
+
"keepalive-timeout",
|
|
104
|
+
"lingering-timeout",
|
|
105
|
+
"nginx-conf-sigil-path",
|
|
106
|
+
"nginx-service-command",
|
|
107
|
+
"proxy-buffer-size",
|
|
108
|
+
"proxy-buffering",
|
|
109
|
+
"proxy-buffers",
|
|
110
|
+
"proxy-busy-buffers-size",
|
|
111
|
+
"proxy-connect-timeout",
|
|
112
|
+
"proxy-keepalive",
|
|
113
|
+
"proxy-read-timeout",
|
|
114
|
+
"proxy-send-timeout",
|
|
115
|
+
"send-timeout",
|
|
116
|
+
"underscore-in-headers",
|
|
117
|
+
"x-forwarded-for-value",
|
|
118
|
+
"x-forwarded-port-value",
|
|
119
|
+
"x-forwarded-proto-value",
|
|
120
|
+
"x-forwarded-ssl"
|
|
121
|
+
];
|
|
122
|
+
var LOGS_KEYS = ["app-label-alias", "max-size", "vector-cron-sink", "vector-sink"];
|
|
123
|
+
var LOGS_GLOBAL_KEYS = [...LOGS_KEYS, "vector-image", "vector-networks"];
|
|
124
|
+
var REGISTRY_KEYS = [
|
|
125
|
+
"image-repo",
|
|
126
|
+
"image-repo-template",
|
|
127
|
+
"push-extra-tags",
|
|
128
|
+
"push-on-release",
|
|
129
|
+
"server"
|
|
130
|
+
];
|
|
131
|
+
function propertyRemovals(allowed) {
|
|
132
|
+
return (current, desired) => Object.keys(current ?? {}).filter((k) => allowed.includes(k) && !(desired && k in desired));
|
|
133
|
+
}
|
|
134
|
+
function propertyPrune(setCmd, afterPrune = []) {
|
|
135
|
+
return async (ctx, target, removed) => {
|
|
136
|
+
for (const key of removed) await ctx.run(setCmd, target, key);
|
|
137
|
+
for (const cmd of afterPrune) {
|
|
138
|
+
try {
|
|
139
|
+
await ctx.run(cmd, target);
|
|
140
|
+
} catch (err) {
|
|
141
|
+
if (await isAppDeployed(ctx, target)) throw err;
|
|
142
|
+
logWarn(target, `${cmd} failed on an undeployed app \u2014 its proxy config will be built on first deploy`);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
function propertyResource(opts) {
|
|
148
|
+
return {
|
|
149
|
+
key: opts.key,
|
|
150
|
+
// Keys only on the server never make the block "changed"; diff shows them as removals.
|
|
151
|
+
additive: true,
|
|
152
|
+
removals: propertyRemovals(opts.settable),
|
|
153
|
+
prune: propertyPrune(opts.setCmd, opts.afterChange),
|
|
154
|
+
async read(ctx, target) {
|
|
155
|
+
if (opts.json) return parseJsonReport(await ctx.queryStrict(`${opts.namespace}:report`, target, "--format", "json"), opts.settable);
|
|
156
|
+
const raw = await ctx.query(`${opts.namespace}:report`, target);
|
|
157
|
+
return parseReport(raw, opts.namespace);
|
|
158
|
+
},
|
|
159
|
+
...opts.json ? {} : {
|
|
160
|
+
async readAll(ctx) {
|
|
161
|
+
const raw = await ctx.query(`${opts.namespace}:report`);
|
|
162
|
+
return parseBulkReport(raw, opts.namespace);
|
|
163
|
+
}
|
|
164
|
+
},
|
|
165
|
+
async onChange(ctx, target, change, deferFollowUp = false) {
|
|
166
|
+
for (const [key, value] of Object.entries({ ...change.added, ...change.modified })) {
|
|
167
|
+
await ctx.run(opts.setCmd, target, key, String(value));
|
|
168
|
+
}
|
|
169
|
+
if (opts.afterChange && !deferFollowUp) {
|
|
170
|
+
for (const cmd of opts.afterChange) {
|
|
171
|
+
try {
|
|
172
|
+
await ctx.run(cmd, target);
|
|
173
|
+
} catch (err) {
|
|
174
|
+
if (await isAppDeployed(ctx, target)) throw err;
|
|
175
|
+
logWarn(target, `${cmd} failed on an undeployed app \u2014 its proxy config will be built on first deploy`);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
var Nginx = propertyResource({
|
|
183
|
+
key: "nginx",
|
|
184
|
+
namespace: "nginx",
|
|
185
|
+
setCmd: "nginx:set",
|
|
186
|
+
settable: NGINX_KEYS,
|
|
187
|
+
afterChange: ["proxy:build-config"]
|
|
188
|
+
});
|
|
189
|
+
var Logs = propertyResource({
|
|
190
|
+
key: "logs",
|
|
191
|
+
namespace: "logs",
|
|
192
|
+
setCmd: "logs:set",
|
|
193
|
+
settable: LOGS_KEYS,
|
|
194
|
+
json: true
|
|
195
|
+
});
|
|
196
|
+
var Registry = propertyResource({
|
|
197
|
+
key: "registry",
|
|
198
|
+
namespace: "registry",
|
|
199
|
+
setCmd: "registry:set",
|
|
200
|
+
settable: REGISTRY_KEYS
|
|
201
|
+
});
|
|
202
|
+
function schedulerParts(cfg) {
|
|
203
|
+
if (typeof cfg === "string") return { selected: cfg, shell: "" };
|
|
204
|
+
return { selected: cfg?.selected ?? "", shell: cfg?.shell ?? "" };
|
|
205
|
+
}
|
|
206
|
+
function schedulerFromReport(report) {
|
|
207
|
+
const selected = report["selected"] ?? "";
|
|
208
|
+
const shell = report["shell"];
|
|
209
|
+
return shell ? { ...selected ? { selected } : {}, shell } : selected;
|
|
210
|
+
}
|
|
211
|
+
var Scheduler = {
|
|
212
|
+
key: "scheduler",
|
|
213
|
+
// A scheduler setting on the app that the file does not declare.
|
|
214
|
+
removals: (current, desired) => {
|
|
215
|
+
const have = schedulerParts(current);
|
|
216
|
+
const want = schedulerParts(desired);
|
|
217
|
+
const removed = [];
|
|
218
|
+
if (have.selected && !want.selected) removed.push("scheduler");
|
|
219
|
+
if (have.shell && !want.shell) removed.push("shell");
|
|
220
|
+
return removed;
|
|
221
|
+
},
|
|
222
|
+
prune: async (ctx, target, removed) => {
|
|
223
|
+
for (const label of removed) await ctx.run("scheduler:set", target, label === "shell" ? "shell" : "selected");
|
|
224
|
+
},
|
|
225
|
+
inSync: (current, desired) => {
|
|
226
|
+
const have = schedulerParts(current);
|
|
227
|
+
const want = schedulerParts(desired);
|
|
228
|
+
return (!want.selected || want.selected === have.selected) && (!want.shell || want.shell === have.shell);
|
|
229
|
+
},
|
|
230
|
+
async read(ctx, target) {
|
|
231
|
+
const raw = await ctx.query("scheduler:report", target);
|
|
232
|
+
return schedulerFromReport(parseReport(raw, "scheduler"));
|
|
233
|
+
},
|
|
234
|
+
async readAll(ctx) {
|
|
235
|
+
const raw = await ctx.query("scheduler:report");
|
|
236
|
+
const bulk = parseBulkReport(raw, "scheduler");
|
|
237
|
+
const result = /* @__PURE__ */ new Map();
|
|
238
|
+
for (const [app, report] of bulk) result.set(app, schedulerFromReport(report));
|
|
239
|
+
return result;
|
|
240
|
+
},
|
|
241
|
+
async onChange(ctx, target, change) {
|
|
242
|
+
const have = schedulerParts(change.before);
|
|
243
|
+
const want = schedulerParts(change.after);
|
|
244
|
+
if (want.selected && want.selected !== have.selected) await ctx.run("scheduler:set", target, "selected", want.selected);
|
|
245
|
+
if (want.shell && want.shell !== have.shell) await ctx.run("scheduler:set", target, "shell", want.shell);
|
|
246
|
+
}
|
|
247
|
+
};
|
|
248
|
+
|
|
249
|
+
// src/core/schema.ts
|
|
250
|
+
function propertyMap(namespace, keys, value) {
|
|
251
|
+
return z.record(z.string(), value).superRefine((map, ctx) => {
|
|
252
|
+
for (const key of Object.keys(map)) {
|
|
253
|
+
if (!keys.includes(key)) {
|
|
254
|
+
ctx.addIssue({ code: "custom", path: [key], message: `'${key}' is not a ${namespace}:set property (valid: ${keys.join(", ")})` });
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
});
|
|
258
|
+
}
|
|
259
|
+
var StringOrNumber = z.union([z.string(), z.number()]);
|
|
15
260
|
var PortSchema = z.string().regex(
|
|
16
261
|
/^(http|https|tcp|udp):\d+:\d+$/,
|
|
17
262
|
"Port must be scheme:host:container (e.g. http:80:3000)"
|
|
@@ -24,46 +269,79 @@ var ChecksSchema = z.union([
|
|
|
24
269
|
z.literal(false),
|
|
25
270
|
z.object({
|
|
26
271
|
disabled: z.array(z.string()).optional(),
|
|
27
|
-
skipped: z.array(z.string()).optional()
|
|
28
|
-
|
|
272
|
+
skipped: z.array(z.string()).optional(),
|
|
273
|
+
// The only `checks:set` key (Dokku 0.38.31); anything else is refused.
|
|
274
|
+
"wait-to-retire": z.union([z.string(), z.number(), z.boolean()]).optional()
|
|
275
|
+
}).strict()
|
|
29
276
|
]);
|
|
30
277
|
var GitSchema = z.object({
|
|
31
|
-
deploy_branch: z.string().optional()
|
|
32
|
-
|
|
278
|
+
deploy_branch: z.string().optional(),
|
|
279
|
+
keep_git_dir: z.union([z.string(), z.boolean()]).optional(),
|
|
280
|
+
rev_env_var: z.string().optional(),
|
|
281
|
+
source_image: z.string().optional(),
|
|
282
|
+
// Dokku reads `archive-max-files` and `archive-max-size` only from the global setting
|
|
283
|
+
// (`git:set --global`), which dokku-compose does not manage yet; a per-app value never applies.
|
|
284
|
+
archive_max_files: z.any().refine((v) => v === void 0, { message: "archive_max_files is read only from the global git setting, which dokku-compose does not manage; run `dokku git:set --global archive-max-files <n>`" }),
|
|
285
|
+
archive_max_size: z.any().refine((v) => v === void 0, { message: "archive_max_size is read only from the global git setting, which dokku-compose does not manage; run `dokku git:set --global archive-max-size <n>`" })
|
|
286
|
+
}).strict();
|
|
287
|
+
var LinkSchema = z.union([
|
|
288
|
+
z.string(),
|
|
289
|
+
z.object({
|
|
290
|
+
service: z.string(),
|
|
291
|
+
alias: z.string().min(1).optional(),
|
|
292
|
+
env_var: z.string().min(1).optional(),
|
|
293
|
+
querystring: z.string().min(1).optional()
|
|
294
|
+
}).strict().refine((l) => !(l.alias && l.env_var), { message: "`alias` and `env_var` cannot be combined" })
|
|
295
|
+
]);
|
|
33
296
|
var AppSchema = z.object({
|
|
34
297
|
domains: z.union([z.array(z.string()), z.literal(false)]).optional(),
|
|
35
|
-
links: z.array(
|
|
298
|
+
links: z.array(LinkSchema).optional(),
|
|
36
299
|
ports: z.array(PortSchema).optional(),
|
|
37
300
|
env: EnvMapSchema.optional(),
|
|
301
|
+
/** App env keys something else sets: names only; `diff` stops noting them. */
|
|
302
|
+
env_external: z.array(z.string().min(1)).optional(),
|
|
38
303
|
ssl: z.union([
|
|
39
304
|
z.literal(false),
|
|
40
305
|
z.literal(true),
|
|
41
306
|
z.object({ certfile: z.string(), keyfile: z.string() })
|
|
42
307
|
]).optional(),
|
|
43
308
|
storage: z.array(z.string()).optional(),
|
|
44
|
-
proxy: z.object({
|
|
309
|
+
proxy: z.object({
|
|
310
|
+
enabled: z.boolean().optional(),
|
|
311
|
+
type: z.string().optional(),
|
|
312
|
+
proxy_port: StringOrNumber.optional(),
|
|
313
|
+
proxy_ssl_port: StringOrNumber.optional()
|
|
314
|
+
}).strict().optional(),
|
|
45
315
|
networks: z.array(z.string()).optional(),
|
|
46
316
|
network: z.object({
|
|
47
317
|
attach_post_create: z.union([z.array(z.string()), z.literal(false)]).optional(),
|
|
48
318
|
initial_network: z.union([z.string(), z.literal(false)]).optional(),
|
|
49
319
|
bind_all_interfaces: z.boolean().optional(),
|
|
320
|
+
static_web_listener: z.union([z.string(), z.literal(false)]).optional(),
|
|
50
321
|
tld: z.union([z.string(), z.literal(false)]).optional()
|
|
51
322
|
}).optional(),
|
|
52
|
-
nginx:
|
|
53
|
-
logs:
|
|
54
|
-
|
|
55
|
-
|
|
323
|
+
nginx: propertyMap("nginx", NGINX_KEYS, StringOrNumber).optional(),
|
|
324
|
+
logs: propertyMap("logs", LOGS_KEYS, StringOrNumber).optional(),
|
|
325
|
+
// `auth-servers` is report-only: it is a registry login, not a `registry:set` key.
|
|
326
|
+
registry: propertyMap("registry", REGISTRY_KEYS, z.union([z.string(), z.boolean()])).optional(),
|
|
327
|
+
scheduler: z.union([z.string(), z.object({ selected: z.string().optional(), shell: z.string().optional() }).strict()]).optional(),
|
|
56
328
|
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
329
|
ps: z.object({
|
|
58
330
|
"procfile-path": z.union([z.string(), z.number()]).optional(),
|
|
59
331
|
"restart-policy": z.union([z.string(), z.number()]).optional(),
|
|
60
|
-
"stop-timeout-seconds": z.union([z.string(), z.number()]).optional()
|
|
332
|
+
"stop-timeout-seconds": z.union([z.string(), z.number()]).optional(),
|
|
333
|
+
"dockerfile-start-cmd": z.union([z.string(), z.number()]).optional(),
|
|
334
|
+
restore: z.union([z.string(), z.boolean()]).optional(),
|
|
335
|
+
"skip-deploy": z.union([z.string(), z.boolean()]).optional(),
|
|
336
|
+
"start-cmd": z.union([z.string(), z.number()]).optional()
|
|
61
337
|
}).strict().optional(),
|
|
62
338
|
checks: ChecksSchema.optional(),
|
|
63
339
|
build: z.object({
|
|
64
340
|
dockerfile: z.string().optional(),
|
|
65
341
|
app_json: z.string().optional(),
|
|
66
342
|
context: z.string().optional(),
|
|
343
|
+
selected: z.string().optional(),
|
|
344
|
+
skip_cleanup: z.union([z.string(), z.boolean()]).optional(),
|
|
67
345
|
args: z.record(z.string(), z.string()).optional()
|
|
68
346
|
}).optional(),
|
|
69
347
|
docker_options: z.object({
|
|
@@ -88,7 +366,8 @@ var ServiceBackupSchema = z.object({
|
|
|
88
366
|
var PostgresSchema = z.object({
|
|
89
367
|
version: z.string().optional(),
|
|
90
368
|
image: z.string().optional(),
|
|
91
|
-
backup
|
|
369
|
+
// false removes the service's backup schedule and credentials; absent leaves them alone
|
|
370
|
+
backup: z.union([ServiceBackupSchema, z.literal(false)]).optional()
|
|
92
371
|
});
|
|
93
372
|
var RedisSchema = z.object({
|
|
94
373
|
version: z.string().optional(),
|
|
@@ -114,8 +393,9 @@ var ConfigSchema = z.object({
|
|
|
114
393
|
apps: z.record(z.string(), AppSchema),
|
|
115
394
|
domains: z.union([z.array(z.string()), z.literal(false)]).optional(),
|
|
116
395
|
env: EnvMapSchema.optional(),
|
|
117
|
-
nginx:
|
|
118
|
-
|
|
396
|
+
nginx: propertyMap("nginx", NGINX_KEYS, StringOrNumber).optional(),
|
|
397
|
+
// The top-level block is also applied with `--global`, which takes two more keys.
|
|
398
|
+
logs: propertyMap("logs", LOGS_GLOBAL_KEYS, StringOrNumber).optional(),
|
|
119
399
|
git: GitSchema.optional()
|
|
120
400
|
});
|
|
121
401
|
function parseConfig(raw) {
|
|
@@ -180,7 +460,7 @@ function maskSensitiveArgs(cmd) {
|
|
|
180
460
|
return `${key}=${maskValue(value)}`;
|
|
181
461
|
});
|
|
182
462
|
masked = masked.replace(
|
|
183
|
-
/^(registry:login\s+(?:--global\s+)?\S+\s+\S+\s+)(\S+)/,
|
|
463
|
+
/^(?!.*--password-stdin)(registry:login\s+(?:--global\s+)?\S+\s+\S+\s+)(\S+)/,
|
|
184
464
|
(_, prefix, password) => `${prefix}${maskValue(password)}`
|
|
185
465
|
);
|
|
186
466
|
masked = masked.replace(
|
|
@@ -205,7 +485,7 @@ function maskSensitiveArgsList(args) {
|
|
|
205
485
|
masked[i] = `${m[1]}=${maskValue(m[2])}`;
|
|
206
486
|
}
|
|
207
487
|
}
|
|
208
|
-
if (masked[0] === "registry:login") {
|
|
488
|
+
if (masked[0] === "registry:login" && !masked.includes("--password-stdin")) {
|
|
209
489
|
mark(masked.length - 1);
|
|
210
490
|
}
|
|
211
491
|
if (/^\S+:backup-auth$/.test(masked[0] ?? "")) {
|
|
@@ -229,7 +509,7 @@ function maskSensitiveData(data) {
|
|
|
229
509
|
if (typeof data === "object") {
|
|
230
510
|
const result = {};
|
|
231
511
|
for (const [key, value] of Object.entries(data)) {
|
|
232
|
-
if (typeof value === "string" && SENSITIVE_RE.test(key)) {
|
|
512
|
+
if (typeof value === "string" && (SENSITIVE_RE.test(key) || /sink/i.test(key))) {
|
|
233
513
|
result[key] = maskValue(value);
|
|
234
514
|
} else if (typeof value === "object" && value !== null) {
|
|
235
515
|
result[key] = maskSensitiveData(value);
|
|
@@ -241,6 +521,13 @@ function maskSensitiveData(data) {
|
|
|
241
521
|
}
|
|
242
522
|
return data;
|
|
243
523
|
}
|
|
524
|
+
function maskQuerystring(qs) {
|
|
525
|
+
return qs.split("&").map((pair) => {
|
|
526
|
+
const i = pair.indexOf("=");
|
|
527
|
+
if (i === -1 || !SENSITIVE_RE.test(pair.slice(0, i))) return pair;
|
|
528
|
+
return `${pair.slice(0, i)}=${maskValue(pair.slice(i + 1))}`;
|
|
529
|
+
}).join("&");
|
|
530
|
+
}
|
|
244
531
|
|
|
245
532
|
// src/core/dokku.ts
|
|
246
533
|
var DokkuCommandError = class extends Error {
|
|
@@ -265,17 +552,18 @@ function createRunner(opts = {}) {
|
|
|
265
552
|
if (/^[A-Za-z0-9_\-:.,/=@]+$/.test(arg)) return arg;
|
|
266
553
|
return `'${arg.replace(/'/g, "'\\''")}'`;
|
|
267
554
|
}
|
|
268
|
-
async function execDokku(args) {
|
|
555
|
+
async function execDokku(args, input) {
|
|
556
|
+
const stdio = input === void 0 ? { stdin: "ignore" } : { input };
|
|
269
557
|
if (opts.host) {
|
|
270
558
|
try {
|
|
271
|
-
const result = await execa("ssh", [...sshControlFlags, `dokku@${opts.host}`, "--", ...args.map(shellQuote)],
|
|
559
|
+
const result = await execa("ssh", [...sshControlFlags, `dokku@${opts.host}`, "--", ...args.map(shellQuote)], stdio);
|
|
272
560
|
return { stdout: String(result.stdout ?? ""), stderr: String(result.stderr ?? ""), exitCode: result.exitCode ?? 0, ok: true };
|
|
273
561
|
} catch (e) {
|
|
274
562
|
return { stdout: String(e.stdout ?? ""), stderr: String(e.stderr ?? ""), exitCode: e.exitCode ?? 1, ok: false };
|
|
275
563
|
}
|
|
276
564
|
} else {
|
|
277
565
|
try {
|
|
278
|
-
const result = await execa("dokku", args,
|
|
566
|
+
const result = await execa("dokku", args, stdio);
|
|
279
567
|
return { stdout: String(result.stdout ?? ""), stderr: String(result.stderr ?? ""), exitCode: result.exitCode ?? 0, ok: true };
|
|
280
568
|
} catch (e) {
|
|
281
569
|
return { stdout: String(e.stdout ?? ""), stderr: String(e.stderr ?? ""), exitCode: e.exitCode ?? 1, ok: false };
|
|
@@ -307,6 +595,18 @@ function createRunner(opts = {}) {
|
|
|
307
595
|
const { ok } = await execDokku(args);
|
|
308
596
|
return ok;
|
|
309
597
|
},
|
|
598
|
+
async status(args, input) {
|
|
599
|
+
const { exitCode, ok } = await execDokku(args, input);
|
|
600
|
+
return ok ? 0 : exitCode;
|
|
601
|
+
},
|
|
602
|
+
async runWithInput(input, ...args) {
|
|
603
|
+
if (opts.dryRun) {
|
|
604
|
+
log.push(args.map((a) => a.includes(" ") ? shellQuote(a) : a).join(" "));
|
|
605
|
+
return;
|
|
606
|
+
}
|
|
607
|
+
const { ok, exitCode, stderr } = await execDokku(args, input);
|
|
608
|
+
if (!ok) throw new DokkuCommandError(args, exitCode, input ? stderr.split(input).join("****") : stderr);
|
|
609
|
+
},
|
|
310
610
|
async close() {
|
|
311
611
|
if (!opts.host || !controlPath) return;
|
|
312
612
|
try {
|
|
@@ -321,9 +621,23 @@ function createRunner(opts = {}) {
|
|
|
321
621
|
function createContext(runner) {
|
|
322
622
|
const cache = /* @__PURE__ */ new Map();
|
|
323
623
|
const commands = [];
|
|
624
|
+
const restarts = /* @__PURE__ */ new Map();
|
|
625
|
+
const rebuilds = /* @__PURE__ */ new Map();
|
|
324
626
|
return {
|
|
325
627
|
commands,
|
|
326
628
|
runner,
|
|
629
|
+
restarts,
|
|
630
|
+
rebuilds,
|
|
631
|
+
needsRebuild(app, reason) {
|
|
632
|
+
const reasons = rebuilds.get(app) ?? [];
|
|
633
|
+
if (!reasons.includes(reason)) reasons.push(reason);
|
|
634
|
+
rebuilds.set(app, reasons);
|
|
635
|
+
},
|
|
636
|
+
needsRestart(app, reason) {
|
|
637
|
+
const reasons = restarts.get(app) ?? [];
|
|
638
|
+
if (!reasons.includes(reason)) reasons.push(reason);
|
|
639
|
+
restarts.set(app, reasons);
|
|
640
|
+
},
|
|
327
641
|
query(...args) {
|
|
328
642
|
const key = args.join("\0");
|
|
329
643
|
if (!cache.has(key)) {
|
|
@@ -337,6 +651,13 @@ function createContext(runner) {
|
|
|
337
651
|
check(...args) {
|
|
338
652
|
return runner.check(...args);
|
|
339
653
|
},
|
|
654
|
+
status(args, input) {
|
|
655
|
+
return runner.status(args, input);
|
|
656
|
+
},
|
|
657
|
+
async runWithInput(input, ...args) {
|
|
658
|
+
commands.push(args);
|
|
659
|
+
await runner.runWithInput(input, ...args);
|
|
660
|
+
},
|
|
340
661
|
async run(...args) {
|
|
341
662
|
commands.push(args);
|
|
342
663
|
await runner.run(...args);
|
|
@@ -348,6 +669,17 @@ function createContext(runner) {
|
|
|
348
669
|
}
|
|
349
670
|
|
|
350
671
|
// src/core/change.ts
|
|
672
|
+
function sameValue(x, y, subset = false) {
|
|
673
|
+
if (x !== null && y !== null && typeof x === "object" && typeof y === "object") {
|
|
674
|
+
if (Array.isArray(x) || Array.isArray(y)) {
|
|
675
|
+
if (!Array.isArray(x) || !Array.isArray(y)) return false;
|
|
676
|
+
return x.length === y.length && x.every((v) => y.some((w) => sameValue(v, w))) && y.every((w) => x.some((v) => sameValue(v, w)));
|
|
677
|
+
}
|
|
678
|
+
const kx = Object.keys(x), ky = Object.keys(y);
|
|
679
|
+
return (subset ? ky : kx).every((k) => k in x && k in y && sameValue(x[k], y[k], subset)) && (subset || kx.length === ky.length);
|
|
680
|
+
}
|
|
681
|
+
return String(x) === String(y);
|
|
682
|
+
}
|
|
351
683
|
function computeChange(before, after) {
|
|
352
684
|
if (before === null || before === void 0 || after === null || after === void 0) {
|
|
353
685
|
return { before, after, changed: before !== after };
|
|
@@ -375,7 +707,7 @@ function computeChange(before, after) {
|
|
|
375
707
|
for (const key of allKeys) {
|
|
376
708
|
if (!(key in b)) added[key] = a[key];
|
|
377
709
|
else if (!(key in a)) removed.push(key);
|
|
378
|
-
else if (
|
|
710
|
+
else if (!sameValue(b[key], a[key])) modified[key] = a[key];
|
|
379
711
|
}
|
|
380
712
|
const changed = Object.keys(added).length > 0 || removed.length > 0 || Object.keys(modified).length > 0;
|
|
381
713
|
return { before, after, changed, added, removed, modified };
|
|
@@ -384,28 +716,13 @@ function computeChange(before, after) {
|
|
|
384
716
|
}
|
|
385
717
|
function computeAdditiveChange(before, after) {
|
|
386
718
|
const change = computeChange(before ?? {}, after);
|
|
719
|
+
const b = before ?? {};
|
|
720
|
+
const a = after;
|
|
721
|
+
for (const k of Object.keys(change.modified)) if (sameValue(b[k], a[k], true)) delete change.modified[k];
|
|
387
722
|
change.changed = Object.keys(change.added).length > 0 || Object.keys(change.modified).length > 0;
|
|
388
723
|
return change;
|
|
389
724
|
}
|
|
390
725
|
|
|
391
|
-
// src/core/logger.ts
|
|
392
|
-
import chalk from "chalk";
|
|
393
|
-
function logAction(context, message) {
|
|
394
|
-
process.stdout.write(chalk.blue(`[${context.padEnd(12)}]`) + ` ${message}`);
|
|
395
|
-
}
|
|
396
|
-
function logDone() {
|
|
397
|
-
console.log(`... ${chalk.green("done")}`);
|
|
398
|
-
}
|
|
399
|
-
function logSkip() {
|
|
400
|
-
console.log(`... ${chalk.yellow("already configured")}`);
|
|
401
|
-
}
|
|
402
|
-
function logError(context, message) {
|
|
403
|
-
console.error(chalk.red(`[${context.padEnd(12)}] ERROR: ${message}`));
|
|
404
|
-
}
|
|
405
|
-
function logWarn(context, message) {
|
|
406
|
-
console.warn(chalk.yellow(`[${context.padEnd(12)}] WARN: ${message}`));
|
|
407
|
-
}
|
|
408
|
-
|
|
409
726
|
// src/core/reconcile.ts
|
|
410
727
|
async function reconcile(resource, ctx, target, desired, onRemovals, prune = false) {
|
|
411
728
|
const canPrune = prune && !!resource.prune && !!resource.removals;
|
|
@@ -428,6 +745,17 @@ async function reconcile(resource, ctx, target, desired, onRemovals, prune = fal
|
|
|
428
745
|
return;
|
|
429
746
|
}
|
|
430
747
|
const before = await resource.read(ctx, target);
|
|
748
|
+
if (resource.inSync) {
|
|
749
|
+
if (before !== void 0 && resource.inSync(before, desired)) {
|
|
750
|
+
logSkip();
|
|
751
|
+
} else {
|
|
752
|
+
await resource.onChange(ctx, target, { before, after: desired, changed: true });
|
|
753
|
+
logDone();
|
|
754
|
+
}
|
|
755
|
+
if (onRemovals && resource.removals) onRemovals(resource.removals(before, desired));
|
|
756
|
+
if (canPrune) await pruneFrom(before);
|
|
757
|
+
return;
|
|
758
|
+
}
|
|
431
759
|
if (onRemovals && resource.removals) onRemovals(resource.removals(before, desired));
|
|
432
760
|
if (canPrune) {
|
|
433
761
|
const change2 = resource.additive ? computeAdditiveChange(before, desired) : computeChange(before, desired);
|
|
@@ -465,50 +793,10 @@ var Apps = {
|
|
|
465
793
|
}
|
|
466
794
|
};
|
|
467
795
|
|
|
468
|
-
// src/resources/parsers.ts
|
|
469
|
-
function parseReport(raw, namespace) {
|
|
470
|
-
const result = {};
|
|
471
|
-
const prefixPattern = namespace.replace(/-/g, "[\\s-]");
|
|
472
|
-
const prefix = new RegExp(`^${prefixPattern}\\s+`, "i");
|
|
473
|
-
for (const line of raw.split("\n")) {
|
|
474
|
-
if (line.trimStart().startsWith("=====>")) continue;
|
|
475
|
-
const colonIdx = line.indexOf(":");
|
|
476
|
-
if (colonIdx === -1) continue;
|
|
477
|
-
const rawKey = line.slice(0, colonIdx).trim();
|
|
478
|
-
if (!rawKey) continue;
|
|
479
|
-
const value = line.slice(colonIdx + 1).trim();
|
|
480
|
-
const stripped = rawKey.replace(prefix, "");
|
|
481
|
-
const key = stripped.toLowerCase().replace(/\s+/g, "-");
|
|
482
|
-
if (key.startsWith("computed-") || key.startsWith("global-") || key === "last-visited-at") continue;
|
|
483
|
-
if (!value) continue;
|
|
484
|
-
result[key] = value;
|
|
485
|
-
}
|
|
486
|
-
return result;
|
|
487
|
-
}
|
|
488
|
-
function parseBulkReport(raw, namespace, headerNamespace) {
|
|
489
|
-
const result = /* @__PURE__ */ new Map();
|
|
490
|
-
const sections = raw.split(/(?=^=====> )/m).filter((s) => s.trim());
|
|
491
|
-
const nsPattern = (headerNamespace ?? namespace).replace(/-/g, "[\\s-]");
|
|
492
|
-
const headerRe = new RegExp(`^=====> (.+?)\\s+${nsPattern}\\s+information`, "i");
|
|
493
|
-
for (const section of sections) {
|
|
494
|
-
const headerEnd = section.indexOf("\n");
|
|
495
|
-
if (headerEnd === -1) continue;
|
|
496
|
-
const header = section.slice(0, headerEnd);
|
|
497
|
-
const match = header.match(headerRe);
|
|
498
|
-
if (!match) continue;
|
|
499
|
-
const app = match[1];
|
|
500
|
-
result.set(app, parseReport(section, namespace));
|
|
501
|
-
}
|
|
502
|
-
return result;
|
|
503
|
-
}
|
|
504
|
-
|
|
505
796
|
// src/resources/lists.ts
|
|
506
797
|
function splitWords(raw) {
|
|
507
798
|
return raw.split(/\s+/).map((s) => s.trim()).filter(Boolean);
|
|
508
799
|
}
|
|
509
|
-
function splitLines(raw) {
|
|
510
|
-
return raw.split("\n").map((s) => s.trim()).filter(Boolean);
|
|
511
|
-
}
|
|
512
800
|
var Ports = {
|
|
513
801
|
key: "ports",
|
|
514
802
|
read: async (ctx, target) => {
|
|
@@ -528,24 +816,53 @@ var Ports = {
|
|
|
528
816
|
await ctx.run("ports:set", target, ...change.after);
|
|
529
817
|
}
|
|
530
818
|
};
|
|
819
|
+
function parseEnabled(raw) {
|
|
820
|
+
const v = raw.trim();
|
|
821
|
+
if (v === "true") return true;
|
|
822
|
+
if (v === "false") return false;
|
|
823
|
+
throw new Error(`unexpected domains:report --domains-app-enabled output: '${v}'`);
|
|
824
|
+
}
|
|
531
825
|
var Domains = {
|
|
532
826
|
key: "domains",
|
|
533
827
|
read: async (ctx, target) => {
|
|
828
|
+
let enabledRaw;
|
|
829
|
+
try {
|
|
830
|
+
enabledRaw = await ctx.queryStrict("domains:report", target, "--domains-app-enabled");
|
|
831
|
+
} catch (err) {
|
|
832
|
+
if (!await ctx.check("apps:exists", target)) return [];
|
|
833
|
+
throw err;
|
|
834
|
+
}
|
|
835
|
+
const enabled = parseEnabled(enabledRaw);
|
|
836
|
+
if (!enabled) return false;
|
|
534
837
|
const raw = await ctx.query("domains:report", target, "--domains-app-vhosts");
|
|
535
|
-
return
|
|
838
|
+
return splitWords(raw);
|
|
536
839
|
},
|
|
537
840
|
readAll: async (ctx) => {
|
|
538
841
|
const raw = await ctx.query("domains:report");
|
|
539
842
|
const bulk = parseBulkReport(raw, "domains");
|
|
540
843
|
const result = /* @__PURE__ */ new Map();
|
|
541
844
|
for (const [app, report] of bulk) {
|
|
542
|
-
|
|
845
|
+
if (report["app-enabled"] === void 0) {
|
|
846
|
+
result.set(app, splitWords(report["app-vhosts"] ?? ""));
|
|
847
|
+
continue;
|
|
848
|
+
}
|
|
849
|
+
result.set(app, parseEnabled(report["app-enabled"]) ? splitWords(report["app-vhosts"] ?? "") : false);
|
|
543
850
|
}
|
|
544
851
|
return result;
|
|
545
852
|
},
|
|
546
|
-
onChange: async (ctx, target, {
|
|
547
|
-
|
|
548
|
-
|
|
853
|
+
onChange: async (ctx, target, { before, after }) => {
|
|
854
|
+
if (after === false) {
|
|
855
|
+
await ctx.run("domains:disable", target);
|
|
856
|
+
return;
|
|
857
|
+
}
|
|
858
|
+
if (Array.isArray(before)) {
|
|
859
|
+
const { added, removed } = computeChange(before, after);
|
|
860
|
+
for (const d of removed) await ctx.run("domains:remove", target, d);
|
|
861
|
+
for (const d of added) await ctx.run("domains:add", target, d);
|
|
862
|
+
return;
|
|
863
|
+
}
|
|
864
|
+
await ctx.run("domains:enable", target);
|
|
865
|
+
await ctx.run("domains:set", target, ...after);
|
|
549
866
|
}
|
|
550
867
|
};
|
|
551
868
|
function parseDeployMounts(raw) {
|
|
@@ -567,146 +884,14 @@ var Storage = {
|
|
|
567
884
|
return result;
|
|
568
885
|
},
|
|
569
886
|
onChange: async (ctx, target, { added, removed }) => {
|
|
570
|
-
for (const m of removed)
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
};
|
|
574
|
-
|
|
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
|
-
}
|
|
887
|
+
for (const m of removed) {
|
|
888
|
+
await ctx.run("storage:unmount", target, m);
|
|
889
|
+
ctx.needsRestart(target, "storage");
|
|
633
890
|
}
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
return {
|
|
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),
|
|
643
|
-
async read(ctx, target) {
|
|
644
|
-
const raw = await ctx.query(`${opts.namespace}:report`, target);
|
|
645
|
-
return parseReport(raw, opts.namespace);
|
|
646
|
-
},
|
|
647
|
-
async readAll(ctx) {
|
|
648
|
-
const raw = await ctx.query(`${opts.namespace}:report`);
|
|
649
|
-
return parseBulkReport(raw, opts.namespace);
|
|
650
|
-
},
|
|
651
|
-
async onChange(ctx, target, change, deferFollowUp = false) {
|
|
652
|
-
for (const [key, value] of Object.entries({ ...change.added, ...change.modified })) {
|
|
653
|
-
await ctx.run(opts.setCmd, target, key, String(value));
|
|
654
|
-
}
|
|
655
|
-
if (opts.afterChange && !deferFollowUp) {
|
|
656
|
-
for (const cmd of opts.afterChange) {
|
|
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
|
-
}
|
|
663
|
-
}
|
|
664
|
-
}
|
|
891
|
+
for (const m of added) {
|
|
892
|
+
await ctx.run("storage:mount", target, m);
|
|
893
|
+
ctx.needsRestart(target, "storage");
|
|
665
894
|
}
|
|
666
|
-
};
|
|
667
|
-
}
|
|
668
|
-
var Nginx = propertyResource({
|
|
669
|
-
key: "nginx",
|
|
670
|
-
namespace: "nginx",
|
|
671
|
-
setCmd: "nginx:set",
|
|
672
|
-
settable: NGINX_KEYS,
|
|
673
|
-
afterChange: ["proxy:build-config"]
|
|
674
|
-
});
|
|
675
|
-
var Logs = propertyResource({
|
|
676
|
-
key: "logs",
|
|
677
|
-
namespace: "logs",
|
|
678
|
-
setCmd: "logs:set",
|
|
679
|
-
settable: LOGS_KEYS
|
|
680
|
-
});
|
|
681
|
-
var Registry = propertyResource({
|
|
682
|
-
key: "registry",
|
|
683
|
-
namespace: "registry",
|
|
684
|
-
setCmd: "registry:set",
|
|
685
|
-
settable: REGISTRY_KEYS
|
|
686
|
-
});
|
|
687
|
-
var Scheduler = {
|
|
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
|
-
},
|
|
694
|
-
async read(ctx, target) {
|
|
695
|
-
const raw = await ctx.query("scheduler:report", target);
|
|
696
|
-
const report = parseReport(raw, "scheduler");
|
|
697
|
-
return report["selected"] ?? "";
|
|
698
|
-
},
|
|
699
|
-
async readAll(ctx) {
|
|
700
|
-
const raw = await ctx.query("scheduler:report");
|
|
701
|
-
const bulk = parseBulkReport(raw, "scheduler");
|
|
702
|
-
const result = /* @__PURE__ */ new Map();
|
|
703
|
-
for (const [app, report] of bulk) {
|
|
704
|
-
result.set(app, report["selected"] ?? "");
|
|
705
|
-
}
|
|
706
|
-
return result;
|
|
707
|
-
},
|
|
708
|
-
async onChange(ctx, target, change) {
|
|
709
|
-
await ctx.run("scheduler:set", target, "selected", change.after);
|
|
710
895
|
}
|
|
711
896
|
};
|
|
712
897
|
|
|
@@ -730,6 +915,40 @@ var Proxy = {
|
|
|
730
915
|
await ctx.run(after ? "proxy:enable" : "proxy:disable", target);
|
|
731
916
|
}
|
|
732
917
|
};
|
|
918
|
+
var PROXY_PROP_KEYS = ["type", "proxy_port", "proxy_ssl_port"];
|
|
919
|
+
function proxyPropsFromReport(report) {
|
|
920
|
+
const config = {};
|
|
921
|
+
if (report["type"]) config.type = report["type"];
|
|
922
|
+
if (report["proxy-port"]) config.proxy_port = report["proxy-port"];
|
|
923
|
+
if (report["proxy-ssl-port"]) config.proxy_ssl_port = report["proxy-ssl-port"];
|
|
924
|
+
return config;
|
|
925
|
+
}
|
|
926
|
+
var ProxyProps = {
|
|
927
|
+
key: "proxy_props",
|
|
928
|
+
additive: true,
|
|
929
|
+
// Dokku writes `proxy-port` and `proxy-ssl-port` itself on every nginx build, so they are
|
|
930
|
+
// compared only when declared and are never removals; only `type` is the user's to remove.
|
|
931
|
+
removals: (current, desired) => ["type"].filter((k) => current?.[k] !== void 0 && !(desired && k in desired)),
|
|
932
|
+
prune: async (ctx, target, removed) => {
|
|
933
|
+
for (const key of removed) await ctx.run("proxy:set", target, key.replaceAll("_", "-"));
|
|
934
|
+
},
|
|
935
|
+
read: async (ctx, target) => proxyPropsFromReport(parseReport(await ctx.query("proxy:report", target), "proxy")),
|
|
936
|
+
readAll: async (ctx) => {
|
|
937
|
+
const bulk = parseBulkReport(await ctx.query("proxy:report"), "proxy");
|
|
938
|
+
return new Map([...bulk].map(([app, report]) => [app, proxyPropsFromReport(report)]));
|
|
939
|
+
},
|
|
940
|
+
onChange: async (ctx, target, change) => {
|
|
941
|
+
for (const [key, value] of Object.entries({ ...change.added, ...change.modified })) {
|
|
942
|
+
await ctx.run("proxy:set", target, key.replaceAll("_", "-"), String(value));
|
|
943
|
+
}
|
|
944
|
+
}
|
|
945
|
+
};
|
|
946
|
+
function proxyProps(proxy) {
|
|
947
|
+
if (!proxy) return void 0;
|
|
948
|
+
const props = {};
|
|
949
|
+
for (const k of PROXY_PROP_KEYS) if (proxy[k] !== void 0) props[k] = proxy[k];
|
|
950
|
+
return Object.keys(props).length > 0 ? props : void 0;
|
|
951
|
+
}
|
|
733
952
|
|
|
734
953
|
// src/core/config-json.ts
|
|
735
954
|
function parseConfigJson(raw, what) {
|
|
@@ -771,6 +990,7 @@ var Config = {
|
|
|
771
990
|
const { added, removed, modified } = change;
|
|
772
991
|
if (removed.length > 0) {
|
|
773
992
|
await ctx.run("config:unset", "--no-restart", target, ...removed);
|
|
993
|
+
ctx.needsRestart(target, "env");
|
|
774
994
|
}
|
|
775
995
|
const allDesiredKeys = Object.keys(change.after);
|
|
776
996
|
if (allDesiredKeys.length === 0) {
|
|
@@ -787,53 +1007,127 @@ var Config = {
|
|
|
787
1007
|
...pairs,
|
|
788
1008
|
`${MANAGED_KEYS_VAR}=${allDesiredKeys.join(",")}`
|
|
789
1009
|
);
|
|
1010
|
+
ctx.needsRestart(target, "env");
|
|
790
1011
|
}
|
|
791
1012
|
}
|
|
792
1013
|
};
|
|
793
1014
|
|
|
794
1015
|
// src/resources/certs.ts
|
|
1016
|
+
import * as fs2 from "fs";
|
|
1017
|
+
var NO_ENDPOINT = "doesn't have an SSL endpoint";
|
|
1018
|
+
function readCertFile(path2) {
|
|
1019
|
+
try {
|
|
1020
|
+
return fs2.readFileSync(path2, "utf8");
|
|
1021
|
+
} catch (err) {
|
|
1022
|
+
throw new Error(`ssl: cannot read certfile '${path2}': ${err instanceof Error ? err.message : String(err)}`);
|
|
1023
|
+
}
|
|
1024
|
+
}
|
|
795
1025
|
var Certs = {
|
|
796
1026
|
key: "ssl",
|
|
797
1027
|
read: async (ctx, target) => {
|
|
798
|
-
const
|
|
799
|
-
if (
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
for (const [app, report] of bulk) {
|
|
807
|
-
const enabled = report["enabled"];
|
|
808
|
-
result.set(app, enabled === void 0 ? void 0 : enabled === "true");
|
|
1028
|
+
const enabled = (await ctx.query("certs:report", target, "--ssl-enabled")).trim();
|
|
1029
|
+
if (enabled === "false") return false;
|
|
1030
|
+
if (enabled !== "true") return void 0;
|
|
1031
|
+
try {
|
|
1032
|
+
return { crt: await ctx.queryStrict("certs:show", target, "crt") };
|
|
1033
|
+
} catch (err) {
|
|
1034
|
+
if (err instanceof DokkuCommandError && err.message.includes(NO_ENDPOINT)) return false;
|
|
1035
|
+
return void 0;
|
|
809
1036
|
}
|
|
810
|
-
|
|
1037
|
+
},
|
|
1038
|
+
inSync: (current, desired) => {
|
|
1039
|
+
const installed = current;
|
|
1040
|
+
if (installed === void 0) return false;
|
|
1041
|
+
if (desired === false) return installed === false;
|
|
1042
|
+
if (desired === true) return installed !== false;
|
|
1043
|
+
if (installed === false || typeof desired !== "object" || !("certfile" in desired)) return false;
|
|
1044
|
+
return installed.crt.trimEnd() === readCertFile(desired.certfile).trimEnd();
|
|
811
1045
|
},
|
|
812
1046
|
onChange: async (ctx, target, { before, after }) => {
|
|
1047
|
+
if (before === void 0) {
|
|
1048
|
+
throw new Error("ssl: could not read the installed certificate; left as is");
|
|
1049
|
+
}
|
|
813
1050
|
if (after === false && before) {
|
|
814
1051
|
await ctx.run("certs:remove", target);
|
|
815
1052
|
}
|
|
816
|
-
if (after && typeof after === "object") {
|
|
817
|
-
await ctx.run("certs:add", target, after.certfile, after.keyfile);
|
|
1053
|
+
if (after && typeof after === "object" && "certfile" in after) {
|
|
1054
|
+
await ctx.run(before ? "certs:update" : "certs:add", target, after.certfile, after.keyfile);
|
|
818
1055
|
}
|
|
819
1056
|
}
|
|
820
1057
|
};
|
|
821
1058
|
|
|
1059
|
+
// src/core/shellwords.ts
|
|
1060
|
+
function shellSplit(input) {
|
|
1061
|
+
const words = [];
|
|
1062
|
+
let cur = "";
|
|
1063
|
+
let inWord = false;
|
|
1064
|
+
let quote = null;
|
|
1065
|
+
for (let i = 0; i < input.length; i++) {
|
|
1066
|
+
const c = input[i];
|
|
1067
|
+
if (quote === "'") {
|
|
1068
|
+
if (c === "'") quote = null;
|
|
1069
|
+
else cur += c;
|
|
1070
|
+
} else if (c === "\\") {
|
|
1071
|
+
if (i + 1 >= input.length) return void 0;
|
|
1072
|
+
cur += input[++i];
|
|
1073
|
+
inWord = true;
|
|
1074
|
+
} else if (quote === '"') {
|
|
1075
|
+
if (c === '"') quote = null;
|
|
1076
|
+
else cur += c;
|
|
1077
|
+
} else if (c === '"' || c === "'") {
|
|
1078
|
+
quote = c;
|
|
1079
|
+
inWord = true;
|
|
1080
|
+
} else if (/\s/.test(c)) {
|
|
1081
|
+
if (inWord) {
|
|
1082
|
+
words.push(cur);
|
|
1083
|
+
cur = "";
|
|
1084
|
+
inWord = false;
|
|
1085
|
+
}
|
|
1086
|
+
} else {
|
|
1087
|
+
cur += c;
|
|
1088
|
+
inWord = true;
|
|
1089
|
+
}
|
|
1090
|
+
}
|
|
1091
|
+
if (quote) return void 0;
|
|
1092
|
+
if (inWord) words.push(cur);
|
|
1093
|
+
return words;
|
|
1094
|
+
}
|
|
1095
|
+
function quoteShellArg(s) {
|
|
1096
|
+
if (s === "") return "''";
|
|
1097
|
+
if (!/[ \t\n"'$`\\*?\[\]<>|&;(){}!#~]/.test(s)) return s;
|
|
1098
|
+
return `'${s.replace(/'/g, `'\\''`)}'`;
|
|
1099
|
+
}
|
|
1100
|
+
|
|
822
1101
|
// src/resources/builder.ts
|
|
823
|
-
function parseBuildOptions(
|
|
1102
|
+
function parseBuildOptions(buildList) {
|
|
824
1103
|
const out = [];
|
|
825
|
-
for (const raw of
|
|
826
|
-
const
|
|
1104
|
+
for (const raw of buildList) {
|
|
1105
|
+
const words = shellSplit(raw) ?? raw.trim().split(/\s+/);
|
|
1106
|
+
if (words[0] !== "--build-arg" || words.length < 2) continue;
|
|
1107
|
+
const m = words.slice(1).join(" ").match(/^([^=\s]+)=?(.*)$/s);
|
|
827
1108
|
if (m) out.push({ key: m[1], value: m[2], raw });
|
|
828
1109
|
}
|
|
829
1110
|
return out;
|
|
830
1111
|
}
|
|
831
|
-
function parseBuildArgs(
|
|
1112
|
+
function parseBuildArgs(buildList) {
|
|
832
1113
|
const args = {};
|
|
833
|
-
for (const o of parseBuildOptions(
|
|
1114
|
+
for (const o of parseBuildOptions(buildList)) args[o.key] = o.value;
|
|
834
1115
|
return args;
|
|
835
1116
|
}
|
|
836
|
-
function
|
|
1117
|
+
async function readBuildList(ctx, target) {
|
|
1118
|
+
const raw = await ctx.query("docker-options:report", target, "--format", "json");
|
|
1119
|
+
if (!raw.trim()) return [];
|
|
1120
|
+
let report;
|
|
1121
|
+
try {
|
|
1122
|
+
report = JSON.parse(raw);
|
|
1123
|
+
} catch {
|
|
1124
|
+
report = void 0;
|
|
1125
|
+
}
|
|
1126
|
+
const list = report?.["build-list"];
|
|
1127
|
+
if (!Array.isArray(list)) throw new Error(`docker-options:report ${target} --format json did not return a build-list (needs Dokku >= 0.38.22)`);
|
|
1128
|
+
return list.map(String);
|
|
1129
|
+
}
|
|
1130
|
+
function buildConfigFromReports(builderReport, dockerfileReport, appJsonReport, buildList) {
|
|
837
1131
|
const config = {};
|
|
838
1132
|
if (dockerfileReport["dockerfile-path"])
|
|
839
1133
|
config.dockerfile = dockerfileReport["dockerfile-path"];
|
|
@@ -841,71 +1135,93 @@ function buildConfigFromReports(builderReport, dockerfileReport, appJsonReport,
|
|
|
841
1135
|
config.app_json = appJsonReport["selected"];
|
|
842
1136
|
if (builderReport["build-dir"])
|
|
843
1137
|
config.context = builderReport["build-dir"];
|
|
844
|
-
|
|
1138
|
+
if (builderReport["selected"])
|
|
1139
|
+
config.selected = builderReport["selected"];
|
|
1140
|
+
if (builderReport["skip-cleanup"])
|
|
1141
|
+
config.skip_cleanup = builderReport["skip-cleanup"];
|
|
1142
|
+
const args = parseBuildArgs(buildList);
|
|
845
1143
|
if (Object.keys(args).length > 0) config.args = args;
|
|
1144
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1145
|
+
const duplicated = /* @__PURE__ */ new Set();
|
|
1146
|
+
for (const o of parseBuildOptions(buildList)) (seen.has(o.key) ? duplicated : seen).add(o.key);
|
|
1147
|
+
Object.defineProperty(config, "duplicateArgs", { value: duplicated, enumerable: false });
|
|
846
1148
|
return config;
|
|
847
1149
|
}
|
|
848
1150
|
var Builder = {
|
|
849
1151
|
key: "build",
|
|
850
1152
|
additive: true,
|
|
851
|
-
//
|
|
852
|
-
|
|
853
|
-
//
|
|
1153
|
+
// Additive compare, plus: a declared arg stored more than once is drift until one option is left.
|
|
1154
|
+
inSync: (current, desired) => !computeAdditiveChange(current, desired).changed && !Object.keys(desired.args ?? {}).some((k) => current?.duplicateArgs?.has(k)),
|
|
1155
|
+
// `builder:set` keys and build args (`--build-arg KEY=...` build docker options) not in the file.
|
|
1156
|
+
removals: (current, desired) => [
|
|
1157
|
+
...["selected", "skip_cleanup"].filter((k) => current?.[k] !== void 0 && desired?.[k] === void 0),
|
|
1158
|
+
...Object.keys(current?.args ?? {}).filter((k) => !(desired?.args && k in desired.args)).map((k) => `args.${k}`)
|
|
1159
|
+
],
|
|
1160
|
+
// Reset the stale `builder:set` keys; remove exactly the stale `--build-arg` options, other build options stay.
|
|
854
1161
|
prune: async (ctx, target, removed) => {
|
|
855
|
-
const
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
1162
|
+
for (const key of removed.filter((l) => !l.startsWith("args."))) {
|
|
1163
|
+
await ctx.run("builder:set", target, key.replaceAll("_", "-"));
|
|
1164
|
+
ctx.needsRebuild(target, "build");
|
|
1165
|
+
}
|
|
1166
|
+
const stale = new Set(removed.filter((l) => l.startsWith("args.")).map((l) => l.slice("args.".length)));
|
|
1167
|
+
if (stale.size === 0) return;
|
|
1168
|
+
const options = parseBuildOptions(await readBuildList(ctx, target)).filter((o) => stale.has(o.key));
|
|
1169
|
+
for (const o of options) {
|
|
1170
|
+
await ctx.run("docker-options:remove", target, "build", o.raw);
|
|
1171
|
+
ctx.needsRebuild(target, "build args");
|
|
1172
|
+
}
|
|
859
1173
|
for (const k of stale) {
|
|
860
1174
|
if (!options.some((o) => o.key === k)) logWarn(target, `build arg '${k}' not found as an exact docker option; left as is`);
|
|
861
1175
|
}
|
|
862
1176
|
},
|
|
863
1177
|
async read(ctx, target) {
|
|
864
|
-
const [builderRaw, dockerfileRaw, appJsonRaw,
|
|
1178
|
+
const [builderRaw, dockerfileRaw, appJsonRaw, buildList] = await Promise.all([
|
|
865
1179
|
ctx.query("builder:report", target),
|
|
866
1180
|
ctx.query("builder-dockerfile:report", target),
|
|
867
1181
|
ctx.query("app-json:report", target),
|
|
868
|
-
ctx
|
|
1182
|
+
readBuildList(ctx, target)
|
|
869
1183
|
]);
|
|
870
1184
|
return buildConfigFromReports(
|
|
871
1185
|
parseReport(builderRaw, "builder"),
|
|
872
1186
|
parseReport(dockerfileRaw, "builder-dockerfile"),
|
|
873
1187
|
parseReport(appJsonRaw, "app-json"),
|
|
874
|
-
|
|
1188
|
+
buildList
|
|
875
1189
|
);
|
|
876
1190
|
},
|
|
877
|
-
async
|
|
878
|
-
|
|
879
|
-
ctx.query("builder:report"),
|
|
880
|
-
ctx.query("builder-dockerfile:report"),
|
|
881
|
-
ctx.query("app-json:report"),
|
|
882
|
-
ctx.query("docker-options:report")
|
|
883
|
-
]);
|
|
884
|
-
const builderBulk = parseBulkReport(builderRaw, "builder");
|
|
885
|
-
const dockerfileBulk = parseBulkReport(dockerfileRaw, "builder-dockerfile");
|
|
886
|
-
const appJsonBulk = parseBulkReport(appJsonRaw, "app-json");
|
|
887
|
-
const dockerOptsBulk = parseBulkReport(dockerOptsRaw, "docker-options");
|
|
888
|
-
const result = /* @__PURE__ */ new Map();
|
|
889
|
-
for (const app of builderBulk.keys()) {
|
|
890
|
-
result.set(app, buildConfigFromReports(
|
|
891
|
-
builderBulk.get(app) ?? {},
|
|
892
|
-
dockerfileBulk.get(app) ?? {},
|
|
893
|
-
appJsonBulk.get(app) ?? {},
|
|
894
|
-
dockerOptsBulk.get(app)?.["build"] ?? ""
|
|
895
|
-
));
|
|
896
|
-
}
|
|
897
|
-
return result;
|
|
898
|
-
},
|
|
899
|
-
async onChange(ctx, target, { after }) {
|
|
900
|
-
if (after.dockerfile)
|
|
1191
|
+
async onChange(ctx, target, { before, after }) {
|
|
1192
|
+
if (after.dockerfile && after.dockerfile !== before?.dockerfile) {
|
|
901
1193
|
await ctx.run("builder-dockerfile:set", target, "dockerfile-path", after.dockerfile);
|
|
902
|
-
|
|
1194
|
+
ctx.needsRebuild(target, "build");
|
|
1195
|
+
}
|
|
1196
|
+
if (after.app_json && after.app_json !== before?.app_json) {
|
|
903
1197
|
await ctx.run("app-json:set", target, "appjson-path", after.app_json);
|
|
904
|
-
|
|
1198
|
+
ctx.needsRebuild(target, "build");
|
|
1199
|
+
}
|
|
1200
|
+
if (after.context && after.context !== before?.context) {
|
|
905
1201
|
await ctx.run("builder:set", target, "build-dir", after.context);
|
|
1202
|
+
ctx.needsRebuild(target, "build");
|
|
1203
|
+
}
|
|
1204
|
+
if (after.selected && after.selected !== before?.selected) {
|
|
1205
|
+
await ctx.run("builder:set", target, "selected", after.selected);
|
|
1206
|
+
ctx.needsRebuild(target, "build");
|
|
1207
|
+
}
|
|
1208
|
+
if (after.skip_cleanup !== void 0 && String(after.skip_cleanup) !== String(before?.skip_cleanup))
|
|
1209
|
+
await ctx.run("builder:set", target, "skip-cleanup", String(after.skip_cleanup));
|
|
906
1210
|
if (after.args) {
|
|
1211
|
+
const stored = parseBuildOptions(await readBuildList(ctx, target));
|
|
1212
|
+
const current = parseBuildArgs(stored.map((o) => o.raw));
|
|
1213
|
+
const dup = before?.duplicateArgs;
|
|
1214
|
+
const changedKeys = new Set(Object.keys(after.args).filter((k) => k in current && (current[k] !== after.args[k] || dup?.has(k))));
|
|
1215
|
+
if (changedKeys.size > 0) {
|
|
1216
|
+
for (const o of stored.filter((o2) => changedKeys.has(o2.key))) {
|
|
1217
|
+
await ctx.run("docker-options:remove", target, "build", o.raw);
|
|
1218
|
+
ctx.needsRebuild(target, "build args");
|
|
1219
|
+
}
|
|
1220
|
+
}
|
|
907
1221
|
for (const [key, value] of Object.entries(after.args)) {
|
|
908
|
-
|
|
1222
|
+
if (key in current && !changedKeys.has(key)) continue;
|
|
1223
|
+
await ctx.run("docker-options:add", target, "build", `--build-arg ${quoteShellArg(`${key}=${value}`)}`);
|
|
1224
|
+
ctx.needsRebuild(target, "build args");
|
|
909
1225
|
}
|
|
910
1226
|
}
|
|
911
1227
|
}
|
|
@@ -949,22 +1265,43 @@ var DockerOptions = {
|
|
|
949
1265
|
}
|
|
950
1266
|
return result;
|
|
951
1267
|
},
|
|
1268
|
+
// Dokku: options "only apply to containers created after the options have been
|
|
1269
|
+
// modified" (docs/advanced-usage/docker-options.md, v0.38.31), so deploy and
|
|
1270
|
+
// run options need a restart. Build options are read only by a build, which
|
|
1271
|
+
// a restart does not run, so they need `ps:rebuild`.
|
|
952
1272
|
async onChange(ctx, target, { before, after }) {
|
|
953
1273
|
for (const phase of PHASES) {
|
|
954
1274
|
const prev = new Set(before[phase] ?? []);
|
|
955
1275
|
const desired = new Set(after[phase] ?? []);
|
|
956
1276
|
for (const opt of desired) {
|
|
957
|
-
if (!prev.has(opt))
|
|
1277
|
+
if (!prev.has(opt)) {
|
|
1278
|
+
await ctx.run("docker-options:add", target, phase, opt);
|
|
1279
|
+
if (phase === "build") ctx.needsRebuild(target, "build options");
|
|
1280
|
+
else ctx.needsRestart(target, "docker_options");
|
|
1281
|
+
}
|
|
958
1282
|
}
|
|
959
1283
|
for (const opt of prev) {
|
|
960
|
-
if (!desired.has(opt))
|
|
1284
|
+
if (!desired.has(opt)) {
|
|
1285
|
+
await ctx.run("docker-options:remove", target, phase, opt);
|
|
1286
|
+
if (phase === "build") ctx.needsRebuild(target, "build options");
|
|
1287
|
+
else ctx.needsRestart(target, "docker_options");
|
|
1288
|
+
}
|
|
961
1289
|
}
|
|
962
1290
|
}
|
|
963
1291
|
}
|
|
964
1292
|
};
|
|
965
1293
|
|
|
966
1294
|
// src/resources/ps.ts
|
|
967
|
-
var PS_KEYS = [
|
|
1295
|
+
var PS_KEYS = [
|
|
1296
|
+
"dockerfile-start-cmd",
|
|
1297
|
+
"procfile-path",
|
|
1298
|
+
"restart-policy",
|
|
1299
|
+
"restore",
|
|
1300
|
+
"skip-deploy",
|
|
1301
|
+
"start-cmd",
|
|
1302
|
+
"stop-timeout-seconds"
|
|
1303
|
+
];
|
|
1304
|
+
var DOKKU_MANAGED = /* @__PURE__ */ new Set(["restore"]);
|
|
968
1305
|
function parseScale(raw) {
|
|
969
1306
|
const result = {};
|
|
970
1307
|
let parsed;
|
|
@@ -1011,7 +1348,7 @@ var Scale = {
|
|
|
1011
1348
|
var Ps = {
|
|
1012
1349
|
key: "ps",
|
|
1013
1350
|
additive: true,
|
|
1014
|
-
removals: (current, desired) => PS_KEYS.filter((k) => current?.[k] !== void 0 && !(desired && k in desired)),
|
|
1351
|
+
removals: (current, desired) => PS_KEYS.filter((k) => current?.[k] !== void 0 && !DOKKU_MANAGED.has(k) && !(desired && k in desired)),
|
|
1015
1352
|
prune: async (ctx, target, removed) => {
|
|
1016
1353
|
for (const key of removed) await ctx.run("ps:set", target, key);
|
|
1017
1354
|
},
|
|
@@ -1026,24 +1363,35 @@ var Ps = {
|
|
|
1026
1363
|
};
|
|
1027
1364
|
|
|
1028
1365
|
// src/resources/git.ts
|
|
1366
|
+
var toDokku = (key) => key.replaceAll("_", "-");
|
|
1367
|
+
var NOT_REMOVABLE = /* @__PURE__ */ new Set(["deploy_branch", "source_image", "rev_env_var"]);
|
|
1368
|
+
function fromReport(report) {
|
|
1369
|
+
const config = {};
|
|
1370
|
+
if (report["deploy-branch"]) config.deploy_branch = report["deploy-branch"];
|
|
1371
|
+
if (report["keep-git-dir"]) config.keep_git_dir = report["keep-git-dir"];
|
|
1372
|
+
if (report["rev-env-var"]) config.rev_env_var = report["rev-env-var"];
|
|
1373
|
+
if (report["source-image"]) config.source_image = report["source-image"];
|
|
1374
|
+
return config;
|
|
1375
|
+
}
|
|
1029
1376
|
var Git = {
|
|
1030
1377
|
key: "git",
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1378
|
+
additive: true,
|
|
1379
|
+
removals: (current, desired) => Object.keys(current ?? {}).filter((k) => !NOT_REMOVABLE.has(k) && !(desired && k in desired)),
|
|
1380
|
+
prune: async (ctx, target, removed) => {
|
|
1381
|
+
for (const key of removed) await ctx.run("git:set", target, toDokku(key));
|
|
1034
1382
|
},
|
|
1383
|
+
read: async (ctx, target) => fromReport(parseReport(await ctx.query("git:report", target), "git")),
|
|
1035
1384
|
readAll: async (ctx) => {
|
|
1036
|
-
const raw = await ctx.query("git:report");
|
|
1037
|
-
const bulk = parseBulkReport(raw, "git");
|
|
1038
1385
|
const result = /* @__PURE__ */ new Map();
|
|
1039
|
-
for (const
|
|
1040
|
-
|
|
1386
|
+
for (const section of (await ctx.query("git:report")).split(/(?=^=====> )/m)) {
|
|
1387
|
+
const app = section.match(/^=====> (.+?)\s+git\s+information/i)?.[1];
|
|
1388
|
+
if (app) result.set(app, fromReport(parseReport(section, "git")));
|
|
1041
1389
|
}
|
|
1042
1390
|
return result;
|
|
1043
1391
|
},
|
|
1044
|
-
onChange: async (ctx, target,
|
|
1045
|
-
|
|
1046
|
-
await ctx.run("git:set", target,
|
|
1392
|
+
onChange: async (ctx, target, change) => {
|
|
1393
|
+
for (const [key, value] of Object.entries({ ...change.added, ...change.modified })) {
|
|
1394
|
+
await ctx.run("git:set", target, toDokku(key), String(value));
|
|
1047
1395
|
}
|
|
1048
1396
|
}
|
|
1049
1397
|
};
|
|
@@ -1051,11 +1399,12 @@ var Git = {
|
|
|
1051
1399
|
// src/resources/checks.ts
|
|
1052
1400
|
var Checks = {
|
|
1053
1401
|
key: "checks",
|
|
1054
|
-
|
|
1055
|
-
//
|
|
1056
|
-
// Settable state: the per-process lists and `wait-to-retire`. A list of "none" is empty.
|
|
1402
|
+
// Settable state: the per-process lists and `wait-to-retire`. A list of "none" is empty;
|
|
1403
|
+
// a disabled list of `_all_` (`checks:disable <app>` with no types) reads as `false`.
|
|
1057
1404
|
read: async (ctx, target) => {
|
|
1058
1405
|
const report = parseReport(await ctx.query("checks:report", target), "checks");
|
|
1406
|
+
if (report["disabled-list"] === void 0) return void 0;
|
|
1407
|
+
if (report["disabled-list"] === "_all_") return false;
|
|
1059
1408
|
const current = {};
|
|
1060
1409
|
for (const [reportKey, key] of [["disabled-list", "disabled"], ["skipped-list", "skipped"]]) {
|
|
1061
1410
|
const v = report[reportKey];
|
|
@@ -1068,7 +1417,7 @@ var Checks = {
|
|
|
1068
1417
|
prune: async (ctx, target, removed, current, desired) => {
|
|
1069
1418
|
const cur = current;
|
|
1070
1419
|
const split = (v) => (v ?? "").split(",").map((t) => t.trim()).filter(Boolean);
|
|
1071
|
-
const declared = new Set(desired
|
|
1420
|
+
const declared = new Set(desired ? [...desired.disabled ?? [], ...desired.skipped ?? []] : []);
|
|
1072
1421
|
const stale = new Set(removed.filter((k) => k === "disabled" || k === "skipped").flatMap((k) => split(cur[k])));
|
|
1073
1422
|
const enable = [...stale].filter((t) => !declared.has(t));
|
|
1074
1423
|
for (const t of stale) {
|
|
@@ -1076,26 +1425,32 @@ var Checks = {
|
|
|
1076
1425
|
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
1426
|
}
|
|
1078
1427
|
}
|
|
1079
|
-
if (enable.length > 0) await ctx.run("checks:enable", target,
|
|
1428
|
+
if (enable.length > 0) await ctx.run("checks:enable", target, enable.join(","));
|
|
1080
1429
|
if (removed.includes("wait-to-retire")) await ctx.run("checks:set", target, "wait-to-retire");
|
|
1081
1430
|
},
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
}
|
|
1087
|
-
if (after.disabled && after.disabled.length > 0) {
|
|
1088
|
-
await ctx.run("checks:disable", target, ...after.disabled);
|
|
1089
|
-
}
|
|
1090
|
-
if (after.skipped && after.skipped.length > 0) {
|
|
1091
|
-
await ctx.run("checks:skip", target, ...after.skipped);
|
|
1092
|
-
}
|
|
1093
|
-
for (const [key, value] of Object.entries(after)) {
|
|
1094
|
-
if (key === "disabled" || key === "skipped") continue;
|
|
1095
|
-
await ctx.run("checks:set", target, key, String(value));
|
|
1096
|
-
}
|
|
1431
|
+
inSync: (current, desired) => checksCalls(current, desired).length === 0,
|
|
1432
|
+
onChange: async (ctx, target, { before, after }) => {
|
|
1433
|
+
if (before === void 0) throw new Error("checks: could not read checks:report; left as is");
|
|
1434
|
+
for (const call of checksCalls(before, after)) await ctx.run(call[0], target, ...call.slice(1));
|
|
1097
1435
|
}
|
|
1098
1436
|
};
|
|
1437
|
+
function checksCalls(current, desired) {
|
|
1438
|
+
if (desired === false) return current === false ? [] : [["checks:disable"]];
|
|
1439
|
+
const calls = [];
|
|
1440
|
+
const cur = current || {};
|
|
1441
|
+
if (current === false) calls.push(["checks:enable"]);
|
|
1442
|
+
const split = (v) => (v ?? "").split(",").map((t) => t.trim()).filter(Boolean);
|
|
1443
|
+
for (const [key, cmd] of [["disabled", "checks:disable"], ["skipped", "checks:skip"]]) {
|
|
1444
|
+
const have = new Set(split(cur[key]));
|
|
1445
|
+
const missing = (desired[key] ?? []).filter((t) => !have.has(t));
|
|
1446
|
+
if (missing.length > 0) calls.push([cmd, missing.join(",")]);
|
|
1447
|
+
}
|
|
1448
|
+
for (const [key, value] of Object.entries(desired)) {
|
|
1449
|
+
if (key === "disabled" || key === "skipped") continue;
|
|
1450
|
+
if (cur[key] !== String(value)) calls.push(["checks:set", key, String(value)]);
|
|
1451
|
+
}
|
|
1452
|
+
return calls;
|
|
1453
|
+
}
|
|
1099
1454
|
|
|
1100
1455
|
// src/resources/network.ts
|
|
1101
1456
|
var Networks = {
|
|
@@ -1115,6 +1470,7 @@ var Networks = {
|
|
|
1115
1470
|
},
|
|
1116
1471
|
onChange: async (ctx, target, { after }) => {
|
|
1117
1472
|
await ctx.run("network:set", target, "attach-post-deploy", ...after);
|
|
1473
|
+
ctx.needsRebuild(target, "networks");
|
|
1118
1474
|
}
|
|
1119
1475
|
};
|
|
1120
1476
|
var NetworkProps = {
|
|
@@ -1123,7 +1479,11 @@ var NetworkProps = {
|
|
|
1123
1479
|
removals: (current, desired) => Object.keys(current ?? {}).filter((k) => !(desired && k in desired)),
|
|
1124
1480
|
// Dokku's property names are hyphenated; the schema's are underscored.
|
|
1125
1481
|
prune: async (ctx, target, removed) => {
|
|
1126
|
-
for (const key of removed)
|
|
1482
|
+
for (const key of removed) {
|
|
1483
|
+
await ctx.run("network:set", target, key.replaceAll("_", "-"));
|
|
1484
|
+
if (key === "attach_post_create" || key === "initial_network") ctx.needsRebuild(target, "network");
|
|
1485
|
+
else ctx.needsRestart(target, "network");
|
|
1486
|
+
}
|
|
1127
1487
|
},
|
|
1128
1488
|
read: async (ctx, target) => {
|
|
1129
1489
|
const raw = await ctx.query("network:report", target);
|
|
@@ -1134,6 +1494,7 @@ var NetworkProps = {
|
|
|
1134
1494
|
}
|
|
1135
1495
|
if (report["initial-network"]) config.initial_network = report["initial-network"];
|
|
1136
1496
|
if (report["bind-all-interfaces"] === "true") config.bind_all_interfaces = true;
|
|
1497
|
+
if (report["static-web-listener"]) config.static_web_listener = report["static-web-listener"];
|
|
1137
1498
|
if (report["tld"]) config.tld = report["tld"];
|
|
1138
1499
|
return config;
|
|
1139
1500
|
},
|
|
@@ -1148,6 +1509,7 @@ var NetworkProps = {
|
|
|
1148
1509
|
}
|
|
1149
1510
|
if (report["initial-network"]) config.initial_network = report["initial-network"];
|
|
1150
1511
|
if (report["bind-all-interfaces"] === "true") config.bind_all_interfaces = true;
|
|
1512
|
+
if (report["static-web-listener"]) config.static_web_listener = report["static-web-listener"];
|
|
1151
1513
|
if (report["tld"]) config.tld = report["tld"];
|
|
1152
1514
|
result.set(app, config);
|
|
1153
1515
|
}
|
|
@@ -1157,15 +1519,22 @@ var NetworkProps = {
|
|
|
1157
1519
|
if (after.attach_post_create !== void 0 && after.attach_post_create !== false) {
|
|
1158
1520
|
const nets = Array.isArray(after.attach_post_create) ? after.attach_post_create : [after.attach_post_create];
|
|
1159
1521
|
await ctx.run("network:set", target, "attach-post-create", ...nets);
|
|
1522
|
+
ctx.needsRebuild(target, "network");
|
|
1160
1523
|
}
|
|
1161
1524
|
if (after.initial_network !== void 0 && after.initial_network !== false) {
|
|
1162
1525
|
await ctx.run("network:set", target, "initial-network", after.initial_network);
|
|
1526
|
+
ctx.needsRebuild(target, "network");
|
|
1163
1527
|
}
|
|
1164
1528
|
if (after.bind_all_interfaces !== void 0) {
|
|
1165
1529
|
await ctx.run("network:set", target, "bind-all-interfaces", String(after.bind_all_interfaces));
|
|
1530
|
+
ctx.needsRestart(target, "network");
|
|
1531
|
+
}
|
|
1532
|
+
if (after.static_web_listener !== void 0 && after.static_web_listener !== false) {
|
|
1533
|
+
await ctx.run("network:set", target, "static-web-listener", after.static_web_listener);
|
|
1166
1534
|
}
|
|
1167
1535
|
if (after.tld !== void 0 && after.tld !== false) {
|
|
1168
1536
|
await ctx.run("network:set", target, "tld", after.tld);
|
|
1537
|
+
ctx.needsRestart(target, "network");
|
|
1169
1538
|
}
|
|
1170
1539
|
}
|
|
1171
1540
|
};
|
|
@@ -1259,13 +1628,61 @@ async function ensurePlugins(ctx, plugins) {
|
|
|
1259
1628
|
}
|
|
1260
1629
|
|
|
1261
1630
|
// src/modules/docker-auth.ts
|
|
1631
|
+
var AUTH_MATCH = 0;
|
|
1632
|
+
var AUTH_MISSING = 1;
|
|
1633
|
+
var AUTH_DIFFERS = 2;
|
|
1634
|
+
var AUTH_INVALID_ARGUMENTS = 3;
|
|
1635
|
+
var AUTH_UNKNOWN = 4;
|
|
1636
|
+
function registryHostname(server) {
|
|
1637
|
+
const normalized = server === "hub.docker.com" || server === "docker.com" ? "docker.io" : server;
|
|
1638
|
+
const host = normalized.replace(/^https?:\/\//, "").split("/")[0];
|
|
1639
|
+
return ["", "docker.io", "index.docker.io"].includes(host) ? "docker.io" : host;
|
|
1640
|
+
}
|
|
1641
|
+
async function loginMatches(ctx, server, creds) {
|
|
1642
|
+
const code = await ctx.status(
|
|
1643
|
+
["registry:auth-status", "--global", "--password-stdin", server, creds.username],
|
|
1644
|
+
creds.password
|
|
1645
|
+
);
|
|
1646
|
+
if (code === AUTH_MATCH) return true;
|
|
1647
|
+
if (code === AUTH_MISSING || code === AUTH_DIFFERS || code === AUTH_UNKNOWN) return false;
|
|
1648
|
+
if (code === AUTH_INVALID_ARGUMENTS) {
|
|
1649
|
+
throw new Error(`registry:auth-status rejected the arguments for ${server} (exit 3)`);
|
|
1650
|
+
}
|
|
1651
|
+
throw new Error(`registry:auth-status failed for ${server} (exit ${code})`);
|
|
1652
|
+
}
|
|
1653
|
+
async function globalAuthServers(ctx) {
|
|
1654
|
+
const raw = await ctx.query("registry:report", "--global", "--registry-global-auth-servers");
|
|
1655
|
+
return raw.split(",").map((s) => s.trim()).filter(Boolean);
|
|
1656
|
+
}
|
|
1262
1657
|
async function ensureDockerAuth(ctx, config) {
|
|
1263
1658
|
for (const [server, creds] of Object.entries(config)) {
|
|
1264
1659
|
logAction("docker-auth", `Logging in to ${server} as ${creds.username}`);
|
|
1265
|
-
await ctx
|
|
1660
|
+
if (await loginMatches(ctx, server, creds)) {
|
|
1661
|
+
logSkip();
|
|
1662
|
+
continue;
|
|
1663
|
+
}
|
|
1664
|
+
await ctx.runWithInput(creds.password, "registry:login", "--global", "--password-stdin", server, creds.username);
|
|
1665
|
+
logDone();
|
|
1666
|
+
}
|
|
1667
|
+
}
|
|
1668
|
+
async function pruneDockerAuth(ctx, config) {
|
|
1669
|
+
const declared = new Set(Object.keys(config).map(registryHostname));
|
|
1670
|
+
for (const server of await globalAuthServers(ctx)) {
|
|
1671
|
+
if (declared.has(server)) continue;
|
|
1672
|
+
logAction("docker-auth", `Logging out of ${server}`);
|
|
1673
|
+
await ctx.run("registry:logout", "--global", server);
|
|
1266
1674
|
logDone();
|
|
1267
1675
|
}
|
|
1268
1676
|
}
|
|
1677
|
+
async function readDockerAuthDrift(ctx, config) {
|
|
1678
|
+
const stale = [];
|
|
1679
|
+
for (const [server, creds] of Object.entries(config)) {
|
|
1680
|
+
if (!await loginMatches(ctx, server, creds)) stale.push(server);
|
|
1681
|
+
}
|
|
1682
|
+
const declared = new Set(Object.keys(config).map(registryHostname));
|
|
1683
|
+
const extra = (await globalAuthServers(ctx)).filter((s) => !declared.has(s));
|
|
1684
|
+
return { stale, extra };
|
|
1685
|
+
}
|
|
1269
1686
|
|
|
1270
1687
|
// src/modules/network.ts
|
|
1271
1688
|
async function ensureNetworks(ctx, networks) {
|
|
@@ -1286,61 +1703,255 @@ async function exportNetworks(ctx) {
|
|
|
1286
1703
|
return output.split("\n").map((s) => s.trim()).filter((s) => s && !s.startsWith("=====>") && !DOCKER_BUILTIN_NETWORKS.has(s));
|
|
1287
1704
|
}
|
|
1288
1705
|
|
|
1289
|
-
// src/modules/
|
|
1706
|
+
// src/modules/service-drift.ts
|
|
1290
1707
|
import { createHash as createHash2 } from "crypto";
|
|
1291
|
-
|
|
1292
|
-
|
|
1708
|
+
var infoReads = /* @__PURE__ */ new WeakMap();
|
|
1709
|
+
function readServiceInfo(ctx, plugin, name) {
|
|
1710
|
+
let reads = infoReads.get(ctx);
|
|
1711
|
+
if (!reads) {
|
|
1712
|
+
reads = /* @__PURE__ */ new Map();
|
|
1713
|
+
infoReads.set(ctx, reads);
|
|
1714
|
+
}
|
|
1715
|
+
const key = `${plugin}\0${name}`;
|
|
1716
|
+
let read = reads.get(key);
|
|
1717
|
+
if (!read) {
|
|
1718
|
+
read = fetchServiceInfo(ctx, plugin, name);
|
|
1719
|
+
read.catch(() => reads.delete(key));
|
|
1720
|
+
reads.set(key, read);
|
|
1721
|
+
}
|
|
1722
|
+
return read;
|
|
1723
|
+
}
|
|
1724
|
+
async function fetchServiceInfo(ctx, plugin, name) {
|
|
1725
|
+
const raw = await ctx.queryStrict(`${plugin}:info`, name, "--format", "json");
|
|
1726
|
+
try {
|
|
1727
|
+
const info = JSON.parse(raw);
|
|
1728
|
+
if (info && typeof info === "object" && !Array.isArray(info)) return info;
|
|
1729
|
+
} catch {
|
|
1730
|
+
}
|
|
1731
|
+
throw new Error(`${plugin}:info ${name} --format json did not return JSON`);
|
|
1732
|
+
}
|
|
1733
|
+
var field = (info, key) => String(info[key] ?? "");
|
|
1734
|
+
function versionMismatches(info, declared) {
|
|
1735
|
+
const out = [];
|
|
1736
|
+
if (declared.version !== void 0 && declared.version !== field(info, "image-version")) {
|
|
1737
|
+
out.push({ field: "version", desired: declared.version, current: field(info, "image-version") });
|
|
1738
|
+
}
|
|
1739
|
+
if (declared.image !== void 0 && declared.image !== field(info, "image")) {
|
|
1740
|
+
out.push({ field: "image", desired: declared.image, current: field(info, "image") });
|
|
1741
|
+
}
|
|
1742
|
+
return out;
|
|
1743
|
+
}
|
|
1744
|
+
async function readServiceMismatches(ctx, plugin, name, declared) {
|
|
1745
|
+
if (declared.version === void 0 && declared.image === void 0) return [];
|
|
1746
|
+
return versionMismatches(await readServiceInfo(ctx, plugin, name), declared);
|
|
1747
|
+
}
|
|
1748
|
+
function backupAuthFingerprint(accessKeyId, secretAccessKey) {
|
|
1749
|
+
const firstLine = (v) => v.trim().split("\n")[0].trim();
|
|
1750
|
+
return createHash2("sha256").update(`${firstLine(accessKeyId)}
|
|
1751
|
+
${firstLine(secretAccessKey)}`).digest("hex");
|
|
1752
|
+
}
|
|
1753
|
+
function hasBackup(info) {
|
|
1754
|
+
return field(info, "backup-schedule") !== "" || field(info, "backup-authenticated") === "true";
|
|
1755
|
+
}
|
|
1756
|
+
function backupMismatches(info, backup) {
|
|
1757
|
+
for (const key of ["backup-authenticated", "backup-auth-fingerprint", "backup-schedule"]) {
|
|
1758
|
+
if (!(key in info)) throw new Error("dokku-postgres too old to report backup state (needs 2.3.1+)");
|
|
1759
|
+
}
|
|
1760
|
+
if (backup === false) {
|
|
1761
|
+
return hasBackup(info) ? [{ field: "backup", desired: "none", current: describeServerBackup(info) }] : [];
|
|
1762
|
+
}
|
|
1763
|
+
const out = [];
|
|
1764
|
+
const { auth } = backup;
|
|
1765
|
+
const authDiffers = [];
|
|
1766
|
+
if (field(info, "backup-auth-fingerprint") !== backupAuthFingerprint(auth.access_key_id, auth.secret_access_key)) {
|
|
1767
|
+
authDiffers.push("credentials");
|
|
1768
|
+
}
|
|
1769
|
+
if (field(info, "backup-default-region") !== auth.region) authDiffers.push("region");
|
|
1770
|
+
if (field(info, "backup-signature-version") !== auth.signature_version) authDiffers.push("signature version");
|
|
1771
|
+
if (field(info, "backup-endpoint-url") !== auth.endpoint) authDiffers.push("endpoint");
|
|
1772
|
+
if (authDiffers.length > 0) {
|
|
1773
|
+
out.push({
|
|
1774
|
+
field: "backup.auth",
|
|
1775
|
+
desired: `${authDiffers.join(", ")} from the file`,
|
|
1776
|
+
current: field(info, "backup-authenticated") === "true" ? "other values" : "not authenticated"
|
|
1777
|
+
});
|
|
1778
|
+
}
|
|
1779
|
+
if (field(info, "backup-schedule") !== backup.schedule) {
|
|
1780
|
+
out.push({ field: "backup.schedule", desired: backup.schedule, current: field(info, "backup-schedule") });
|
|
1781
|
+
}
|
|
1782
|
+
if (field(info, "backup-bucket") !== backup.bucket) {
|
|
1783
|
+
out.push({ field: "backup.bucket", desired: backup.bucket, current: field(info, "backup-bucket") });
|
|
1784
|
+
}
|
|
1785
|
+
return out;
|
|
1293
1786
|
}
|
|
1294
|
-
function
|
|
1295
|
-
|
|
1787
|
+
function describeServerBackup(info) {
|
|
1788
|
+
const schedule = field(info, "backup-schedule");
|
|
1789
|
+
const parts = [];
|
|
1790
|
+
if (schedule) parts.push(`scheduled ${schedule} to ${field(info, "backup-bucket")}`);
|
|
1791
|
+
if (field(info, "backup-authenticated") === "true") parts.push("credentials stored");
|
|
1792
|
+
return parts.join(", ") || "none";
|
|
1296
1793
|
}
|
|
1794
|
+
|
|
1795
|
+
// src/modules/config.ts
|
|
1796
|
+
var MANAGED_KEYS_VAR2 = "DOKKU_COMPOSE_MANAGED_KEYS";
|
|
1797
|
+
var GLOBAL_MANAGED_KEYS_VAR = "DOKKU_COMPOSE_GLOBAL_MANAGED_KEYS";
|
|
1798
|
+
function isReserved(key) {
|
|
1799
|
+
return key === GLOBAL_MANAGED_KEYS_VAR || key === MANAGED_KEYS_VAR2 || key.startsWith(BACKUP_HASH_PREFIX);
|
|
1800
|
+
}
|
|
1801
|
+
var globalEnvReads = /* @__PURE__ */ new WeakMap();
|
|
1802
|
+
function readGlobalEnv(ctx) {
|
|
1803
|
+
let read = globalEnvReads.get(ctx);
|
|
1804
|
+
if (!read) {
|
|
1805
|
+
read = ctx.queryStrict("config:export", "--global", "--format", "json").then((raw) => parseConfigJson(raw, "--global"));
|
|
1806
|
+
read.catch(() => globalEnvReads.delete(ctx));
|
|
1807
|
+
globalEnvReads.set(ctx, read);
|
|
1808
|
+
}
|
|
1809
|
+
return read;
|
|
1810
|
+
}
|
|
1811
|
+
var BACKUP_HASH_PREFIX = "DOKKU_COMPOSE_BACKUP_HASH_";
|
|
1812
|
+
async function planGlobalEnv(ctx, env, read) {
|
|
1813
|
+
const current = read ?? await readGlobalEnv(ctx);
|
|
1814
|
+
const desired = Object.fromEntries(Object.entries(env).map(([k, v]) => [k, String(v)]));
|
|
1815
|
+
const managed = (current[GLOBAL_MANAGED_KEYS_VAR] ?? "").split(",").filter(Boolean);
|
|
1816
|
+
const removed = managed.filter((k) => !(k in desired) && !isReserved(k) && k in current);
|
|
1817
|
+
const toSet = Object.fromEntries(Object.entries(desired).filter(([k, v]) => current[k] !== v));
|
|
1818
|
+
const marker = Object.keys(desired).join(",");
|
|
1819
|
+
const markerOutdated = [...managed].sort().join(",") !== Object.keys(desired).sort().join(",");
|
|
1820
|
+
return { toSet, removed, marker, markerOutdated };
|
|
1821
|
+
}
|
|
1822
|
+
async function ensureGlobalConfig(ctx, env) {
|
|
1823
|
+
if (env === false) return false;
|
|
1824
|
+
const { toSet, removed, marker, markerOutdated } = await planGlobalEnv(ctx, env ?? {});
|
|
1825
|
+
const unset = [...removed];
|
|
1826
|
+
if (markerOutdated && marker === "") unset.push(GLOBAL_MANAGED_KEYS_VAR);
|
|
1827
|
+
if (unset.length > 0) await ctx.run("config:unset", "--global", ...unset);
|
|
1828
|
+
const pairs = Object.entries(toSet).map(([k, v]) => `${k}=${v}`);
|
|
1829
|
+
if (markerOutdated && marker !== "") pairs.push(`${GLOBAL_MANAGED_KEYS_VAR}=${marker}`);
|
|
1830
|
+
if (pairs.length > 0) await ctx.run("config:set", "--global", ...pairs);
|
|
1831
|
+
return Object.keys(toSet).length > 0 || removed.length > 0;
|
|
1832
|
+
}
|
|
1833
|
+
async function readGlobalEnvDrift(ctx, env, current) {
|
|
1834
|
+
const { toSet, removed, markerOutdated } = await planGlobalEnv(ctx, env ?? {}, current);
|
|
1835
|
+
return { changed: Object.keys(toSet), removed, markerOutdated };
|
|
1836
|
+
}
|
|
1837
|
+
|
|
1838
|
+
// src/modules/postgres.ts
|
|
1297
1839
|
async function ensurePostgres(ctx, services) {
|
|
1840
|
+
const created = /* @__PURE__ */ new Set();
|
|
1298
1841
|
for (const [name, config] of Object.entries(services)) {
|
|
1299
1842
|
logAction("services", `Ensuring ${name}`);
|
|
1300
1843
|
const exists = await ctx.check("postgres:exists", name);
|
|
1301
1844
|
if (exists) {
|
|
1302
1845
|
logSkip();
|
|
1846
|
+
try {
|
|
1847
|
+
const mismatches = await readServiceMismatches(ctx, "postgres", name, config);
|
|
1848
|
+
if (mismatches.length > 0) {
|
|
1849
|
+
const parts = mismatches.map((m) => `${m.field} is ${m.current || "(unknown)"} on the server but ${m.desired} in the file`);
|
|
1850
|
+
logWarn("services", `${name}: ${parts.join("; ")}; up does not change it`);
|
|
1851
|
+
}
|
|
1852
|
+
} catch (err) {
|
|
1853
|
+
logWarn("services", `${name}: could not compare version and image: ${err instanceof Error ? err.message : String(err)}`);
|
|
1854
|
+
}
|
|
1303
1855
|
continue;
|
|
1304
1856
|
}
|
|
1305
1857
|
const args = ["postgres:create", name];
|
|
1306
1858
|
if (config.image) args.push("--image", config.image);
|
|
1307
1859
|
if (config.version) args.push("--image-version", config.version);
|
|
1308
1860
|
await ctx.run(...args);
|
|
1861
|
+
created.add(name);
|
|
1309
1862
|
logDone();
|
|
1310
1863
|
}
|
|
1864
|
+
return created;
|
|
1311
1865
|
}
|
|
1312
|
-
async function ensurePostgresBackups(ctx, services) {
|
|
1866
|
+
async function ensurePostgresBackups(ctx, services, opts = {}) {
|
|
1313
1867
|
for (const [name, config] of Object.entries(services)) {
|
|
1314
|
-
|
|
1868
|
+
const backup = config.backup;
|
|
1869
|
+
if (backup === void 0 && !opts.prune) continue;
|
|
1870
|
+
if (backup === void 0) {
|
|
1871
|
+
let info2;
|
|
1872
|
+
try {
|
|
1873
|
+
info2 = await readServiceInfo(ctx, "postgres", name);
|
|
1874
|
+
} catch (err) {
|
|
1875
|
+
logWarn("services", `could not read backup state for ${name}: ${err instanceof Error ? err.message : String(err)}`);
|
|
1876
|
+
continue;
|
|
1877
|
+
}
|
|
1878
|
+
if (hasBackup(info2)) {
|
|
1879
|
+
logWarn("services", `${name}: has a backup the file does not declare (${describeServerBackup(info2)}); --prune leaves it, set backup: false to remove it`);
|
|
1880
|
+
}
|
|
1881
|
+
continue;
|
|
1882
|
+
}
|
|
1883
|
+
if (opts.dryRunCreated?.has(name)) {
|
|
1884
|
+
logAction("services", `Configuring backup for ${name}`);
|
|
1885
|
+
if (backup !== false) {
|
|
1886
|
+
const { schedule: schedule2, bucket: bucket2, auth: auth2 } = backup;
|
|
1887
|
+
await ctx.run(
|
|
1888
|
+
"postgres:backup-auth",
|
|
1889
|
+
name,
|
|
1890
|
+
auth2.access_key_id,
|
|
1891
|
+
auth2.secret_access_key,
|
|
1892
|
+
auth2.region,
|
|
1893
|
+
auth2.signature_version,
|
|
1894
|
+
auth2.endpoint
|
|
1895
|
+
);
|
|
1896
|
+
await ctx.run("postgres:backup-schedule", name, schedule2, bucket2);
|
|
1897
|
+
}
|
|
1898
|
+
logDone();
|
|
1899
|
+
continue;
|
|
1900
|
+
}
|
|
1901
|
+
const info = await readServiceInfo(ctx, "postgres", name);
|
|
1315
1902
|
logAction("services", `Configuring backup for ${name}`);
|
|
1316
|
-
const
|
|
1317
|
-
|
|
1318
|
-
const storedHash = await ctx.query("config:get", "--global", hashKey);
|
|
1319
|
-
if (storedHash === desiredHash) {
|
|
1903
|
+
const fields = new Set(backupMismatches(info, backup).map((m) => m.field));
|
|
1904
|
+
if (fields.size === 0) {
|
|
1320
1905
|
logSkip();
|
|
1321
1906
|
continue;
|
|
1322
1907
|
}
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
"postgres:backup-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1908
|
+
if (backup === false) {
|
|
1909
|
+
if (String(info["backup-schedule"] ?? "") !== "") await ctx.run("postgres:backup-unschedule", name);
|
|
1910
|
+
await ctx.run("postgres:backup-deauth", name);
|
|
1911
|
+
logDone();
|
|
1912
|
+
continue;
|
|
1913
|
+
}
|
|
1914
|
+
const { schedule, bucket, auth } = backup;
|
|
1915
|
+
if (fields.has("backup.auth")) {
|
|
1916
|
+
await ctx.run(
|
|
1917
|
+
"postgres:backup-auth",
|
|
1918
|
+
name,
|
|
1919
|
+
auth.access_key_id,
|
|
1920
|
+
auth.secret_access_key,
|
|
1921
|
+
auth.region,
|
|
1922
|
+
auth.signature_version,
|
|
1923
|
+
auth.endpoint
|
|
1338
1924
|
);
|
|
1339
1925
|
}
|
|
1340
|
-
|
|
1926
|
+
if (fields.has("backup.schedule")) {
|
|
1927
|
+
await ctx.run("postgres:backup-schedule", name, schedule, bucket);
|
|
1928
|
+
const cronOutput = await ctx.query("postgres:backup-schedule-cat", name);
|
|
1929
|
+
if (!parseBackupSchedule(cronOutput)) {
|
|
1930
|
+
throw new Error(`Backup schedule for ${name} was not installed after backup-schedule ran`);
|
|
1931
|
+
}
|
|
1932
|
+
} else if (fields.has("backup.bucket")) {
|
|
1933
|
+
await ctx.run("postgres:set", name, "backup-bucket", bucket);
|
|
1934
|
+
}
|
|
1341
1935
|
logDone();
|
|
1342
1936
|
}
|
|
1343
1937
|
}
|
|
1938
|
+
async function removeBackupHashKeys(ctx) {
|
|
1939
|
+
let env;
|
|
1940
|
+
try {
|
|
1941
|
+
env = await readGlobalEnv(ctx);
|
|
1942
|
+
} catch (err) {
|
|
1943
|
+
logWarn("global", `could not read global env to remove old backup hash keys: ${err instanceof Error ? err.message : String(err)}`);
|
|
1944
|
+
return;
|
|
1945
|
+
}
|
|
1946
|
+
const stale = staleBackupHashKeys(env);
|
|
1947
|
+
if (stale.length === 0) return;
|
|
1948
|
+
logAction("global", `Removing ${stale.length} old backup hash key(s)`);
|
|
1949
|
+
await ctx.run("config:unset", "--global", "--no-restart", ...stale);
|
|
1950
|
+
logDone();
|
|
1951
|
+
}
|
|
1952
|
+
function staleBackupHashKeys(env) {
|
|
1953
|
+
return Object.keys(env).filter((k) => k.startsWith(BACKUP_HASH_PREFIX)).sort();
|
|
1954
|
+
}
|
|
1344
1955
|
async function destroyPostgres(ctx, services) {
|
|
1345
1956
|
for (const [name] of Object.entries(services)) {
|
|
1346
1957
|
logAction("services", `Destroying ${name}`);
|
|
@@ -1413,6 +2024,15 @@ async function ensureRedis(ctx, services) {
|
|
|
1413
2024
|
const exists = await ctx.check("redis:exists", name);
|
|
1414
2025
|
if (exists) {
|
|
1415
2026
|
logSkip();
|
|
2027
|
+
try {
|
|
2028
|
+
const mismatches = await readServiceMismatches(ctx, "redis", name, config);
|
|
2029
|
+
if (mismatches.length > 0) {
|
|
2030
|
+
const parts = mismatches.map((m) => `${m.field} is ${m.current || "(unknown)"} on the server but ${m.desired} in the file`);
|
|
2031
|
+
logWarn("services", `${name}: ${parts.join("; ")}; up does not change it`);
|
|
2032
|
+
}
|
|
2033
|
+
} catch (err) {
|
|
2034
|
+
logWarn("services", `${name}: could not compare version and image: ${err instanceof Error ? err.message : String(err)}`);
|
|
2035
|
+
}
|
|
1416
2036
|
continue;
|
|
1417
2037
|
}
|
|
1418
2038
|
const args = ["redis:create", name];
|
|
@@ -1460,41 +2080,10 @@ async function exportRedis(ctx) {
|
|
|
1460
2080
|
return services;
|
|
1461
2081
|
}
|
|
1462
2082
|
|
|
1463
|
-
// src/modules/version.ts
|
|
1464
|
-
var SEMVER_RE = /^(\d+)\.(\d+)\.(\d+)/;
|
|
1465
|
-
function parseSemver(input) {
|
|
1466
|
-
const m = input.match(SEMVER_RE);
|
|
1467
|
-
if (!m) throw new Error(`Cannot parse version: ${input}`);
|
|
1468
|
-
return [Number(m[1]), Number(m[2]), Number(m[3])];
|
|
1469
|
-
}
|
|
1470
|
-
function extractServerVersion(output) {
|
|
1471
|
-
const match = output.match(/(\d+\.\d+\.\d+)/);
|
|
1472
|
-
return match ? match[1] : null;
|
|
1473
|
-
}
|
|
1474
|
-
function compareSemver(a, b) {
|
|
1475
|
-
const [aMaj, aMin, aPatch] = parseSemver(a);
|
|
1476
|
-
const [bMaj, bMin, bPatch] = parseSemver(b);
|
|
1477
|
-
if (aMaj !== bMaj) return aMaj < bMaj ? -1 : 1;
|
|
1478
|
-
if (aMin !== bMin) return aMin < bMin ? -1 : 1;
|
|
1479
|
-
if (aPatch !== bPatch) return aPatch < bPatch ? -1 : 1;
|
|
1480
|
-
return 0;
|
|
1481
|
-
}
|
|
1482
|
-
async function ensureDokkuVersion(ctx, pinned) {
|
|
1483
|
-
if (!pinned) return;
|
|
1484
|
-
const output = await ctx.query("version");
|
|
1485
|
-
const server = extractServerVersion(output);
|
|
1486
|
-
if (!server) {
|
|
1487
|
-
throw new Error(`Cannot parse Dokku server version from output: ${output}`);
|
|
1488
|
-
}
|
|
1489
|
-
if (compareSemver(server, pinned) === -1) {
|
|
1490
|
-
logWarn(
|
|
1491
|
-
"dokku",
|
|
1492
|
-
`server is v${server} but dokku-compose.yml pins >= v${pinned}. Some features may be unavailable.`
|
|
1493
|
-
);
|
|
1494
|
-
}
|
|
1495
|
-
}
|
|
1496
|
-
|
|
1497
2083
|
// src/modules/links.ts
|
|
2084
|
+
function normalizeLink(entry) {
|
|
2085
|
+
return typeof entry === "string" ? { service: entry } : entry;
|
|
2086
|
+
}
|
|
1498
2087
|
function resolveServicePlugin(name, config) {
|
|
1499
2088
|
if (config.postgres?.[name]) return { plugin: "postgres", config: config.postgres[name] };
|
|
1500
2089
|
if (config.redis?.[name]) return { plugin: "redis", config: config.redis[name] };
|
|
@@ -1506,24 +2095,112 @@ function allServices(config) {
|
|
|
1506
2095
|
for (const name of Object.keys(config.redis ?? {})) entries.push([name, "redis"]);
|
|
1507
2096
|
return entries;
|
|
1508
2097
|
}
|
|
2098
|
+
var DEFAULT_ENV_VAR = { postgres: "DATABASE_URL", redis: "REDIS_URL" };
|
|
2099
|
+
function linkEnvVar(plugin, link) {
|
|
2100
|
+
if (link.env_var) return link.env_var;
|
|
2101
|
+
if (link.alias) return `${link.alias}_URL`;
|
|
2102
|
+
return DEFAULT_ENV_VAR[plugin] ?? `${plugin.toUpperCase()}_URL`;
|
|
2103
|
+
}
|
|
2104
|
+
function linkFlags(link) {
|
|
2105
|
+
const flags = [];
|
|
2106
|
+
if (link.alias) flags.push("--alias", link.alias);
|
|
2107
|
+
if (link.env_var) flags.push("--env-var", link.env_var);
|
|
2108
|
+
if (link.querystring) flags.push("--querystring", link.querystring);
|
|
2109
|
+
return flags;
|
|
2110
|
+
}
|
|
2111
|
+
function readEnv(ctx, app, cache) {
|
|
2112
|
+
let read = cache?.get(app);
|
|
2113
|
+
if (!read) {
|
|
2114
|
+
read = ctx.queryStrict("config:export", "--format", "json", app).then((out) => JSON.parse(out));
|
|
2115
|
+
cache?.set(app, read);
|
|
2116
|
+
}
|
|
2117
|
+
return read;
|
|
2118
|
+
}
|
|
2119
|
+
function findLinkVariables(env, plugin, service) {
|
|
2120
|
+
const host = `dokku-${plugin}-${service}`;
|
|
2121
|
+
const found = [];
|
|
2122
|
+
for (const [name, value] of Object.entries(env)) {
|
|
2123
|
+
if (typeof value !== "string") continue;
|
|
2124
|
+
const m = value.match(/^[a-z][a-z0-9+.-]*:\/\/(?:[^@/?]*@)?([^:/?]+)/i);
|
|
2125
|
+
if (m && m[1] === host) found.push({ name, value });
|
|
2126
|
+
}
|
|
2127
|
+
return found;
|
|
2128
|
+
}
|
|
2129
|
+
function isFallbackName(plugin, name) {
|
|
2130
|
+
return new RegExp(`^DOKKU_${plugin.toUpperCase()}_[A-Z]+_URL$`).test(name);
|
|
2131
|
+
}
|
|
2132
|
+
function querystringOf(url) {
|
|
2133
|
+
const q = url.indexOf("?");
|
|
2134
|
+
return q === -1 ? "" : url.slice(q + 1);
|
|
2135
|
+
}
|
|
2136
|
+
function show(name, qs) {
|
|
2137
|
+
return qs ? `${name}?${maskQuerystring(qs)}` : name;
|
|
2138
|
+
}
|
|
2139
|
+
async function readLinkOptions(ctx, app, plugin, link, cache) {
|
|
2140
|
+
let env;
|
|
2141
|
+
try {
|
|
2142
|
+
env = await readEnv(ctx, app, cache);
|
|
2143
|
+
} catch (err) {
|
|
2144
|
+
return { state: "unreadable", reason: err instanceof Error ? err.message : String(err) };
|
|
2145
|
+
}
|
|
2146
|
+
const found = findLinkVariables(env, plugin, link.service);
|
|
2147
|
+
if (found.length === 0) {
|
|
2148
|
+
return { state: "unreadable", reason: `no variable of '${app}' points at ${link.service}` };
|
|
2149
|
+
}
|
|
2150
|
+
const name = linkEnvVar(plugin, link);
|
|
2151
|
+
const desired = link.querystring ?? "";
|
|
2152
|
+
const plain = !link.alias && !link.env_var;
|
|
2153
|
+
const hit = found.find((v) => v.name === name) ?? (plain ? found.find((v) => isFallbackName(plugin, v.name)) : void 0);
|
|
2154
|
+
if (!hit) return { state: "differs", current: show(found[0].name, querystringOf(found[0].value)), desired: show(name, desired) };
|
|
2155
|
+
const current = querystringOf(hit.value);
|
|
2156
|
+
if (current === desired) return { state: "same" };
|
|
2157
|
+
return { state: "differs", current: show(name, current), desired: show(name, desired) };
|
|
2158
|
+
}
|
|
2159
|
+
async function readServiceLinks(ctx, app, desiredLinks, config) {
|
|
2160
|
+
const desired = new Map(desiredLinks.map(normalizeLink).map((l) => [l.service, l]));
|
|
2161
|
+
const states = [];
|
|
2162
|
+
for (const [service, plugin] of allServices(config)) {
|
|
2163
|
+
states.push({ service, plugin, link: desired.get(service), isLinked: await ctx.check(`${plugin}:linked`, service, app) });
|
|
2164
|
+
}
|
|
2165
|
+
return states;
|
|
2166
|
+
}
|
|
2167
|
+
function servicesToUnlink(states) {
|
|
2168
|
+
return states.filter((s) => !s.link && s.isLinked);
|
|
2169
|
+
}
|
|
1509
2170
|
async function ensureAppLinks(ctx, app, desiredLinks, config) {
|
|
1510
|
-
const
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
const
|
|
1514
|
-
if (
|
|
2171
|
+
const states = await readServiceLinks(ctx, app, desiredLinks, config);
|
|
2172
|
+
const unlink = new Set(servicesToUnlink(states));
|
|
2173
|
+
for (const state of states) {
|
|
2174
|
+
const { service: serviceName, plugin, link, isLinked } = state;
|
|
2175
|
+
if (link && !isLinked) {
|
|
1515
2176
|
logAction(app, `Linking ${serviceName}`);
|
|
1516
|
-
await ctx.run(`${plugin}:link`, serviceName, app, "--no-restart");
|
|
2177
|
+
await ctx.run(`${plugin}:link`, serviceName, app, "--no-restart", ...linkFlags(link));
|
|
2178
|
+
ctx.needsRestart(app, "links");
|
|
1517
2179
|
logDone();
|
|
1518
|
-
} else if (
|
|
2180
|
+
} else if (link && isLinked) {
|
|
2181
|
+
const opts = await readLinkOptions(ctx, app, plugin, link);
|
|
2182
|
+
if (opts.state === "differs") {
|
|
2183
|
+
logAction(app, `Relinking ${serviceName}`);
|
|
2184
|
+
await ctx.run(`${plugin}:unlink`, serviceName, app, "--no-restart");
|
|
2185
|
+
try {
|
|
2186
|
+
await ctx.run(`${plugin}:link`, serviceName, app, "--no-restart", ...linkFlags(link));
|
|
2187
|
+
} catch (err) {
|
|
2188
|
+
logWarn(app, `${app} is now unlinked from ${serviceName}`);
|
|
2189
|
+
throw err;
|
|
2190
|
+
}
|
|
2191
|
+
ctx.needsRestart(app, "links");
|
|
2192
|
+
logDone();
|
|
2193
|
+
}
|
|
2194
|
+
} else if (unlink.has(state)) {
|
|
1519
2195
|
logAction(app, `Unlinking ${serviceName}`);
|
|
1520
2196
|
await ctx.run(`${plugin}:unlink`, serviceName, app, "--no-restart");
|
|
2197
|
+
ctx.needsRestart(app, "links");
|
|
1521
2198
|
logDone();
|
|
1522
2199
|
}
|
|
1523
2200
|
}
|
|
1524
2201
|
}
|
|
1525
2202
|
async function destroyAppLinks(ctx, app, links, config) {
|
|
1526
|
-
for (const serviceName of links) {
|
|
2203
|
+
for (const serviceName of links.map((l) => normalizeLink(l).service)) {
|
|
1527
2204
|
const resolved = resolveServicePlugin(serviceName, config);
|
|
1528
2205
|
if (!resolved) continue;
|
|
1529
2206
|
const isLinked = await ctx.check(`${resolved.plugin}:linked`, serviceName, app);
|
|
@@ -1535,45 +2212,103 @@ async function destroyAppLinks(ctx, app, links, config) {
|
|
|
1535
2212
|
async function exportAppLinks(ctx, app, config) {
|
|
1536
2213
|
const linked = [];
|
|
1537
2214
|
for (const [serviceName, plugin] of allServices(config)) {
|
|
1538
|
-
|
|
1539
|
-
|
|
2215
|
+
if (!await ctx.check(`${plugin}:linked`, serviceName, app)) continue;
|
|
2216
|
+
let found = [];
|
|
2217
|
+
try {
|
|
2218
|
+
found = findLinkVariables(await readEnv(ctx, app), plugin, serviceName);
|
|
2219
|
+
} catch {
|
|
2220
|
+
}
|
|
2221
|
+
const fallback = linkEnvVar(plugin, { service: serviceName });
|
|
2222
|
+
const v = found.find((f) => f.name === fallback) ?? found.find((f) => isFallbackName(plugin, f.name)) ?? found[0];
|
|
2223
|
+
if (!v) {
|
|
2224
|
+
linked.push(serviceName);
|
|
2225
|
+
continue;
|
|
2226
|
+
}
|
|
2227
|
+
const querystring = querystringOf(v.value);
|
|
2228
|
+
if ((v.name === fallback || isFallbackName(plugin, v.name)) && !querystring) {
|
|
2229
|
+
linked.push(serviceName);
|
|
2230
|
+
continue;
|
|
2231
|
+
}
|
|
2232
|
+
const entry = { service: serviceName };
|
|
2233
|
+
if (v.name !== fallback && !isFallbackName(plugin, v.name)) {
|
|
2234
|
+
if (v.name.endsWith("_URL") && v.name.length > 4) entry.alias = v.name.slice(0, -4);
|
|
2235
|
+
else entry.env_var = v.name;
|
|
2236
|
+
}
|
|
2237
|
+
if (querystring) entry.querystring = querystring;
|
|
2238
|
+
linked.push(entry);
|
|
1540
2239
|
}
|
|
1541
2240
|
return linked;
|
|
1542
2241
|
}
|
|
1543
2242
|
|
|
1544
2243
|
// src/modules/domains.ts
|
|
2244
|
+
async function readGlobalDomains(ctx) {
|
|
2245
|
+
const raw = await ctx.queryStrict("domains:report", "--global", "--domains-global-vhosts");
|
|
2246
|
+
return raw.split(/\s+/).filter(Boolean);
|
|
2247
|
+
}
|
|
2248
|
+
function globalDomainsInSync(current, desired) {
|
|
2249
|
+
if (desired === false) return current.length === 0;
|
|
2250
|
+
const sa = new Set(current);
|
|
2251
|
+
const sb = new Set(desired);
|
|
2252
|
+
return sa.size === sb.size && [...sa].every((x) => sb.has(x));
|
|
2253
|
+
}
|
|
1545
2254
|
async function ensureGlobalDomains(ctx, domains) {
|
|
1546
2255
|
logAction("global", "Configuring domains");
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
2256
|
+
const current = await readGlobalDomains(ctx);
|
|
2257
|
+
if (globalDomainsInSync(current, domains)) {
|
|
2258
|
+
logSkip();
|
|
2259
|
+
return;
|
|
1551
2260
|
}
|
|
2261
|
+
if (domains === false) await ctx.run("domains:clear-global");
|
|
2262
|
+
else await ctx.run("domains:set-global", ...domains);
|
|
1552
2263
|
logDone();
|
|
1553
2264
|
}
|
|
1554
2265
|
|
|
1555
|
-
// src/modules/config.ts
|
|
1556
|
-
async function ensureGlobalConfig(ctx, env) {
|
|
1557
|
-
if (env === false) return;
|
|
1558
|
-
const pairs = Object.entries(env).map(([k, v]) => `${k}=${v}`);
|
|
1559
|
-
await ctx.run("config:set", "--global", ...pairs);
|
|
1560
|
-
}
|
|
1561
|
-
|
|
1562
2266
|
// src/modules/logs.ts
|
|
2267
|
+
async function readGlobalLogs(ctx) {
|
|
2268
|
+
return parseJsonReport(await ctx.queryStrict("logs:report", "--global", "--format", "json"), "global");
|
|
2269
|
+
}
|
|
1563
2270
|
async function ensureGlobalLogs(ctx, logs) {
|
|
2271
|
+
logAction("global", "Configuring logs");
|
|
2272
|
+
const current = await readGlobalLogs(ctx);
|
|
2273
|
+
let changed = false;
|
|
1564
2274
|
for (const [key, value] of Object.entries(logs)) {
|
|
2275
|
+
if (current[key] === String(value)) continue;
|
|
1565
2276
|
await ctx.run("logs:set", "--global", key, String(value));
|
|
2277
|
+
changed = true;
|
|
1566
2278
|
}
|
|
2279
|
+
if (changed) logDone();
|
|
2280
|
+
else logSkip();
|
|
1567
2281
|
}
|
|
1568
2282
|
|
|
1569
2283
|
// src/modules/nginx.ts
|
|
2284
|
+
async function readGlobalNginx(ctx) {
|
|
2285
|
+
return parseJsonReport(await ctx.queryStrict("nginx:report", "--global", "--format", "json"), "global");
|
|
2286
|
+
}
|
|
1570
2287
|
async function ensureGlobalNginx(ctx, nginx) {
|
|
2288
|
+
logAction("global", "Configuring nginx");
|
|
2289
|
+
const current = await readGlobalNginx(ctx);
|
|
2290
|
+
let changed = false;
|
|
1571
2291
|
for (const [key, value] of Object.entries(nginx)) {
|
|
2292
|
+
if (current[key] === String(value)) continue;
|
|
1572
2293
|
await ctx.run("nginx:set", "--global", key, String(value));
|
|
2294
|
+
changed = true;
|
|
1573
2295
|
}
|
|
2296
|
+
if (changed) logDone();
|
|
2297
|
+
else logSkip();
|
|
1574
2298
|
}
|
|
1575
2299
|
|
|
1576
2300
|
// src/commands/up.ts
|
|
2301
|
+
function markGlobalEnvRestart(ctx, apps, reason = "global env") {
|
|
2302
|
+
for (const app of apps) ctx.needsRestart(app, reason);
|
|
2303
|
+
}
|
|
2304
|
+
function printRestartSummary(ctx, dryRun = false) {
|
|
2305
|
+
const restarts = [...ctx.restarts].filter(([app]) => !ctx.rebuilds.has(app));
|
|
2306
|
+
if (restarts.length === 0 && ctx.rebuilds.size === 0) return;
|
|
2307
|
+
console.log(`
|
|
2308
|
+
# Apps that ${dryRun ? "would need" : "need"} a restart or rebuild:`);
|
|
2309
|
+
for (const [app, reasons] of restarts) console.log(`dokku ps:restart ${app} # ${reasons.join(", ")}`);
|
|
2310
|
+
for (const [app, reasons] of ctx.rebuilds) console.log(`dokku ps:rebuild ${app} # ${reasons.join(", ")}`);
|
|
2311
|
+
}
|
|
1577
2312
|
var UpFailedError = class extends Error {
|
|
1578
2313
|
constructor(failures) {
|
|
1579
2314
|
super(
|
|
@@ -1609,6 +2344,7 @@ async function processApp(ctx, config, app, appConfig, failFast = false, prune =
|
|
|
1609
2344
|
await step(() => reconcile(Networks, ctx, app, appConfig.networks));
|
|
1610
2345
|
await step(() => reconcile(NetworkProps, ctx, app, appConfig.network, count, prune));
|
|
1611
2346
|
await step(() => reconcile(Proxy, ctx, app, appConfig.proxy?.enabled));
|
|
2347
|
+
await step(() => reconcile(ProxyProps, ctx, app, proxyProps(appConfig.proxy), count, prune));
|
|
1612
2348
|
await step(() => reconcile(Ports, ctx, app, appConfig.ports));
|
|
1613
2349
|
if (config.postgres || config.redis) {
|
|
1614
2350
|
await step(() => ensureAppLinks(ctx, app, appConfig.links ?? [], config));
|
|
@@ -1632,18 +2368,36 @@ async function processApp(ctx, config, app, appConfig, failFast = false, prune =
|
|
|
1632
2368
|
if (errors.length > 0) throw new AppStepsError(errors);
|
|
1633
2369
|
}
|
|
1634
2370
|
async function runUp(ctx, config, appFilter, opts = {}) {
|
|
2371
|
+
try {
|
|
2372
|
+
await runUpPhases(ctx, config, appFilter, opts);
|
|
2373
|
+
} finally {
|
|
2374
|
+
printRestartSummary(ctx, opts.dryRun);
|
|
2375
|
+
}
|
|
2376
|
+
}
|
|
2377
|
+
async function runUpPhases(ctx, config, appFilter, opts) {
|
|
1635
2378
|
const apps = appFilter.length > 0 ? appFilter : Object.keys(config.apps);
|
|
1636
|
-
await
|
|
2379
|
+
enforceFileVersion(await assertDokkuSupported(ctx), config.dokku?.version);
|
|
1637
2380
|
if (config.plugins) await ensurePlugins(ctx, config.plugins);
|
|
1638
|
-
if (config.docker_auth)
|
|
2381
|
+
if (config.docker_auth) {
|
|
2382
|
+
await ensureDockerAuth(ctx, config.docker_auth);
|
|
2383
|
+
if (opts.prune) await pruneDockerAuth(ctx, config.docker_auth);
|
|
2384
|
+
}
|
|
1639
2385
|
if (config.domains !== void 0) await ensureGlobalDomains(ctx, config.domains);
|
|
1640
|
-
if (
|
|
2386
|
+
if (await ensureGlobalConfig(ctx, config.env)) {
|
|
2387
|
+
markGlobalEnvRestart(ctx, apps.filter((app) => config.apps[app]));
|
|
2388
|
+
}
|
|
1641
2389
|
if (config.logs !== void 0) await ensureGlobalLogs(ctx, config.logs);
|
|
1642
2390
|
if (config.nginx !== void 0) await ensureGlobalNginx(ctx, config.nginx);
|
|
1643
2391
|
if (config.networks) await ensureNetworks(ctx, config.networks);
|
|
1644
|
-
|
|
2392
|
+
const createdPostgres = config.postgres ? await ensurePostgres(ctx, config.postgres) : /* @__PURE__ */ new Set();
|
|
1645
2393
|
if (config.redis) await ensureRedis(ctx, config.redis);
|
|
1646
|
-
if (config.postgres)
|
|
2394
|
+
if (config.postgres) {
|
|
2395
|
+
await ensurePostgresBackups(ctx, config.postgres, {
|
|
2396
|
+
prune: opts.prune,
|
|
2397
|
+
dryRunCreated: opts.dryRun ? createdPostgres : void 0
|
|
2398
|
+
});
|
|
2399
|
+
}
|
|
2400
|
+
await removeBackupHashKeys(ctx);
|
|
1647
2401
|
const failures = [];
|
|
1648
2402
|
for (const app of apps) {
|
|
1649
2403
|
const appConfig = config.apps[app];
|
|
@@ -1693,6 +2447,7 @@ var DownFailedError = class extends Error {
|
|
|
1693
2447
|
};
|
|
1694
2448
|
async function runDown(ctx, config, appFilter, opts) {
|
|
1695
2449
|
const apps = appFilter.length > 0 ? appFilter : Object.keys(config.apps);
|
|
2450
|
+
await assertDokkuSupported(ctx);
|
|
1696
2451
|
const failures = [];
|
|
1697
2452
|
for (const app of apps) {
|
|
1698
2453
|
const appConfig = config.apps[app];
|
|
@@ -1734,6 +2489,7 @@ var NETWORKING_RESOURCES = [
|
|
|
1734
2489
|
Networks,
|
|
1735
2490
|
NetworkProps,
|
|
1736
2491
|
Proxy,
|
|
2492
|
+
ProxyProps,
|
|
1737
2493
|
Ports
|
|
1738
2494
|
];
|
|
1739
2495
|
var CONFIG_RESOURCES = [
|
|
@@ -1762,8 +2518,7 @@ var ALL_APP_RESOURCES = [
|
|
|
1762
2518
|
// src/commands/export.ts
|
|
1763
2519
|
async function runExport(ctx, opts) {
|
|
1764
2520
|
const config = { apps: {} };
|
|
1765
|
-
|
|
1766
|
-
if (version2) config.dokku = { version: version2 };
|
|
2521
|
+
config.dokku = { version: await assertDokkuSupported(ctx) };
|
|
1767
2522
|
const apps = opts.appFilter?.length ? opts.appFilter : await exportApps(ctx);
|
|
1768
2523
|
const networks = await exportNetworks(ctx);
|
|
1769
2524
|
if (networks.length > 0) config.networks = networks;
|
|
@@ -1774,7 +2529,10 @@ async function runExport(ctx, opts) {
|
|
|
1774
2529
|
const prefetched = /* @__PURE__ */ new Map();
|
|
1775
2530
|
await Promise.all(
|
|
1776
2531
|
ALL_APP_RESOURCES.filter((r) => !r.forceApply && !r.key.startsWith("_") && r.readAll).map(async (r) => {
|
|
1777
|
-
|
|
2532
|
+
try {
|
|
2533
|
+
prefetched.set(r.key, await r.readAll(ctx));
|
|
2534
|
+
} catch {
|
|
2535
|
+
}
|
|
1778
2536
|
})
|
|
1779
2537
|
);
|
|
1780
2538
|
for (const app of apps) {
|
|
@@ -1782,13 +2540,24 @@ async function runExport(ctx, opts) {
|
|
|
1782
2540
|
for (const resource of ALL_APP_RESOURCES) {
|
|
1783
2541
|
if (resource.key.startsWith("_")) continue;
|
|
1784
2542
|
if (resource.forceApply) continue;
|
|
2543
|
+
if (resource.key === "checks") continue;
|
|
1785
2544
|
const bulk = prefetched.get(resource.key);
|
|
1786
|
-
|
|
2545
|
+
let value;
|
|
2546
|
+
try {
|
|
2547
|
+
value = bulk ? bulk.get(app) : await resource.read(ctx, app);
|
|
2548
|
+
} catch (err) {
|
|
2549
|
+
logWarn(app, `could not read '${resource.key}', left out of the export: ${err instanceof Error ? err.message : String(err)}`);
|
|
2550
|
+
continue;
|
|
2551
|
+
}
|
|
1787
2552
|
if (value === void 0 || value === null || value === "") continue;
|
|
1788
2553
|
if (Array.isArray(value) && value.length === 0) continue;
|
|
1789
2554
|
if (typeof value === "object" && !Array.isArray(value) && Object.keys(value).length === 0) continue;
|
|
1790
|
-
if (resource.key === "
|
|
1791
|
-
appConfig.
|
|
2555
|
+
if (resource.key === "ssl") {
|
|
2556
|
+
appConfig.ssl = value !== false;
|
|
2557
|
+
} else if (resource.key === "proxy") {
|
|
2558
|
+
appConfig.proxy = { ...appConfig.proxy, enabled: value };
|
|
2559
|
+
} else if (resource.key === "proxy_props") {
|
|
2560
|
+
appConfig.proxy = { ...appConfig.proxy, ...value };
|
|
1792
2561
|
} else {
|
|
1793
2562
|
appConfig[resource.key] = value;
|
|
1794
2563
|
}
|
|
@@ -1802,14 +2571,41 @@ async function runExport(ctx, opts) {
|
|
|
1802
2571
|
return config;
|
|
1803
2572
|
}
|
|
1804
2573
|
|
|
2574
|
+
// src/modules/env-notes.ts
|
|
2575
|
+
var MANAGED_KEYS_VAR3 = "DOKKU_COMPOSE_MANAGED_KEYS";
|
|
2576
|
+
var DOKKU_WRITTEN = /* @__PURE__ */ new Set(["GIT_REV", "NO_VHOST"]);
|
|
2577
|
+
var SERVICE_URL_HOST = /^[a-z][a-z0-9+.-]*:\/\/(?:[^@/?]*@)?dokku-[a-z]+-[^:/?]+/i;
|
|
2578
|
+
function undeclaredEnvKeys(input) {
|
|
2579
|
+
const { env } = input;
|
|
2580
|
+
const skip = /* @__PURE__ */ new Set([MANAGED_KEYS_VAR3, ...input.declared, ...input.external]);
|
|
2581
|
+
for (const k of String(env[MANAGED_KEYS_VAR3] ?? "").split(",")) if (k) skip.add(k);
|
|
2582
|
+
for (const link of input.links) {
|
|
2583
|
+
for (const v of findLinkVariables(env, link.plugin, link.service)) skip.add(v.name);
|
|
2584
|
+
}
|
|
2585
|
+
const notes = [];
|
|
2586
|
+
for (const [key, value] of Object.entries(env)) {
|
|
2587
|
+
if (skip.has(key) || DOKKU_WRITTEN.has(key)) continue;
|
|
2588
|
+
if (key.startsWith("DOKKU_") && key !== "DOKKU_START_CMD") continue;
|
|
2589
|
+
if (typeof value === "string" && SERVICE_URL_HOST.test(value)) continue;
|
|
2590
|
+
const duplicate = key in input.globalFile && key in input.globalServer && input.globalServer[key] === String(value);
|
|
2591
|
+
notes.push(duplicate ? { key, label: "duplicate of global" } : { key });
|
|
2592
|
+
}
|
|
2593
|
+
return notes;
|
|
2594
|
+
}
|
|
2595
|
+
|
|
1805
2596
|
// src/commands/diff.ts
|
|
1806
|
-
import
|
|
1807
|
-
function
|
|
1808
|
-
if (
|
|
1809
|
-
|
|
1810
|
-
|
|
1811
|
-
|
|
1812
|
-
|
|
2597
|
+
import chalk from "chalk";
|
|
2598
|
+
function displayCurrent(resourceKey, current) {
|
|
2599
|
+
if (resourceKey === "ssl" && current && typeof current === "object") return "(certificate installed)";
|
|
2600
|
+
return current;
|
|
2601
|
+
}
|
|
2602
|
+
function safeInSync(resource, current, desired, app, warnings) {
|
|
2603
|
+
try {
|
|
2604
|
+
return resource.inSync(current, desired);
|
|
2605
|
+
} catch (err) {
|
|
2606
|
+
warnings.push(`could not compare '${resource.key}' for app '${app}': ${err instanceof Error ? err.message : String(err)}`);
|
|
2607
|
+
return void 0;
|
|
2608
|
+
}
|
|
1813
2609
|
}
|
|
1814
2610
|
function hasAdditionsOrChanges(change) {
|
|
1815
2611
|
const c = change;
|
|
@@ -1821,9 +2617,14 @@ function isEmptyValue(value) {
|
|
|
1821
2617
|
return value === null || value === void 0 || Array.isArray(value) && value.length === 0 || typeof value === "object" && Object.keys(value).length === 0;
|
|
1822
2618
|
}
|
|
1823
2619
|
async function computeDiff(ctx, config) {
|
|
1824
|
-
const result = { apps: {}, services: {}, plugins: {}, inSync: true, readWarnings: [] };
|
|
2620
|
+
const result = { apps: {}, services: {}, plugins: {}, global: {}, networks: {}, inSync: true, envNotes: {}, readWarnings: [] };
|
|
1825
2621
|
const appNames = Object.keys(config.apps);
|
|
1826
|
-
await
|
|
2622
|
+
const serverVersion = await assertDokkuSupported(ctx);
|
|
2623
|
+
const fileVersion = config.dokku?.version;
|
|
2624
|
+
if (fileVersion && compareToFile(serverVersion, fileVersion) !== "equal") {
|
|
2625
|
+
result.dokku = { file: fileVersion, server: serverVersion };
|
|
2626
|
+
result.inSync = false;
|
|
2627
|
+
}
|
|
1827
2628
|
const appExists = /* @__PURE__ */ new Map();
|
|
1828
2629
|
await Promise.all(
|
|
1829
2630
|
appNames.map(async (app) => {
|
|
@@ -1843,13 +2644,29 @@ async function computeDiff(ctx, config) {
|
|
|
1843
2644
|
}
|
|
1844
2645
|
})
|
|
1845
2646
|
);
|
|
2647
|
+
let globalEnv;
|
|
2648
|
+
let globalEnvValues = {};
|
|
2649
|
+
let globalEnvError;
|
|
2650
|
+
if (config.env !== false) {
|
|
2651
|
+
try {
|
|
2652
|
+
globalEnvValues = await readGlobalEnv(ctx);
|
|
2653
|
+
globalEnv = await readGlobalEnvDrift(ctx, config.env, globalEnvValues);
|
|
2654
|
+
} catch (err) {
|
|
2655
|
+
globalEnvError = err;
|
|
2656
|
+
}
|
|
2657
|
+
}
|
|
1846
2658
|
for (const [app, appConfig] of Object.entries(config.apps)) {
|
|
1847
2659
|
const appDiff = {};
|
|
2660
|
+
const envCache = /* @__PURE__ */ new Map();
|
|
1848
2661
|
for (const resource of ALL_APP_RESOURCES) {
|
|
1849
2662
|
if (resource.key.startsWith("_")) continue;
|
|
1850
2663
|
let desired;
|
|
1851
2664
|
if (resource.key === "proxy") {
|
|
1852
2665
|
desired = appConfig.proxy?.enabled;
|
|
2666
|
+
} else if (resource.key === "git") {
|
|
2667
|
+
desired = appConfig.git ?? config.git;
|
|
2668
|
+
} else if (resource.key === "proxy_props") {
|
|
2669
|
+
desired = proxyProps(appConfig.proxy);
|
|
1853
2670
|
} else {
|
|
1854
2671
|
desired = appConfig[resource.key];
|
|
1855
2672
|
}
|
|
@@ -1884,17 +2701,28 @@ async function computeDiff(ctx, config) {
|
|
|
1884
2701
|
current = await resource.read(ctx, app);
|
|
1885
2702
|
} else if (bulk) {
|
|
1886
2703
|
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
2704
|
} else {
|
|
1892
2705
|
current = await resource.read(ctx, app);
|
|
1893
2706
|
}
|
|
1894
|
-
|
|
1895
|
-
|
|
1896
|
-
|
|
1897
|
-
if (
|
|
2707
|
+
if (resource.inSync) {
|
|
2708
|
+
const removedHere = resource.removals ? resource.removals(current, desired) : [];
|
|
2709
|
+
let status;
|
|
2710
|
+
if (current === void 0) {
|
|
2711
|
+
status = "unknown";
|
|
2712
|
+
result.readWarnings.push(`could not read '${resource.key}' for app '${app}'`);
|
|
2713
|
+
} else {
|
|
2714
|
+
const same = safeInSync(resource, current, desired, app, result.readWarnings);
|
|
2715
|
+
if (same === void 0) status = "unknown";
|
|
2716
|
+
else if (same) status = removedHere.length > 0 ? "extra" : "in-sync";
|
|
2717
|
+
else status = current === false || isEmptyValue(current) ? "missing" : "changed";
|
|
2718
|
+
}
|
|
2719
|
+
appDiff[resource.key] = {
|
|
2720
|
+
status,
|
|
2721
|
+
desired,
|
|
2722
|
+
current: displayCurrent(resource.key, current),
|
|
2723
|
+
...removedHere.length > 0 ? { removed: removedHere } : {}
|
|
2724
|
+
};
|
|
2725
|
+
if (status !== "in-sync") result.inSync = false;
|
|
1898
2726
|
continue;
|
|
1899
2727
|
}
|
|
1900
2728
|
let removed = resource.removals ? resource.removals(current, desired) : [];
|
|
@@ -1930,16 +2758,57 @@ async function computeDiff(ctx, config) {
|
|
|
1930
2758
|
appDiff[resource.key].removed = removed;
|
|
1931
2759
|
}
|
|
1932
2760
|
}
|
|
1933
|
-
if (
|
|
1934
|
-
const
|
|
1935
|
-
|
|
1936
|
-
|
|
1937
|
-
|
|
2761
|
+
if (appExists.get(app) && (config.postgres || config.redis)) {
|
|
2762
|
+
const unlink = servicesToUnlink(await readServiceLinks(ctx, app, appConfig.links ?? [], config)).map((s) => s.service);
|
|
2763
|
+
if (appConfig.links === void 0) {
|
|
2764
|
+
if (unlink.length > 0) {
|
|
2765
|
+
appDiff["links"] = { status: "extra", current: unlink, removed: unlink, note: "no links: in the file; up will unlink these" };
|
|
2766
|
+
result.inSync = false;
|
|
2767
|
+
}
|
|
2768
|
+
} else {
|
|
2769
|
+
for (const service of unlink) {
|
|
2770
|
+
appDiff[`links:${service}`] = { status: "extra", current: service, removed: [service], note: "up unlinks" };
|
|
2771
|
+
result.inSync = false;
|
|
2772
|
+
}
|
|
2773
|
+
}
|
|
2774
|
+
}
|
|
2775
|
+
if (appConfig.links && appExists.get(app)) {
|
|
2776
|
+
for (const entry of appConfig.links) {
|
|
2777
|
+
const link = normalizeLink(entry);
|
|
2778
|
+
const resolved = resolveServicePlugin(link.service, config);
|
|
2779
|
+
if (!resolved) continue;
|
|
2780
|
+
if (!await ctx.check(`${resolved.plugin}:linked`, link.service, app)) {
|
|
2781
|
+
appDiff[`links:${link.service}`] = { status: "missing", desired: linkEnvVar(resolved.plugin, link), current: void 0 };
|
|
2782
|
+
result.inSync = false;
|
|
2783
|
+
continue;
|
|
2784
|
+
}
|
|
2785
|
+
const opts = await readLinkOptions(ctx, app, resolved.plugin, link, envCache);
|
|
2786
|
+
if (opts.state === "differs") {
|
|
2787
|
+
appDiff[`links:${link.service}`] = { status: "changed", desired: opts.desired, current: opts.current, note: "up unlinks and relinks it" };
|
|
2788
|
+
result.inSync = false;
|
|
2789
|
+
} else if (opts.state === "unreadable") {
|
|
2790
|
+
result.readWarnings.push(`could not read the link options of '${link.service}' for app '${app}' (not counted as drift): ${opts.reason}`);
|
|
1938
2791
|
}
|
|
1939
2792
|
}
|
|
1940
|
-
|
|
1941
|
-
|
|
1942
|
-
|
|
2793
|
+
}
|
|
2794
|
+
if (appExists.get(app)) {
|
|
2795
|
+
try {
|
|
2796
|
+
const links = (appConfig.links ?? []).flatMap((entry) => {
|
|
2797
|
+
const link = normalizeLink(entry);
|
|
2798
|
+
const resolved = resolveServicePlugin(link.service, config);
|
|
2799
|
+
return resolved ? [{ plugin: resolved.plugin, service: link.service }] : [];
|
|
2800
|
+
});
|
|
2801
|
+
const notes = undeclaredEnvKeys({
|
|
2802
|
+
env: await readEnv(ctx, app, envCache),
|
|
2803
|
+
declared: appConfig.env ? Object.keys(appConfig.env) : [],
|
|
2804
|
+
links,
|
|
2805
|
+
external: appConfig.env_external ?? [],
|
|
2806
|
+
globalFile: config.env || {},
|
|
2807
|
+
globalServer: globalEnvValues
|
|
2808
|
+
});
|
|
2809
|
+
if (notes.length > 0) result.envNotes[app] = notes;
|
|
2810
|
+
} catch (err) {
|
|
2811
|
+
result.readWarnings.push(`could not read the env of app '${app}' to note keys the file does not declare: ${err instanceof Error ? err.message : String(err)}`);
|
|
1943
2812
|
}
|
|
1944
2813
|
}
|
|
1945
2814
|
result.apps[app] = appDiff;
|
|
@@ -1954,20 +2823,137 @@ async function computeDiff(ctx, config) {
|
|
|
1954
2823
|
if (plan.action !== "unknown") result.inSync = false;
|
|
1955
2824
|
}
|
|
1956
2825
|
}
|
|
1957
|
-
|
|
1958
|
-
|
|
1959
|
-
|
|
1960
|
-
|
|
2826
|
+
if (config.domains !== void 0) {
|
|
2827
|
+
try {
|
|
2828
|
+
const current = await readGlobalDomains(ctx);
|
|
2829
|
+
if (!globalDomainsInSync(current, config.domains)) {
|
|
2830
|
+
result.global.domains = { status: current.length === 0 ? "missing" : "changed", desired: config.domains, current };
|
|
2831
|
+
result.inSync = false;
|
|
2832
|
+
}
|
|
2833
|
+
} catch (err) {
|
|
2834
|
+
result.global.domains = { status: "unknown", desired: config.domains };
|
|
2835
|
+
result.readWarnings.push(`could not read global 'domains': ${err instanceof Error ? err.message : String(err)}`);
|
|
2836
|
+
result.inSync = false;
|
|
2837
|
+
}
|
|
2838
|
+
}
|
|
2839
|
+
if (globalEnv) {
|
|
2840
|
+
const { changed, removed, markerOutdated } = globalEnv;
|
|
2841
|
+
if (changed.length > 0 || removed.length > 0 || markerOutdated) {
|
|
2842
|
+
result.global.env = {
|
|
2843
|
+
status: changed.length === 0 && removed.length > 0 ? "extra" : "changed",
|
|
2844
|
+
desired: Object.fromEntries(changed.map((k) => [k, "****"])),
|
|
2845
|
+
removed,
|
|
2846
|
+
note: changed.length === 0 && removed.length === 0 ? `${GLOBAL_MANAGED_KEYS_VAR} not written yet` : void 0
|
|
2847
|
+
};
|
|
2848
|
+
result.inSync = false;
|
|
2849
|
+
}
|
|
2850
|
+
} else if (config.env !== false) {
|
|
2851
|
+
const err = globalEnvError;
|
|
2852
|
+
result.global.env = { status: "unknown", desired: Object.keys(config.env ?? {}) };
|
|
2853
|
+
result.readWarnings.push(`could not read global 'env': ${err instanceof Error ? err.message : String(err)}`);
|
|
2854
|
+
result.inSync = false;
|
|
2855
|
+
}
|
|
2856
|
+
try {
|
|
2857
|
+
const stale = staleBackupHashKeys(await readGlobalEnv(ctx));
|
|
2858
|
+
if (stale.length > 0) {
|
|
2859
|
+
result.global.backup_hash = { status: "extra", removed: stale, note: "old backup hash keys; up unsets them" };
|
|
2860
|
+
result.inSync = false;
|
|
2861
|
+
}
|
|
2862
|
+
} catch (err) {
|
|
2863
|
+
if (config.env === false) result.readWarnings.push(`could not read global env: ${err instanceof Error ? err.message : String(err)}`);
|
|
2864
|
+
}
|
|
2865
|
+
if (config.docker_auth) {
|
|
2866
|
+
try {
|
|
2867
|
+
const { stale, extra } = await readDockerAuthDrift(ctx, config.docker_auth);
|
|
2868
|
+
if (stale.length > 0 || extra.length > 0) {
|
|
2869
|
+
result.global.docker_auth = {
|
|
2870
|
+
status: stale.length > 0 ? "changed" : "extra",
|
|
2871
|
+
desired: Object.fromEntries(stale.map((server) => [server, "login missing or differs"])),
|
|
2872
|
+
removed: extra,
|
|
2873
|
+
note: extra.length > 0 ? "up --prune logs out of these" : void 0
|
|
2874
|
+
};
|
|
2875
|
+
result.inSync = false;
|
|
2876
|
+
}
|
|
2877
|
+
} catch (err) {
|
|
2878
|
+
result.global.docker_auth = { status: "unknown", desired: Object.keys(config.docker_auth) };
|
|
2879
|
+
result.readWarnings.push(`could not read global 'docker_auth': ${err instanceof Error ? err.message : String(err)}`);
|
|
2880
|
+
result.inSync = false;
|
|
2881
|
+
}
|
|
2882
|
+
}
|
|
2883
|
+
for (const [name, declared, read] of [
|
|
2884
|
+
["logs", config.logs, readGlobalLogs],
|
|
2885
|
+
["nginx", config.nginx, readGlobalNginx]
|
|
2886
|
+
]) {
|
|
2887
|
+
if (declared === void 0) continue;
|
|
2888
|
+
let current;
|
|
2889
|
+
try {
|
|
2890
|
+
current = await read(ctx);
|
|
2891
|
+
} catch (err) {
|
|
2892
|
+
result.global[name] = { status: "unknown", desired: declared };
|
|
2893
|
+
result.readWarnings.push(`could not read global '${name}': ${err instanceof Error ? err.message : String(err)}`);
|
|
2894
|
+
result.inSync = false;
|
|
2895
|
+
continue;
|
|
2896
|
+
}
|
|
2897
|
+
const differs = Object.entries(declared).filter(([k, v]) => current[k] !== String(v));
|
|
2898
|
+
if (differs.length === 0) continue;
|
|
2899
|
+
const shown = Object.fromEntries(Object.keys(declared).filter((k) => k in current).map((k) => [k, current[k]]));
|
|
2900
|
+
result.global[name] = { status: differs.every(([k]) => !(k in current)) ? "missing" : "changed", desired: declared, current: shown };
|
|
2901
|
+
result.inSync = false;
|
|
2902
|
+
}
|
|
2903
|
+
for (const net of config.networks ?? []) {
|
|
2904
|
+
if (await ctx.check("network:exists", net)) continue;
|
|
2905
|
+
result.networks[net] = { status: "missing" };
|
|
2906
|
+
result.inSync = false;
|
|
1961
2907
|
}
|
|
1962
|
-
for (const
|
|
1963
|
-
const
|
|
1964
|
-
|
|
1965
|
-
|
|
2908
|
+
for (const [plugin, services] of [["postgres", config.postgres], ["redis", config.redis]]) {
|
|
2909
|
+
for (const [svc, declared] of Object.entries(services ?? {})) {
|
|
2910
|
+
const exists = await ctx.check(`${plugin}:exists`, svc);
|
|
2911
|
+
if (!exists) {
|
|
2912
|
+
result.services[svc] = { status: "missing" };
|
|
2913
|
+
result.inSync = false;
|
|
2914
|
+
continue;
|
|
2915
|
+
}
|
|
2916
|
+
const backup = plugin === "postgres" ? declared.backup : void 0;
|
|
2917
|
+
const compares = declared?.version !== void 0 || declared?.image !== void 0 || backup !== void 0;
|
|
2918
|
+
if (!compares && plugin === "redis") {
|
|
2919
|
+
result.services[svc] = { status: "in-sync", mismatches: [] };
|
|
2920
|
+
continue;
|
|
2921
|
+
}
|
|
2922
|
+
try {
|
|
2923
|
+
const info = await readServiceInfo(ctx, plugin, svc);
|
|
2924
|
+
const mismatches = [...versionMismatches(info, declared), ...backup !== void 0 ? backupMismatches(info, backup) : []];
|
|
2925
|
+
const note = plugin === "postgres" && backup === void 0 && hasBackup(info) ? `backup on the server is not in the file (${describeServerBackup(info)}); up leaves it, backup: false removes it` : void 0;
|
|
2926
|
+
result.services[svc] = { status: mismatches.length ? "changed" : "in-sync", mismatches, note };
|
|
2927
|
+
if (mismatches.length) result.inSync = false;
|
|
2928
|
+
} catch (err) {
|
|
2929
|
+
result.readWarnings.push(`could not read ${plugin} service '${svc}': ${err instanceof Error ? err.message : String(err)}`);
|
|
2930
|
+
if (!compares) {
|
|
2931
|
+
result.services[svc] = { status: "in-sync", mismatches: [] };
|
|
2932
|
+
continue;
|
|
2933
|
+
}
|
|
2934
|
+
result.services[svc] = { status: "unknown" };
|
|
2935
|
+
result.inSync = false;
|
|
2936
|
+
}
|
|
2937
|
+
}
|
|
1966
2938
|
}
|
|
1967
2939
|
return result;
|
|
1968
2940
|
}
|
|
1969
2941
|
function maskDiffResult(diff) {
|
|
1970
|
-
const masked = {
|
|
2942
|
+
const masked = {
|
|
2943
|
+
apps: {},
|
|
2944
|
+
services: diff.services,
|
|
2945
|
+
plugins: diff.plugins,
|
|
2946
|
+
dokku: diff.dokku,
|
|
2947
|
+
global: Object.fromEntries(Object.entries(diff.global).map(([k, d]) => [k, {
|
|
2948
|
+
...d,
|
|
2949
|
+
desired: maskSensitiveData(d.desired),
|
|
2950
|
+
current: maskSensitiveData(d.current)
|
|
2951
|
+
}])),
|
|
2952
|
+
networks: diff.networks,
|
|
2953
|
+
inSync: diff.inSync,
|
|
2954
|
+
envNotes: diff.envNotes,
|
|
2955
|
+
readWarnings: diff.readWarnings
|
|
2956
|
+
};
|
|
1971
2957
|
for (const [app, appDiff] of Object.entries(diff.apps)) {
|
|
1972
2958
|
const maskedAppDiff = {};
|
|
1973
2959
|
for (const [feature, d] of Object.entries(appDiff)) {
|
|
@@ -1983,6 +2969,9 @@ function maskDiffResult(diff) {
|
|
|
1983
2969
|
}
|
|
1984
2970
|
return masked;
|
|
1985
2971
|
}
|
|
2972
|
+
function driftCount(diff) {
|
|
2973
|
+
return 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 + Object.keys(diff.global).length + Object.keys(diff.networks).length + (diff.dokku ? 1 : 0);
|
|
2974
|
+
}
|
|
1986
2975
|
function formatSummary(diff) {
|
|
1987
2976
|
const lines = [""];
|
|
1988
2977
|
for (const [app, appDiff] of Object.entries(diff.apps)) {
|
|
@@ -1991,15 +2980,30 @@ function formatSummary(diff) {
|
|
|
1991
2980
|
lines.push(` app: ${app}`);
|
|
1992
2981
|
lines.push(` (in sync)`);
|
|
1993
2982
|
} else {
|
|
1994
|
-
lines.push(` app: ${
|
|
2983
|
+
lines.push(` app: ${chalk.bold(app)}`);
|
|
1995
2984
|
for (const [feature, d] of changes) {
|
|
1996
|
-
const sym = d.status === "missing" ?
|
|
2985
|
+
const sym = d.status === "missing" ? chalk.green("+") : d.status === "unknown" ? chalk.red("?") : d.status === "extra" ? chalk.red("-") : chalk.yellow("~");
|
|
1997
2986
|
lines.push(` ${sym} ${feature}: ${formatFeatureSummary(d)}`);
|
|
1998
2987
|
}
|
|
1999
2988
|
}
|
|
2989
|
+
const notes = diff.envNotes[app];
|
|
2990
|
+
if (notes) lines.push(` note: env not in the file (up leaves these): ${notes.map(showNote).join(", ")}`);
|
|
2991
|
+
}
|
|
2992
|
+
if (diff.dokku) {
|
|
2993
|
+
lines.push(` dokku`);
|
|
2994
|
+
lines.push(` ${chalk.yellow("~")} version: ${diff.dokku.file} (file) \u2192 ${diff.dokku.server} (server)`);
|
|
2995
|
+
}
|
|
2996
|
+
for (const [name, d] of Object.entries(diff.global)) {
|
|
2997
|
+
const sym = d.status === "missing" ? chalk.green("+") : d.status === "unknown" ? chalk.red("?") : chalk.yellow("~");
|
|
2998
|
+
lines.push(` global`);
|
|
2999
|
+
lines.push(` ${sym} ${name}: ${formatFeatureSummary(d)}`);
|
|
3000
|
+
}
|
|
3001
|
+
for (const net of Object.keys(diff.networks)) {
|
|
3002
|
+
lines.push(` networks:`);
|
|
3003
|
+
lines.push(` ${chalk.green("+")} ${net}: not created`);
|
|
2000
3004
|
}
|
|
2001
3005
|
for (const [name, d] of Object.entries(diff.plugins)) {
|
|
2002
|
-
const sym = d.status === "missing" ?
|
|
3006
|
+
const sym = d.status === "missing" ? chalk.green("+") : d.status === "unknown" ? chalk.red("?") : chalk.yellow("~");
|
|
2003
3007
|
const detail = d.status === "missing" ? `not installed (want ${d.desired ?? "any"})` : `${d.current} \u2192 ${d.desired}`;
|
|
2004
3008
|
lines.push(` plugin: ${name}`);
|
|
2005
3009
|
lines.push(` ${sym} version: ${detail}${d.note ? ` (${d.note})` : ""}`);
|
|
@@ -2007,24 +3011,32 @@ function formatSummary(diff) {
|
|
|
2007
3011
|
for (const [svc, d] of Object.entries(diff.services)) {
|
|
2008
3012
|
if (d.status === "missing") {
|
|
2009
3013
|
lines.push(` services:`);
|
|
2010
|
-
lines.push(` ${
|
|
3014
|
+
lines.push(` ${chalk.green("+")} ${svc}: not provisioned`);
|
|
3015
|
+
}
|
|
3016
|
+
if (d.mismatches?.length || d.note) lines.push(` services: ${svc}`);
|
|
3017
|
+
for (const m of d.mismatches ?? []) {
|
|
3018
|
+
lines.push(` ${chalk.yellow("~")} ${m.field}: ${m.current || "(unknown)"} \u2192 ${m.desired}`);
|
|
2011
3019
|
}
|
|
3020
|
+
if (d.note) lines.push(` note: ${d.note}`);
|
|
2012
3021
|
}
|
|
2013
|
-
const total =
|
|
3022
|
+
const total = driftCount(diff);
|
|
2014
3023
|
if (diff.readWarnings.length > 0) {
|
|
2015
3024
|
lines.push("");
|
|
2016
|
-
lines.push(
|
|
2017
|
-
for (const w of diff.readWarnings) lines.push(
|
|
3025
|
+
lines.push(chalk.red(" Could not read some server state \u2014 treated as unknown, not missing:"));
|
|
3026
|
+
for (const w of diff.readWarnings) lines.push(chalk.red(` - ${w}`));
|
|
2018
3027
|
}
|
|
2019
3028
|
lines.push("");
|
|
2020
3029
|
if (total === 0) {
|
|
2021
|
-
lines.push(
|
|
3030
|
+
lines.push(chalk.green(" Everything in sync."));
|
|
2022
3031
|
} else {
|
|
2023
|
-
lines.push(
|
|
3032
|
+
lines.push(chalk.yellow(` ${total} resource(s) out of sync.`));
|
|
2024
3033
|
}
|
|
2025
3034
|
lines.push("");
|
|
2026
3035
|
return lines.join("\n");
|
|
2027
3036
|
}
|
|
3037
|
+
function showNote(n) {
|
|
3038
|
+
return n.label ? `${n.key} (${n.label})` : n.key;
|
|
3039
|
+
}
|
|
2028
3040
|
function formatFeatureSummary(d) {
|
|
2029
3041
|
const gone = d.removed?.length ? `not in the file: ${d.removed.join(", ")}` : "";
|
|
2030
3042
|
if (d.status === "extra") return `${gone}${d.note ? ` (${d.note})` : ""}`;
|
|
@@ -2037,7 +3049,8 @@ function formatChange(d) {
|
|
|
2037
3049
|
if (Array.isArray(d.desired) && Array.isArray(d.current)) {
|
|
2038
3050
|
return `${d.current.length} \u2192 ${d.desired.length} items`;
|
|
2039
3051
|
}
|
|
2040
|
-
|
|
3052
|
+
const desired = JSON.stringify(d.desired, showUnset);
|
|
3053
|
+
return d.current === void 0 ? desired : `${JSON.stringify(d.current, showUnset)} \u2192 ${desired}`;
|
|
2041
3054
|
}
|
|
2042
3055
|
function showUnset(_key, value) {
|
|
2043
3056
|
return value === "\0missing" ? "(unset)" : value;
|
|
@@ -2050,53 +3063,87 @@ function formatVerbose(diff) {
|
|
|
2050
3063
|
for (const [feature, d] of changes) {
|
|
2051
3064
|
lines.push(`@@ app: ${app} / ${feature} @@`);
|
|
2052
3065
|
if (d.status === "unknown") {
|
|
2053
|
-
lines.push(
|
|
3066
|
+
lines.push(chalk.red(` ? could not read server state \u2014 not reported as missing`));
|
|
2054
3067
|
continue;
|
|
2055
3068
|
}
|
|
2056
3069
|
if (d.status === "extra") {
|
|
2057
|
-
for (const r of d.removed ?? []) lines.push(
|
|
3070
|
+
for (const r of d.removed ?? []) lines.push(chalk.red(`- ${r}`));
|
|
2058
3071
|
if (d.note) lines.push(` (${d.note})`);
|
|
2059
3072
|
continue;
|
|
2060
3073
|
}
|
|
2061
3074
|
const currentLines = d.current !== void 0 ? JSON.stringify(d.current, showUnset, 2).split("\n") : [];
|
|
2062
3075
|
const desiredLines = JSON.stringify(d.desired, showUnset, 2).split("\n");
|
|
2063
|
-
for (const line of currentLines) lines.push(
|
|
2064
|
-
for (const line of desiredLines) lines.push(
|
|
2065
|
-
for (const r of d.removed ?? []) lines.push(
|
|
3076
|
+
for (const line of currentLines) lines.push(chalk.red(`- ${line}`));
|
|
3077
|
+
for (const line of desiredLines) lines.push(chalk.green(`+ ${line}`));
|
|
3078
|
+
for (const r of d.removed ?? []) lines.push(chalk.red(`- ${r} (not in the file)`));
|
|
3079
|
+
}
|
|
3080
|
+
}
|
|
3081
|
+
for (const [app, notes] of Object.entries(diff.envNotes)) {
|
|
3082
|
+
lines.push(`@@ app: ${app} / env not in the file (note; up leaves these) @@`);
|
|
3083
|
+
for (const n of notes) lines.push(` note: ${showNote(n)}`);
|
|
3084
|
+
}
|
|
3085
|
+
if (diff.dokku) {
|
|
3086
|
+
lines.push(`@@ dokku version @@`);
|
|
3087
|
+
lines.push(chalk.red(`- ${diff.dokku.server} (server)`));
|
|
3088
|
+
lines.push(chalk.green(`+ ${diff.dokku.file} (file)`));
|
|
3089
|
+
}
|
|
3090
|
+
for (const [name, d] of Object.entries(diff.global)) {
|
|
3091
|
+
lines.push(`@@ global / ${name} @@`);
|
|
3092
|
+
if (d.status === "unknown") {
|
|
3093
|
+
lines.push(chalk.red(` ? could not read server state \u2014 not reported as missing`));
|
|
3094
|
+
continue;
|
|
2066
3095
|
}
|
|
3096
|
+
if (d.current !== void 0) for (const line of JSON.stringify(d.current, null, 2).split("\n")) lines.push(chalk.red(`- ${line}`));
|
|
3097
|
+
if (d.status !== "extra") for (const line of JSON.stringify(d.desired, null, 2).split("\n")) lines.push(chalk.green(`+ ${line}`));
|
|
3098
|
+
for (const r of d.removed ?? []) lines.push(chalk.red(`- ${r} (not in the file)`));
|
|
3099
|
+
if (d.note) lines.push(` (${d.note})`);
|
|
3100
|
+
}
|
|
3101
|
+
for (const net of Object.keys(diff.networks)) {
|
|
3102
|
+
lines.push(`@@ networks @@`);
|
|
3103
|
+
lines.push(chalk.green(`+ ${net}`));
|
|
2067
3104
|
}
|
|
2068
3105
|
for (const [name, d] of Object.entries(diff.plugins)) {
|
|
2069
3106
|
lines.push(`@@ plugin: ${name} @@`);
|
|
2070
|
-
if (d.current !== void 0) lines.push(
|
|
2071
|
-
lines.push(
|
|
3107
|
+
if (d.current !== void 0) lines.push(chalk.red(`- ${d.current}`));
|
|
3108
|
+
lines.push(chalk.green(`+ ${d.desired ?? "(any version)"}`) + (d.note ? ` (${d.note})` : ""));
|
|
2072
3109
|
}
|
|
2073
3110
|
for (const [svc, d] of Object.entries(diff.services)) {
|
|
2074
3111
|
if (d.status === "missing") {
|
|
2075
3112
|
lines.push(`@@ services @@`);
|
|
2076
|
-
lines.push(
|
|
3113
|
+
lines.push(chalk.green(`+ ${svc}`));
|
|
3114
|
+
}
|
|
3115
|
+
for (const m of d.mismatches ?? []) {
|
|
3116
|
+
lines.push(`@@ services: ${svc} ${m.field} @@`);
|
|
3117
|
+
lines.push(chalk.red(`- ${m.current}`));
|
|
3118
|
+
lines.push(chalk.green(`+ ${m.desired}`));
|
|
2077
3119
|
}
|
|
3120
|
+
if (d.note) lines.push(`@@ services: ${svc} @@
|
|
3121
|
+
(${d.note})`);
|
|
2078
3122
|
}
|
|
2079
3123
|
if (diff.readWarnings.length > 0) {
|
|
2080
3124
|
lines.push("");
|
|
2081
|
-
lines.push(
|
|
2082
|
-
for (const w of diff.readWarnings) lines.push(
|
|
3125
|
+
lines.push(chalk.red("Could not read some server state \u2014 treated as unknown, not missing:"));
|
|
3126
|
+
for (const w of diff.readWarnings) lines.push(chalk.red(` - ${w}`));
|
|
3127
|
+
}
|
|
3128
|
+
if (driftCount(diff) === 0) {
|
|
3129
|
+
lines.push(chalk.green("Everything in sync."));
|
|
2083
3130
|
}
|
|
2084
3131
|
lines.push("");
|
|
2085
3132
|
return lines.join("\n");
|
|
2086
3133
|
}
|
|
2087
3134
|
|
|
2088
3135
|
// src/commands/validate.ts
|
|
2089
|
-
import * as
|
|
3136
|
+
import * as fs3 from "fs";
|
|
2090
3137
|
import * as yaml2 from "js-yaml";
|
|
2091
3138
|
function validate(filePath) {
|
|
2092
3139
|
const errors = [];
|
|
2093
3140
|
const warnings = [];
|
|
2094
|
-
if (!
|
|
3141
|
+
if (!fs3.existsSync(filePath)) {
|
|
2095
3142
|
return { errors: [`File not found: ${filePath}`], warnings };
|
|
2096
3143
|
}
|
|
2097
3144
|
let raw;
|
|
2098
3145
|
try {
|
|
2099
|
-
raw = yaml2.load(
|
|
3146
|
+
raw = yaml2.load(fs3.readFileSync(filePath, "utf8"));
|
|
2100
3147
|
} catch (e) {
|
|
2101
3148
|
return { errors: [`YAML parse error: ${e.message}`], warnings };
|
|
2102
3149
|
}
|
|
@@ -2104,7 +3151,13 @@ function validate(filePath) {
|
|
|
2104
3151
|
if (!result.success) {
|
|
2105
3152
|
for (const issue of result.error.issues) {
|
|
2106
3153
|
const issuePath = issue.path.join(".");
|
|
2107
|
-
|
|
3154
|
+
const branches = issue.code === "invalid_union" ? issue.errors : void 0;
|
|
3155
|
+
const reasons = branches?.filter((b) => !b.every((i) => i.code === "invalid_type" && i.path.length === 0)).flat();
|
|
3156
|
+
if (reasons && reasons.length > 0) {
|
|
3157
|
+
for (const r of reasons) errors.push(`${[issuePath, ...r.path].filter((x) => x !== "").join(".")}: ${r.message}`);
|
|
3158
|
+
} else {
|
|
3159
|
+
errors.push(`${issuePath}: ${issue.message}`);
|
|
3160
|
+
}
|
|
2108
3161
|
}
|
|
2109
3162
|
}
|
|
2110
3163
|
const data = raw;
|
|
@@ -2117,9 +3170,11 @@ function validate(filePath) {
|
|
|
2117
3170
|
for (const name of Object.keys(data.redis)) serviceNames.add(name);
|
|
2118
3171
|
}
|
|
2119
3172
|
for (const [appName, appCfg] of Object.entries(data.apps)) {
|
|
3173
|
+
errors.push(...envExternalErrors(appName, appCfg));
|
|
2120
3174
|
if (!appCfg?.links) continue;
|
|
2121
|
-
for (const
|
|
2122
|
-
|
|
3175
|
+
for (const entry of appCfg.links) {
|
|
3176
|
+
const link = typeof entry === "string" ? entry : entry?.service;
|
|
3177
|
+
if (typeof link === "string" && !serviceNames.has(link)) {
|
|
2123
3178
|
errors.push(`apps.${appName}.links: service "${link}" not defined in postgres or redis`);
|
|
2124
3179
|
}
|
|
2125
3180
|
}
|
|
@@ -2134,9 +3189,25 @@ function validate(filePath) {
|
|
|
2134
3189
|
}
|
|
2135
3190
|
return { errors, warnings };
|
|
2136
3191
|
}
|
|
3192
|
+
function envExternalErrors(appName, appCfg) {
|
|
3193
|
+
const list = appCfg?.env_external;
|
|
3194
|
+
if (!Array.isArray(list)) return [];
|
|
3195
|
+
const declared = appCfg.env && typeof appCfg.env === "object" ? appCfg.env : {};
|
|
3196
|
+
const errors = [];
|
|
3197
|
+
const seen = /* @__PURE__ */ new Set();
|
|
3198
|
+
for (const key of list) {
|
|
3199
|
+
if (typeof key !== "string") continue;
|
|
3200
|
+
const where = `apps.${appName}.env_external`;
|
|
3201
|
+
if (key === "DOKKU_COMPOSE_MANAGED_KEYS") errors.push(`${where}: "${key}" is dokku-compose's own marker`);
|
|
3202
|
+
else if (seen.has(key)) errors.push(`${where}: "${key}" is listed twice`);
|
|
3203
|
+
else if (key in declared) errors.push(`${where}: "${key}" is also in env:`);
|
|
3204
|
+
seen.add(key);
|
|
3205
|
+
}
|
|
3206
|
+
return errors;
|
|
3207
|
+
}
|
|
2137
3208
|
|
|
2138
3209
|
// src/index.ts
|
|
2139
|
-
import
|
|
3210
|
+
import chalk2 from "chalk";
|
|
2140
3211
|
var require2 = createRequire(import.meta.url);
|
|
2141
3212
|
var { version } = require2("../package.json");
|
|
2142
3213
|
var program = new Command().name("dokku-compose").version(version);
|
|
@@ -2151,7 +3222,7 @@ program.command("up [apps...]").description("Create/update apps and services to
|
|
|
2151
3222
|
const runner = makeRunner(opts);
|
|
2152
3223
|
const ctx = createContext(runner);
|
|
2153
3224
|
try {
|
|
2154
|
-
await runUp(ctx, config, apps, { failFast: opts.failFast ?? false, prune: opts.prune ?? false });
|
|
3225
|
+
await runUp(ctx, config, apps, { failFast: opts.failFast ?? false, prune: opts.prune ?? false, dryRun: opts.dryRun ?? false });
|
|
2155
3226
|
if (opts.dryRun) {
|
|
2156
3227
|
console.log("\n# Commands that would run:");
|
|
2157
3228
|
for (const cmd of runner.dryRunLog) {
|
|
@@ -2160,7 +3231,7 @@ program.command("up [apps...]").description("Create/update apps and services to
|
|
|
2160
3231
|
}
|
|
2161
3232
|
}
|
|
2162
3233
|
} catch (error) {
|
|
2163
|
-
console.error(
|
|
3234
|
+
console.error(chalk2.red(`
|
|
2164
3235
|
${error.message}`));
|
|
2165
3236
|
process.exitCode = 1;
|
|
2166
3237
|
} finally {
|
|
@@ -2178,7 +3249,7 @@ program.command("down [apps...]").description("Destroy apps and services (requir
|
|
|
2178
3249
|
try {
|
|
2179
3250
|
await runDown(ctx, config, apps, { force: true, failFast: opts.failFast ?? false });
|
|
2180
3251
|
} catch (error) {
|
|
2181
|
-
console.error(
|
|
3252
|
+
console.error(chalk2.red(`
|
|
2182
3253
|
${error.message}`));
|
|
2183
3254
|
process.exitCode = 1;
|
|
2184
3255
|
} finally {
|
|
@@ -2211,11 +3282,15 @@ program.command("export").description("Export server state to dokku-compose.yml
|
|
|
2211
3282
|
const result = opts.sensitive ? rawResult : maskSensitiveData(rawResult);
|
|
2212
3283
|
const out = yaml3.dump(result, { lineWidth: 120 });
|
|
2213
3284
|
if (opts.output) {
|
|
2214
|
-
|
|
3285
|
+
fs4.writeFileSync(opts.output, out);
|
|
2215
3286
|
console.error(`Written to ${opts.output}`);
|
|
2216
3287
|
} else {
|
|
2217
3288
|
process.stdout.write(out);
|
|
2218
3289
|
}
|
|
3290
|
+
} catch (error) {
|
|
3291
|
+
console.error(chalk2.red(`
|
|
3292
|
+
${error.message}`));
|
|
3293
|
+
process.exitCode = 1;
|
|
2219
3294
|
} finally {
|
|
2220
3295
|
await ctx.close();
|
|
2221
3296
|
}
|
|
@@ -2230,6 +3305,10 @@ program.command("diff").description("Show what is out of sync between config and
|
|
|
2230
3305
|
const output = opts.verbose ? formatVerbose(diff) : formatSummary(diff);
|
|
2231
3306
|
process.stdout.write(output);
|
|
2232
3307
|
process.exitCode = rawDiff.inSync ? 0 : 1;
|
|
3308
|
+
} catch (error) {
|
|
3309
|
+
console.error(chalk2.red(`
|
|
3310
|
+
${error.message}`));
|
|
3311
|
+
process.exitCode = 1;
|
|
2233
3312
|
} finally {
|
|
2234
3313
|
await ctx.close();
|
|
2235
3314
|
}
|
|
@@ -2238,8 +3317,12 @@ program.command("ps [apps...]").description("Show status of configured apps").op
|
|
|
2238
3317
|
const config = loadConfig(opts.file);
|
|
2239
3318
|
const runner = makeRunner({});
|
|
2240
3319
|
try {
|
|
2241
|
-
const { runPs } = await import("./ps-
|
|
3320
|
+
const { runPs } = await import("./ps-QQAVQTEV.js");
|
|
2242
3321
|
await runPs(runner, config, apps);
|
|
3322
|
+
} catch (error) {
|
|
3323
|
+
console.error(chalk2.red(`
|
|
3324
|
+
${error.message}`));
|
|
3325
|
+
process.exitCode = 1;
|
|
2243
3326
|
} finally {
|
|
2244
3327
|
await runner.close();
|
|
2245
3328
|
}
|