dokku-compose 0.2.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/LICENSE +21 -0
- package/README.md +555 -0
- package/bin/dokku-compose +4 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +965 -0
- package/dist/init-GIXEVLNW.js +31 -0
- package/dist/ps-33II4UU3.js +20 -0
- package/package.json +52 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,965 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/index.ts
|
|
4
|
+
import { Command } from "commander";
|
|
5
|
+
import * as fs3 from "fs";
|
|
6
|
+
import * as yaml3 from "js-yaml";
|
|
7
|
+
|
|
8
|
+
// src/core/config.ts
|
|
9
|
+
import * as fs from "fs";
|
|
10
|
+
import * as yaml from "js-yaml";
|
|
11
|
+
|
|
12
|
+
// src/core/schema.ts
|
|
13
|
+
import { z } from "zod";
|
|
14
|
+
var PortSchema = z.string().regex(
|
|
15
|
+
/^(http|https|tcp|udp):\d+:\d+$/,
|
|
16
|
+
"Port must be scheme:host:container (e.g. http:80:3000)"
|
|
17
|
+
);
|
|
18
|
+
var EnvMapSchema = z.union([
|
|
19
|
+
z.record(z.string(), z.union([z.string(), z.number(), z.boolean()])),
|
|
20
|
+
z.literal(false)
|
|
21
|
+
]);
|
|
22
|
+
var ChecksSchema = z.union([
|
|
23
|
+
z.literal(false),
|
|
24
|
+
z.object({
|
|
25
|
+
disabled: z.array(z.string()).optional(),
|
|
26
|
+
skipped: z.array(z.string()).optional()
|
|
27
|
+
}).catchall(z.union([z.string(), z.number(), z.boolean()]))
|
|
28
|
+
]);
|
|
29
|
+
var AppSchema = z.object({
|
|
30
|
+
domains: z.union([z.array(z.string()), z.literal(false)]).optional(),
|
|
31
|
+
links: z.array(z.string()).optional(),
|
|
32
|
+
ports: z.array(PortSchema).optional(),
|
|
33
|
+
env: EnvMapSchema.optional(),
|
|
34
|
+
ssl: z.union([
|
|
35
|
+
z.literal(false),
|
|
36
|
+
z.literal(true),
|
|
37
|
+
z.object({ certfile: z.string(), keyfile: z.string() })
|
|
38
|
+
]).optional(),
|
|
39
|
+
storage: z.array(z.string()).optional(),
|
|
40
|
+
proxy: z.object({ enabled: z.boolean() }).optional(),
|
|
41
|
+
networks: z.array(z.string()).optional(),
|
|
42
|
+
network: z.object({
|
|
43
|
+
attach_post_create: z.union([z.array(z.string()), z.literal(false)]).optional(),
|
|
44
|
+
initial_network: z.union([z.string(), z.literal(false)]).optional(),
|
|
45
|
+
bind_all_interfaces: z.boolean().optional(),
|
|
46
|
+
tld: z.union([z.string(), z.literal(false)]).optional()
|
|
47
|
+
}).optional(),
|
|
48
|
+
nginx: z.record(z.string(), z.union([z.string(), z.number()])).optional(),
|
|
49
|
+
logs: z.record(z.string(), z.union([z.string(), z.number()])).optional(),
|
|
50
|
+
registry: z.record(z.string(), z.union([z.string(), z.boolean()])).optional(),
|
|
51
|
+
scheduler: z.string().optional(),
|
|
52
|
+
checks: ChecksSchema.optional(),
|
|
53
|
+
build: z.object({
|
|
54
|
+
dockerfile: z.string().optional(),
|
|
55
|
+
app_json: z.string().optional(),
|
|
56
|
+
context: z.string().optional(),
|
|
57
|
+
args: z.record(z.string(), z.string()).optional()
|
|
58
|
+
}).optional(),
|
|
59
|
+
docker_options: z.object({
|
|
60
|
+
build: z.array(z.string()).optional(),
|
|
61
|
+
deploy: z.array(z.string()).optional(),
|
|
62
|
+
run: z.array(z.string()).optional()
|
|
63
|
+
}).optional()
|
|
64
|
+
});
|
|
65
|
+
var ServiceSchema = z.object({
|
|
66
|
+
plugin: z.string(),
|
|
67
|
+
version: z.string().optional(),
|
|
68
|
+
image: z.string().optional()
|
|
69
|
+
});
|
|
70
|
+
var PluginSchema = z.object({
|
|
71
|
+
url: z.string().url(),
|
|
72
|
+
version: z.string().optional()
|
|
73
|
+
});
|
|
74
|
+
var ConfigSchema = z.object({
|
|
75
|
+
dokku: z.object({
|
|
76
|
+
version: z.string().optional()
|
|
77
|
+
}).optional(),
|
|
78
|
+
plugins: z.record(z.string(), PluginSchema).optional(),
|
|
79
|
+
networks: z.array(z.string()).optional(),
|
|
80
|
+
services: z.record(z.string(), ServiceSchema).optional(),
|
|
81
|
+
apps: z.record(z.string(), AppSchema),
|
|
82
|
+
domains: z.union([z.array(z.string()), z.literal(false)]).optional(),
|
|
83
|
+
env: EnvMapSchema.optional(),
|
|
84
|
+
nginx: z.record(z.string(), z.union([z.string(), z.number()])).optional(),
|
|
85
|
+
logs: z.record(z.string(), z.union([z.string(), z.number()])).optional()
|
|
86
|
+
});
|
|
87
|
+
function parseConfig(raw) {
|
|
88
|
+
return ConfigSchema.parse(raw);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// src/core/config.ts
|
|
92
|
+
function loadConfig(filePath) {
|
|
93
|
+
if (!fs.existsSync(filePath)) {
|
|
94
|
+
throw new Error(`Config file not found: ${filePath}`);
|
|
95
|
+
}
|
|
96
|
+
const raw = yaml.load(fs.readFileSync(filePath, "utf8"));
|
|
97
|
+
return parseConfig(raw);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// src/core/dokku.ts
|
|
101
|
+
import { execa } from "execa";
|
|
102
|
+
function createRunner(opts = {}) {
|
|
103
|
+
const log = [];
|
|
104
|
+
async function execDokku(args) {
|
|
105
|
+
if (opts.host) {
|
|
106
|
+
try {
|
|
107
|
+
const result = await execa("ssh", [`dokku@${opts.host}`, ...args]);
|
|
108
|
+
return { stdout: result.stdout, ok: true };
|
|
109
|
+
} catch (e) {
|
|
110
|
+
return { stdout: e.stdout ?? "", ok: false };
|
|
111
|
+
}
|
|
112
|
+
} else {
|
|
113
|
+
try {
|
|
114
|
+
const result = await execa("dokku", args);
|
|
115
|
+
return { stdout: result.stdout, ok: true };
|
|
116
|
+
} catch (e) {
|
|
117
|
+
return { stdout: e.stdout ?? "", ok: false };
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
return {
|
|
122
|
+
dryRunLog: log,
|
|
123
|
+
async run(...args) {
|
|
124
|
+
if (opts.dryRun) {
|
|
125
|
+
log.push(args.join(" "));
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
await execDokku(args);
|
|
129
|
+
},
|
|
130
|
+
async query(...args) {
|
|
131
|
+
if (opts.dryRun) return "";
|
|
132
|
+
const { stdout } = await execDokku(args);
|
|
133
|
+
return stdout.trim();
|
|
134
|
+
},
|
|
135
|
+
async check(...args) {
|
|
136
|
+
if (opts.dryRun) return false;
|
|
137
|
+
const { ok } = await execDokku(args);
|
|
138
|
+
return ok;
|
|
139
|
+
}
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// src/core/logger.ts
|
|
144
|
+
import chalk from "chalk";
|
|
145
|
+
function logAction(context, message) {
|
|
146
|
+
process.stdout.write(chalk.blue(`[${context.padEnd(12)}]`) + ` ${message}`);
|
|
147
|
+
}
|
|
148
|
+
function logDone() {
|
|
149
|
+
console.log(`... ${chalk.green("done")}`);
|
|
150
|
+
}
|
|
151
|
+
function logSkip() {
|
|
152
|
+
console.log(`... ${chalk.yellow("already configured")}`);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// src/modules/apps.ts
|
|
156
|
+
async function ensureApp(runner, app) {
|
|
157
|
+
const exists = await runner.check("apps:exists", app);
|
|
158
|
+
logAction(app, "Creating app");
|
|
159
|
+
if (exists) {
|
|
160
|
+
logSkip();
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
await runner.run("apps:create", app);
|
|
164
|
+
logDone();
|
|
165
|
+
}
|
|
166
|
+
async function destroyApp(runner, app) {
|
|
167
|
+
const exists = await runner.check("apps:exists", app);
|
|
168
|
+
logAction(app, "Destroying app");
|
|
169
|
+
if (!exists) {
|
|
170
|
+
logSkip();
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
await runner.run("apps:destroy", app, "--force");
|
|
174
|
+
logDone();
|
|
175
|
+
}
|
|
176
|
+
async function exportApps(runner) {
|
|
177
|
+
const output = await runner.query("apps:list");
|
|
178
|
+
return output.split("\n").map((s) => s.trim()).filter(Boolean);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// src/modules/domains.ts
|
|
182
|
+
async function ensureAppDomains(runner, app, domains) {
|
|
183
|
+
if (domains === void 0) return;
|
|
184
|
+
logAction(app, "Configuring domains");
|
|
185
|
+
if (domains === false) {
|
|
186
|
+
await runner.run("domains:disable", app);
|
|
187
|
+
await runner.run("domains:clear", app);
|
|
188
|
+
} else {
|
|
189
|
+
await runner.run("domains:enable", app);
|
|
190
|
+
await runner.run("domains:set", app, ...domains);
|
|
191
|
+
}
|
|
192
|
+
logDone();
|
|
193
|
+
}
|
|
194
|
+
async function ensureGlobalDomains(runner, domains) {
|
|
195
|
+
logAction("global", "Configuring domains");
|
|
196
|
+
if (domains === false) {
|
|
197
|
+
await runner.run("domains:clear-global");
|
|
198
|
+
} else {
|
|
199
|
+
await runner.run("domains:set-global", ...domains);
|
|
200
|
+
}
|
|
201
|
+
logDone();
|
|
202
|
+
}
|
|
203
|
+
async function exportAppDomains(runner, app) {
|
|
204
|
+
const enabledRaw = await runner.query("domains:report", app, "--domains-app-enabled");
|
|
205
|
+
if (enabledRaw.trim() === "false") return false;
|
|
206
|
+
const raw = await runner.query("domains:report", app, "--domains-app-vhosts");
|
|
207
|
+
const vhosts = raw.split("\n").map((s) => s.trim()).filter(Boolean);
|
|
208
|
+
if (vhosts.length === 0) return void 0;
|
|
209
|
+
return vhosts;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// src/modules/plugins.ts
|
|
213
|
+
async function ensurePlugins(runner, plugins) {
|
|
214
|
+
const listOutput = await runner.query("plugin:list");
|
|
215
|
+
const installedNames = new Set(
|
|
216
|
+
listOutput.split("\n").map((line) => line.trim().split(/\s+/)[0]).filter(Boolean)
|
|
217
|
+
);
|
|
218
|
+
for (const [name, config] of Object.entries(plugins)) {
|
|
219
|
+
logAction("plugins", `Installing ${name}`);
|
|
220
|
+
if (installedNames.has(name)) {
|
|
221
|
+
logSkip();
|
|
222
|
+
continue;
|
|
223
|
+
}
|
|
224
|
+
await runner.run("plugin:install", config.url, "--name", name);
|
|
225
|
+
logDone();
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// src/modules/network.ts
|
|
230
|
+
async function ensureNetworks(runner, networks) {
|
|
231
|
+
for (const net of networks) {
|
|
232
|
+
logAction("network", `Creating ${net}`);
|
|
233
|
+
const exists = await runner.check("network:exists", net);
|
|
234
|
+
if (exists) {
|
|
235
|
+
logSkip();
|
|
236
|
+
continue;
|
|
237
|
+
}
|
|
238
|
+
await runner.run("network:create", net);
|
|
239
|
+
logDone();
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
async function ensureAppNetworks(runner, app, networks) {
|
|
243
|
+
if (!networks || networks.length === 0) return;
|
|
244
|
+
logAction(app, "Setting networks");
|
|
245
|
+
await runner.run("network:set", app, "attach-post-deploy", ...networks);
|
|
246
|
+
logDone();
|
|
247
|
+
}
|
|
248
|
+
async function ensureAppNetwork(runner, app, network) {
|
|
249
|
+
if (!network) return;
|
|
250
|
+
if (network.attach_post_create !== void 0 && network.attach_post_create !== false) {
|
|
251
|
+
const nets = Array.isArray(network.attach_post_create) ? network.attach_post_create : [network.attach_post_create];
|
|
252
|
+
await runner.run("network:set", app, "attach-post-create", ...nets);
|
|
253
|
+
}
|
|
254
|
+
if (network.initial_network !== void 0 && network.initial_network !== false) {
|
|
255
|
+
await runner.run("network:set", app, "initial-network", network.initial_network);
|
|
256
|
+
}
|
|
257
|
+
if (network.bind_all_interfaces !== void 0) {
|
|
258
|
+
await runner.run("network:set", app, "bind-all-interfaces", String(network.bind_all_interfaces));
|
|
259
|
+
}
|
|
260
|
+
if (network.tld !== void 0 && network.tld !== false) {
|
|
261
|
+
await runner.run("network:set", app, "tld", network.tld);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
async function exportNetworks(runner) {
|
|
265
|
+
const output = await runner.query("network:list");
|
|
266
|
+
return output.split("\n").map((s) => s.trim()).filter(Boolean);
|
|
267
|
+
}
|
|
268
|
+
async function exportAppNetwork(runner, app) {
|
|
269
|
+
const output = await runner.query("network:report", app);
|
|
270
|
+
if (!output) return void 0;
|
|
271
|
+
const result = {};
|
|
272
|
+
const lines = output.split("\n");
|
|
273
|
+
for (const line of lines) {
|
|
274
|
+
const [key, ...valueParts] = line.split(":").map((s) => s.trim());
|
|
275
|
+
const value = valueParts.join(":").trim();
|
|
276
|
+
if (!key || !value) continue;
|
|
277
|
+
if (key === "Network attach post deploy") {
|
|
278
|
+
result.networks = value.split(" ").filter(Boolean);
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
return Object.keys(result).length > 0 ? result : void 0;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// src/modules/services.ts
|
|
285
|
+
async function ensureServices(runner, services) {
|
|
286
|
+
for (const [name, config] of Object.entries(services)) {
|
|
287
|
+
logAction("services", `Ensuring ${name}`);
|
|
288
|
+
const exists = await runner.check(`${config.plugin}:exists`, name);
|
|
289
|
+
if (exists) {
|
|
290
|
+
logSkip();
|
|
291
|
+
continue;
|
|
292
|
+
}
|
|
293
|
+
await runner.run(`${config.plugin}:create`, name);
|
|
294
|
+
logDone();
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
async function ensureAppLinks(runner, app, desiredLinks, allServices) {
|
|
298
|
+
const desiredSet = new Set(desiredLinks);
|
|
299
|
+
for (const [serviceName, serviceConfig] of Object.entries(allServices)) {
|
|
300
|
+
const isLinked = await runner.check(`${serviceConfig.plugin}:linked`, serviceName, app);
|
|
301
|
+
const isDesired = desiredSet.has(serviceName);
|
|
302
|
+
if (isDesired && !isLinked) {
|
|
303
|
+
logAction(app, `Linking ${serviceName}`);
|
|
304
|
+
await runner.run(`${serviceConfig.plugin}:link`, serviceName, app, "--no-restart");
|
|
305
|
+
logDone();
|
|
306
|
+
} else if (!isDesired && isLinked) {
|
|
307
|
+
logAction(app, `Unlinking ${serviceName}`);
|
|
308
|
+
await runner.run(`${serviceConfig.plugin}:unlink`, serviceName, app, "--no-restart");
|
|
309
|
+
logDone();
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
async function destroyAppLinks(runner, app, links, allServices) {
|
|
314
|
+
for (const serviceName of links) {
|
|
315
|
+
const config = allServices[serviceName];
|
|
316
|
+
if (!config) continue;
|
|
317
|
+
const isLinked = await runner.check(`${config.plugin}:linked`, serviceName, app);
|
|
318
|
+
if (isLinked) {
|
|
319
|
+
await runner.run(`${config.plugin}:unlink`, serviceName, app, "--no-restart");
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
async function destroyServices(runner, services) {
|
|
324
|
+
for (const [name, config] of Object.entries(services)) {
|
|
325
|
+
logAction("services", `Destroying ${name}`);
|
|
326
|
+
const exists = await runner.check(`${config.plugin}:exists`, name);
|
|
327
|
+
if (!exists) {
|
|
328
|
+
logSkip();
|
|
329
|
+
continue;
|
|
330
|
+
}
|
|
331
|
+
await runner.run(`${config.plugin}:destroy`, name, "--force");
|
|
332
|
+
logDone();
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
async function exportServices(_runner) {
|
|
336
|
+
return {};
|
|
337
|
+
}
|
|
338
|
+
async function exportAppLinks(runner, app, services) {
|
|
339
|
+
const linked = [];
|
|
340
|
+
for (const [serviceName, config] of Object.entries(services)) {
|
|
341
|
+
const isLinked = await runner.check(`${config.plugin}:linked`, serviceName, app);
|
|
342
|
+
if (isLinked) linked.push(serviceName);
|
|
343
|
+
}
|
|
344
|
+
return linked;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
// src/modules/proxy.ts
|
|
348
|
+
async function ensureAppProxy(runner, app, enabled) {
|
|
349
|
+
logAction(app, `Setting proxy enabled=${enabled}`);
|
|
350
|
+
const currentRaw = await runner.query("proxy:report", app, "--proxy-enabled");
|
|
351
|
+
const current = currentRaw.trim() === "true";
|
|
352
|
+
if (current === enabled) {
|
|
353
|
+
logSkip();
|
|
354
|
+
return;
|
|
355
|
+
}
|
|
356
|
+
if (enabled) {
|
|
357
|
+
await runner.run("proxy:enable", app);
|
|
358
|
+
} else {
|
|
359
|
+
await runner.run("proxy:disable", app);
|
|
360
|
+
}
|
|
361
|
+
logDone();
|
|
362
|
+
}
|
|
363
|
+
async function exportAppProxy(runner, app) {
|
|
364
|
+
const raw = await runner.query("proxy:report", app, "--proxy-enabled");
|
|
365
|
+
const enabled = raw.trim() === "true";
|
|
366
|
+
if (enabled) return void 0;
|
|
367
|
+
return { enabled };
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
// src/modules/ports.ts
|
|
371
|
+
async function ensureAppPorts(runner, app, ports) {
|
|
372
|
+
logAction(app, "Configuring ports");
|
|
373
|
+
const currentRaw = await runner.query("ports:report", app, "--ports-map");
|
|
374
|
+
const current = currentRaw.split(/\s+/).map((s) => s.trim()).filter(Boolean).sort();
|
|
375
|
+
const desired = [...ports].sort();
|
|
376
|
+
if (JSON.stringify(current) === JSON.stringify(desired)) {
|
|
377
|
+
logSkip();
|
|
378
|
+
return;
|
|
379
|
+
}
|
|
380
|
+
await runner.run("ports:set", app, ...ports);
|
|
381
|
+
logDone();
|
|
382
|
+
}
|
|
383
|
+
async function exportAppPorts(runner, app) {
|
|
384
|
+
const raw = await runner.query("ports:report", app, "--ports-map");
|
|
385
|
+
const ports = raw.split(/\s+/).map((s) => s.trim()).filter(Boolean);
|
|
386
|
+
if (ports.length === 0) return void 0;
|
|
387
|
+
return ports;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
// src/modules/certs.ts
|
|
391
|
+
async function ensureAppCerts(runner, app, ssl) {
|
|
392
|
+
logAction(app, "Configuring SSL");
|
|
393
|
+
const enabledRaw = await runner.query("certs:report", app, "--ssl-enabled");
|
|
394
|
+
const enabled = enabledRaw.trim() === "true";
|
|
395
|
+
if (ssl === false) {
|
|
396
|
+
if (!enabled) {
|
|
397
|
+
logSkip();
|
|
398
|
+
return;
|
|
399
|
+
}
|
|
400
|
+
await runner.run("certs:remove", app);
|
|
401
|
+
logDone();
|
|
402
|
+
return;
|
|
403
|
+
}
|
|
404
|
+
if (ssl === true) {
|
|
405
|
+
if (enabled) {
|
|
406
|
+
logSkip();
|
|
407
|
+
return;
|
|
408
|
+
}
|
|
409
|
+
logSkip();
|
|
410
|
+
return;
|
|
411
|
+
}
|
|
412
|
+
if (enabled) {
|
|
413
|
+
logSkip();
|
|
414
|
+
return;
|
|
415
|
+
}
|
|
416
|
+
await runner.run("certs:add", app, ssl.certfile, ssl.keyfile);
|
|
417
|
+
logDone();
|
|
418
|
+
}
|
|
419
|
+
async function exportAppCerts(runner, app) {
|
|
420
|
+
const raw = await runner.query("certs:report", app, "--ssl-enabled");
|
|
421
|
+
const enabled = raw.trim() === "true";
|
|
422
|
+
if (!enabled) return void 0;
|
|
423
|
+
return true;
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
// src/modules/storage.ts
|
|
427
|
+
async function ensureAppStorage(runner, app, storage) {
|
|
428
|
+
logAction(app, "Configuring storage");
|
|
429
|
+
const currentRaw = await runner.query("storage:report", app, "--storage-mounts");
|
|
430
|
+
const current = currentRaw.split("\n").map((s) => s.trim()).filter(Boolean);
|
|
431
|
+
const desired = new Set(storage);
|
|
432
|
+
const currentSet = new Set(current);
|
|
433
|
+
const toUnmount = current.filter((m) => !desired.has(m));
|
|
434
|
+
const toMount = storage.filter((m) => !currentSet.has(m));
|
|
435
|
+
if (toUnmount.length === 0 && toMount.length === 0) {
|
|
436
|
+
logSkip();
|
|
437
|
+
return;
|
|
438
|
+
}
|
|
439
|
+
for (const mount of toUnmount) {
|
|
440
|
+
await runner.run("storage:unmount", app, mount);
|
|
441
|
+
}
|
|
442
|
+
for (const mount of toMount) {
|
|
443
|
+
await runner.run("storage:mount", app, mount);
|
|
444
|
+
}
|
|
445
|
+
logDone();
|
|
446
|
+
}
|
|
447
|
+
async function exportAppStorage(runner, app) {
|
|
448
|
+
const raw = await runner.query("storage:report", app, "--storage-mounts");
|
|
449
|
+
const mounts = raw.split("\n").map((s) => s.trim()).filter(Boolean);
|
|
450
|
+
if (mounts.length === 0) return void 0;
|
|
451
|
+
return mounts;
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
// src/modules/nginx.ts
|
|
455
|
+
async function ensureAppNginx(runner, app, nginx) {
|
|
456
|
+
for (const [key, value] of Object.entries(nginx)) {
|
|
457
|
+
await runner.run("nginx:set", app, key, String(value));
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
async function ensureGlobalNginx(runner, nginx) {
|
|
461
|
+
for (const [key, value] of Object.entries(nginx)) {
|
|
462
|
+
await runner.run("nginx:set", "--global", key, String(value));
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
async function exportAppNginx(runner, app) {
|
|
466
|
+
const raw = await runner.query("nginx:report", app);
|
|
467
|
+
if (!raw) return void 0;
|
|
468
|
+
const result = {};
|
|
469
|
+
for (const line of raw.split("\n")) {
|
|
470
|
+
const match = line.match(/^\s*Nginx\s+(.+?):\s*(.+?)\s*$/);
|
|
471
|
+
if (match) {
|
|
472
|
+
const key = match[1].toLowerCase().replace(/\s+/g, "-");
|
|
473
|
+
result[key] = match[2];
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
return Object.keys(result).length > 0 ? result : void 0;
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
// src/modules/checks.ts
|
|
480
|
+
async function ensureAppChecks(runner, app, checks) {
|
|
481
|
+
logAction(app, "Configuring checks");
|
|
482
|
+
if (checks === false) {
|
|
483
|
+
await runner.run("checks:disable", app);
|
|
484
|
+
logDone();
|
|
485
|
+
return;
|
|
486
|
+
}
|
|
487
|
+
if (checks.disabled && checks.disabled.length > 0) {
|
|
488
|
+
await runner.run("checks:disable", app, ...checks.disabled);
|
|
489
|
+
}
|
|
490
|
+
if (checks.skipped && checks.skipped.length > 0) {
|
|
491
|
+
await runner.run("checks:skip", app, ...checks.skipped);
|
|
492
|
+
}
|
|
493
|
+
for (const [key, value] of Object.entries(checks)) {
|
|
494
|
+
if (key === "disabled" || key === "skipped") continue;
|
|
495
|
+
await runner.run("checks:set", app, key, String(value));
|
|
496
|
+
}
|
|
497
|
+
logDone();
|
|
498
|
+
}
|
|
499
|
+
async function exportAppChecks(runner, app) {
|
|
500
|
+
const raw = await runner.query("checks:report", app);
|
|
501
|
+
if (!raw) return void 0;
|
|
502
|
+
return void 0;
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
// src/modules/logs.ts
|
|
506
|
+
async function ensureAppLogs(runner, app, logs) {
|
|
507
|
+
for (const [key, value] of Object.entries(logs)) {
|
|
508
|
+
await runner.run("logs:set", app, key, String(value));
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
async function ensureGlobalLogs(runner, logs) {
|
|
512
|
+
for (const [key, value] of Object.entries(logs)) {
|
|
513
|
+
await runner.run("logs:set", "--global", key, String(value));
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
async function exportAppLogs(runner, app) {
|
|
517
|
+
const raw = await runner.query("logs:report", app);
|
|
518
|
+
if (!raw) return void 0;
|
|
519
|
+
return void 0;
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
// src/modules/registry.ts
|
|
523
|
+
async function ensureAppRegistry(runner, app, registry) {
|
|
524
|
+
for (const [key, value] of Object.entries(registry)) {
|
|
525
|
+
await runner.run("registry:set", app, key, String(value));
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
async function exportAppRegistry(runner, app) {
|
|
529
|
+
const raw = await runner.query("registry:report", app);
|
|
530
|
+
if (!raw) return void 0;
|
|
531
|
+
return void 0;
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
// src/modules/scheduler.ts
|
|
535
|
+
async function ensureAppScheduler(runner, app, scheduler) {
|
|
536
|
+
logAction(app, `Setting scheduler to ${scheduler}`);
|
|
537
|
+
const current = await runner.query("scheduler:report", app, "--scheduler-selected");
|
|
538
|
+
if (current.trim() === scheduler) {
|
|
539
|
+
logSkip();
|
|
540
|
+
return;
|
|
541
|
+
}
|
|
542
|
+
await runner.run("scheduler:set", app, "selected", scheduler);
|
|
543
|
+
logDone();
|
|
544
|
+
}
|
|
545
|
+
async function exportAppScheduler(runner, app) {
|
|
546
|
+
const raw = await runner.query("scheduler:report", app, "--scheduler-selected");
|
|
547
|
+
const scheduler = raw.trim();
|
|
548
|
+
if (!scheduler || scheduler === "docker-local") return void 0;
|
|
549
|
+
return scheduler;
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
// src/modules/config.ts
|
|
553
|
+
var MANAGED_KEYS_VAR = "DOKKU_COMPOSE_MANAGED_KEYS";
|
|
554
|
+
async function ensureAppConfig(runner, app, env) {
|
|
555
|
+
logAction(app, "Configuring env vars");
|
|
556
|
+
if (env === false) {
|
|
557
|
+
logSkip();
|
|
558
|
+
return;
|
|
559
|
+
}
|
|
560
|
+
const prevManagedRaw = await runner.query("config:get", app, MANAGED_KEYS_VAR);
|
|
561
|
+
const prevManaged = prevManagedRaw.trim() ? prevManagedRaw.trim().split(",").filter(Boolean) : [];
|
|
562
|
+
const desiredKeys = Object.keys(env);
|
|
563
|
+
const toUnset = prevManaged.filter((k) => !desiredKeys.includes(k));
|
|
564
|
+
if (toUnset.length > 0) {
|
|
565
|
+
await runner.run("config:unset", "--no-restart", app, ...toUnset);
|
|
566
|
+
}
|
|
567
|
+
const pairs = Object.entries(env).map(([k, v]) => `${k}=${v}`);
|
|
568
|
+
const newManagedKeys = desiredKeys.join(",");
|
|
569
|
+
await runner.run(
|
|
570
|
+
"config:set",
|
|
571
|
+
"--no-restart",
|
|
572
|
+
app,
|
|
573
|
+
...pairs,
|
|
574
|
+
`${MANAGED_KEYS_VAR}=${newManagedKeys}`
|
|
575
|
+
);
|
|
576
|
+
logDone();
|
|
577
|
+
}
|
|
578
|
+
async function ensureGlobalConfig(runner, env) {
|
|
579
|
+
if (env === false) return;
|
|
580
|
+
const pairs = Object.entries(env).map(([k, v]) => `${k}=${v}`);
|
|
581
|
+
await runner.run("config:set", "--global", ...pairs);
|
|
582
|
+
}
|
|
583
|
+
async function exportAppConfig(runner, app) {
|
|
584
|
+
const raw = await runner.query("config:export", app, "--format", "shell");
|
|
585
|
+
if (!raw) return void 0;
|
|
586
|
+
const result = {};
|
|
587
|
+
for (const line of raw.split("\n")) {
|
|
588
|
+
const match = line.match(/^export\s+(\w+)=['"]?(.*?)['"]?$/);
|
|
589
|
+
if (match && match[1] !== MANAGED_KEYS_VAR) {
|
|
590
|
+
result[match[1]] = match[2];
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
return Object.keys(result).length > 0 ? result : void 0;
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
// src/modules/builder.ts
|
|
597
|
+
async function ensureAppBuilder(runner, app, build) {
|
|
598
|
+
if (build.dockerfile) {
|
|
599
|
+
await runner.run("builder-dockerfile:set", app, "dockerfile-path", build.dockerfile);
|
|
600
|
+
}
|
|
601
|
+
if (build.app_json) {
|
|
602
|
+
await runner.run("app-json:set", app, "appjson-path", build.app_json);
|
|
603
|
+
}
|
|
604
|
+
if (build.context) {
|
|
605
|
+
await runner.run("builder:set", app, "build-dir", build.context);
|
|
606
|
+
}
|
|
607
|
+
if (build.args && Object.keys(build.args).length > 0) {
|
|
608
|
+
for (const [key, value] of Object.entries(build.args)) {
|
|
609
|
+
await runner.run("docker-options:add", app, "build", `--build-arg ${key}=${value}`);
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
// src/modules/docker-options.ts
|
|
615
|
+
async function ensureAppDockerOptions(runner, app, options) {
|
|
616
|
+
const phases = ["build", "deploy", "run"];
|
|
617
|
+
for (const phase of phases) {
|
|
618
|
+
const opts = options[phase];
|
|
619
|
+
if (!opts || opts.length === 0) continue;
|
|
620
|
+
await runner.run("docker-options:clear", app, phase);
|
|
621
|
+
for (const opt of opts) {
|
|
622
|
+
await runner.run("docker-options:add", app, phase, opt);
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
// src/commands/up.ts
|
|
628
|
+
async function runUp(runner, config, appFilter) {
|
|
629
|
+
const apps = appFilter.length > 0 ? appFilter : Object.keys(config.apps);
|
|
630
|
+
if (config.plugins) await ensurePlugins(runner, config.plugins);
|
|
631
|
+
if (config.domains !== void 0) await ensureGlobalDomains(runner, config.domains);
|
|
632
|
+
if (config.env !== void 0) await ensureGlobalConfig(runner, config.env);
|
|
633
|
+
if (config.logs !== void 0) await ensureGlobalLogs(runner, config.logs);
|
|
634
|
+
if (config.nginx !== void 0) await ensureGlobalNginx(runner, config.nginx);
|
|
635
|
+
if (config.networks) await ensureNetworks(runner, config.networks);
|
|
636
|
+
if (config.services) await ensureServices(runner, config.services);
|
|
637
|
+
for (const app of apps) {
|
|
638
|
+
const appConfig = config.apps[app];
|
|
639
|
+
if (!appConfig) continue;
|
|
640
|
+
await ensureApp(runner, app);
|
|
641
|
+
await ensureAppDomains(runner, app, appConfig.domains);
|
|
642
|
+
if (config.services) await ensureAppLinks(runner, app, appConfig.links ?? [], config.services);
|
|
643
|
+
await ensureAppNetworks(runner, app, appConfig.networks);
|
|
644
|
+
await ensureAppNetwork(runner, app, appConfig.network);
|
|
645
|
+
if (appConfig.proxy) await ensureAppProxy(runner, app, appConfig.proxy.enabled);
|
|
646
|
+
if (appConfig.ports) await ensureAppPorts(runner, app, appConfig.ports);
|
|
647
|
+
if (appConfig.ssl !== void 0) await ensureAppCerts(runner, app, appConfig.ssl);
|
|
648
|
+
if (appConfig.storage) await ensureAppStorage(runner, app, appConfig.storage);
|
|
649
|
+
if (appConfig.nginx) await ensureAppNginx(runner, app, appConfig.nginx);
|
|
650
|
+
if (appConfig.checks !== void 0) await ensureAppChecks(runner, app, appConfig.checks);
|
|
651
|
+
if (appConfig.logs) await ensureAppLogs(runner, app, appConfig.logs);
|
|
652
|
+
if (appConfig.registry) await ensureAppRegistry(runner, app, appConfig.registry);
|
|
653
|
+
if (appConfig.scheduler) await ensureAppScheduler(runner, app, appConfig.scheduler);
|
|
654
|
+
if (appConfig.env !== void 0) await ensureAppConfig(runner, app, appConfig.env);
|
|
655
|
+
if (appConfig.build) await ensureAppBuilder(runner, app, appConfig.build);
|
|
656
|
+
if (appConfig.docker_options) await ensureAppDockerOptions(runner, app, appConfig.docker_options);
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
// src/commands/down.ts
|
|
661
|
+
async function runDown(runner, config, appFilter, opts) {
|
|
662
|
+
const apps = appFilter.length > 0 ? appFilter : Object.keys(config.apps);
|
|
663
|
+
for (const app of apps) {
|
|
664
|
+
const appConfig = config.apps[app];
|
|
665
|
+
if (!appConfig) continue;
|
|
666
|
+
if (config.services && appConfig.links) {
|
|
667
|
+
await destroyAppLinks(runner, app, appConfig.links, config.services);
|
|
668
|
+
}
|
|
669
|
+
await destroyApp(runner, app);
|
|
670
|
+
}
|
|
671
|
+
if (config.services) {
|
|
672
|
+
await destroyServices(runner, config.services);
|
|
673
|
+
}
|
|
674
|
+
if (config.networks) {
|
|
675
|
+
for (const net of config.networks) {
|
|
676
|
+
logAction("network", `Destroying ${net}`);
|
|
677
|
+
const exists = await runner.check("network:exists", net);
|
|
678
|
+
if (!exists) {
|
|
679
|
+
logSkip();
|
|
680
|
+
continue;
|
|
681
|
+
}
|
|
682
|
+
await runner.run("network:destroy", net, "--force");
|
|
683
|
+
logDone();
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
// src/commands/export.ts
|
|
689
|
+
async function runExport(runner, opts) {
|
|
690
|
+
const config = { apps: {} };
|
|
691
|
+
const apps = opts.appFilter?.length ? opts.appFilter : await exportApps(runner);
|
|
692
|
+
const networks = await exportNetworks(runner);
|
|
693
|
+
if (networks.length > 0) config.networks = networks;
|
|
694
|
+
const services = await exportServices(runner);
|
|
695
|
+
if (Object.keys(services).length > 0) config.services = services;
|
|
696
|
+
for (const app of apps) {
|
|
697
|
+
const appConfig = {};
|
|
698
|
+
const domains = await exportAppDomains(runner, app);
|
|
699
|
+
if (domains !== void 0) appConfig.domains = domains;
|
|
700
|
+
const links = await exportAppLinks(runner, app, services);
|
|
701
|
+
if (links.length > 0) appConfig.links = links;
|
|
702
|
+
const ports = await exportAppPorts(runner, app);
|
|
703
|
+
if (ports?.length) appConfig.ports = ports;
|
|
704
|
+
const proxy = await exportAppProxy(runner, app);
|
|
705
|
+
if (proxy !== void 0) appConfig.proxy = proxy;
|
|
706
|
+
const ssl = await exportAppCerts(runner, app);
|
|
707
|
+
if (ssl !== void 0) appConfig.ssl = ssl;
|
|
708
|
+
const storage = await exportAppStorage(runner, app);
|
|
709
|
+
if (storage?.length) appConfig.storage = storage;
|
|
710
|
+
const nginx = await exportAppNginx(runner, app);
|
|
711
|
+
if (nginx && Object.keys(nginx).length) appConfig.nginx = nginx;
|
|
712
|
+
const checks = await exportAppChecks(runner, app);
|
|
713
|
+
if (checks !== void 0) appConfig.checks = checks;
|
|
714
|
+
const logs = await exportAppLogs(runner, app);
|
|
715
|
+
if (logs && Object.keys(logs).length) appConfig.logs = logs;
|
|
716
|
+
const registry = await exportAppRegistry(runner, app);
|
|
717
|
+
if (registry && Object.keys(registry).length) appConfig.registry = registry;
|
|
718
|
+
const scheduler = await exportAppScheduler(runner, app);
|
|
719
|
+
if (scheduler) appConfig.scheduler = scheduler;
|
|
720
|
+
const networkCfg = await exportAppNetwork(runner, app);
|
|
721
|
+
if (networkCfg?.networks?.length) appConfig.networks = networkCfg.networks;
|
|
722
|
+
if (networkCfg?.network) appConfig.network = networkCfg.network;
|
|
723
|
+
const env = await exportAppConfig(runner, app);
|
|
724
|
+
if (env && Object.keys(env).length) appConfig.env = env;
|
|
725
|
+
config.apps[app] = appConfig;
|
|
726
|
+
}
|
|
727
|
+
return config;
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
// src/commands/diff.ts
|
|
731
|
+
import chalk2 from "chalk";
|
|
732
|
+
function computeDiff(desired, current) {
|
|
733
|
+
const result = { apps: {}, services: {}, inSync: true };
|
|
734
|
+
for (const [app, desiredApp] of Object.entries(desired.apps)) {
|
|
735
|
+
const currentApp = current.apps[app] ?? {};
|
|
736
|
+
const appDiff = {};
|
|
737
|
+
const features = [
|
|
738
|
+
"domains",
|
|
739
|
+
"ports",
|
|
740
|
+
"env",
|
|
741
|
+
"ssl",
|
|
742
|
+
"storage",
|
|
743
|
+
"nginx",
|
|
744
|
+
"logs",
|
|
745
|
+
"registry",
|
|
746
|
+
"scheduler",
|
|
747
|
+
"checks",
|
|
748
|
+
"networks",
|
|
749
|
+
"proxy",
|
|
750
|
+
"links"
|
|
751
|
+
];
|
|
752
|
+
for (const feature of features) {
|
|
753
|
+
const d = desiredApp[feature];
|
|
754
|
+
const c = currentApp[feature];
|
|
755
|
+
if (d === void 0) continue;
|
|
756
|
+
const dStr = JSON.stringify(d);
|
|
757
|
+
const cStr = JSON.stringify(c);
|
|
758
|
+
if (c === void 0) {
|
|
759
|
+
appDiff[feature] = { status: "missing", desired: d, current: void 0 };
|
|
760
|
+
result.inSync = false;
|
|
761
|
+
} else if (dStr !== cStr) {
|
|
762
|
+
appDiff[feature] = { status: "changed", desired: d, current: c };
|
|
763
|
+
result.inSync = false;
|
|
764
|
+
} else {
|
|
765
|
+
appDiff[feature] = { status: "in-sync", desired: d, current: c };
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
result.apps[app] = appDiff;
|
|
769
|
+
}
|
|
770
|
+
for (const [svc] of Object.entries(desired.services ?? {})) {
|
|
771
|
+
const exists = current.services?.[svc];
|
|
772
|
+
if (!exists) {
|
|
773
|
+
result.services[svc] = { status: "missing" };
|
|
774
|
+
result.inSync = false;
|
|
775
|
+
} else {
|
|
776
|
+
result.services[svc] = { status: "in-sync" };
|
|
777
|
+
}
|
|
778
|
+
}
|
|
779
|
+
return result;
|
|
780
|
+
}
|
|
781
|
+
function formatSummary(diff) {
|
|
782
|
+
const lines = [""];
|
|
783
|
+
for (const [app, appDiff] of Object.entries(diff.apps)) {
|
|
784
|
+
const changes = Object.entries(appDiff).filter(([, d]) => d.status !== "in-sync");
|
|
785
|
+
if (changes.length === 0) {
|
|
786
|
+
lines.push(` app: ${app}`);
|
|
787
|
+
lines.push(` (in sync)`);
|
|
788
|
+
} else {
|
|
789
|
+
lines.push(` app: ${chalk2.bold(app)}`);
|
|
790
|
+
for (const [feature, d] of changes) {
|
|
791
|
+
const sym = d.status === "missing" ? chalk2.green("+") : chalk2.yellow("~");
|
|
792
|
+
lines.push(` ${sym} ${feature}: ${formatFeatureSummary(d)}`);
|
|
793
|
+
}
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
for (const [svc, d] of Object.entries(diff.services)) {
|
|
797
|
+
if (d.status === "missing") {
|
|
798
|
+
lines.push(` services:`);
|
|
799
|
+
lines.push(` ${chalk2.green("+")} ${svc}: not provisioned`);
|
|
800
|
+
}
|
|
801
|
+
}
|
|
802
|
+
const total = Object.values(diff.apps).flatMap((a) => Object.values(a)).filter((d) => d.status !== "in-sync").length + Object.values(diff.services).filter((d) => d.status !== "in-sync").length;
|
|
803
|
+
lines.push("");
|
|
804
|
+
if (total === 0) {
|
|
805
|
+
lines.push(chalk2.green(" Everything in sync."));
|
|
806
|
+
} else {
|
|
807
|
+
lines.push(chalk2.yellow(` ${total} resource(s) out of sync.`));
|
|
808
|
+
}
|
|
809
|
+
lines.push("");
|
|
810
|
+
return lines.join("\n");
|
|
811
|
+
}
|
|
812
|
+
function formatFeatureSummary(d) {
|
|
813
|
+
if (d.status === "missing") return "(not set on server)";
|
|
814
|
+
if (Array.isArray(d.desired) && Array.isArray(d.current)) {
|
|
815
|
+
return `${d.current.length} \u2192 ${d.desired.length} items`;
|
|
816
|
+
}
|
|
817
|
+
return `${JSON.stringify(d.current)} \u2192 ${JSON.stringify(d.desired)}`;
|
|
818
|
+
}
|
|
819
|
+
function formatVerbose(diff) {
|
|
820
|
+
const lines = [""];
|
|
821
|
+
for (const [app, appDiff] of Object.entries(diff.apps)) {
|
|
822
|
+
const changes = Object.entries(appDiff).filter(([, d]) => d.status !== "in-sync");
|
|
823
|
+
if (changes.length === 0) continue;
|
|
824
|
+
for (const [feature, d] of changes) {
|
|
825
|
+
lines.push(`@@ app: ${app} / ${feature} @@`);
|
|
826
|
+
const currentLines = d.current !== void 0 ? JSON.stringify(d.current, null, 2).split("\n") : [];
|
|
827
|
+
const desiredLines = JSON.stringify(d.desired, null, 2).split("\n");
|
|
828
|
+
for (const line of currentLines) lines.push(chalk2.red(`- ${line}`));
|
|
829
|
+
for (const line of desiredLines) lines.push(chalk2.green(`+ ${line}`));
|
|
830
|
+
}
|
|
831
|
+
}
|
|
832
|
+
for (const [svc, d] of Object.entries(diff.services)) {
|
|
833
|
+
if (d.status === "missing") {
|
|
834
|
+
lines.push(`@@ services @@`);
|
|
835
|
+
lines.push(chalk2.green(`+ ${svc}`));
|
|
836
|
+
}
|
|
837
|
+
}
|
|
838
|
+
lines.push("");
|
|
839
|
+
return lines.join("\n");
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
// src/commands/validate.ts
|
|
843
|
+
import * as fs2 from "fs";
|
|
844
|
+
import * as yaml2 from "js-yaml";
|
|
845
|
+
function validate(filePath) {
|
|
846
|
+
const errors = [];
|
|
847
|
+
const warnings = [];
|
|
848
|
+
if (!fs2.existsSync(filePath)) {
|
|
849
|
+
return { errors: [`File not found: ${filePath}`], warnings };
|
|
850
|
+
}
|
|
851
|
+
let raw;
|
|
852
|
+
try {
|
|
853
|
+
raw = yaml2.load(fs2.readFileSync(filePath, "utf8"));
|
|
854
|
+
} catch (e) {
|
|
855
|
+
return { errors: [`YAML parse error: ${e.message}`], warnings };
|
|
856
|
+
}
|
|
857
|
+
const result = ConfigSchema.safeParse(raw);
|
|
858
|
+
if (!result.success) {
|
|
859
|
+
for (const issue of result.error.issues) {
|
|
860
|
+
const issuePath = issue.path.join(".");
|
|
861
|
+
errors.push(`${issuePath}: ${issue.message}`);
|
|
862
|
+
}
|
|
863
|
+
}
|
|
864
|
+
const data = raw;
|
|
865
|
+
if (data?.apps && typeof data.apps === "object") {
|
|
866
|
+
const serviceNames = new Set(
|
|
867
|
+
data?.services && typeof data.services === "object" ? Object.keys(data.services) : []
|
|
868
|
+
);
|
|
869
|
+
for (const [appName, appCfg] of Object.entries(data.apps)) {
|
|
870
|
+
if (!appCfg?.links) continue;
|
|
871
|
+
for (const link of appCfg.links) {
|
|
872
|
+
if (!serviceNames.has(link)) {
|
|
873
|
+
errors.push(`apps.${appName}.links: service "${link}" not defined in services`);
|
|
874
|
+
}
|
|
875
|
+
}
|
|
876
|
+
}
|
|
877
|
+
}
|
|
878
|
+
if (data?.services && data?.plugins) {
|
|
879
|
+
const pluginNames = new Set(Object.keys(data.plugins));
|
|
880
|
+
for (const [svcName, svcCfg] of Object.entries(data.services)) {
|
|
881
|
+
if (svcCfg?.plugin && !pluginNames.has(svcCfg.plugin)) {
|
|
882
|
+
warnings.push(`services.${svcName}.plugin: "${svcCfg.plugin}" not declared in plugins (may be pre-installed)`);
|
|
883
|
+
}
|
|
884
|
+
}
|
|
885
|
+
}
|
|
886
|
+
return { errors, warnings };
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
// src/index.ts
|
|
890
|
+
var program = new Command().name("dokku-compose").version("0.3.0");
|
|
891
|
+
function makeRunner(opts) {
|
|
892
|
+
return createRunner({
|
|
893
|
+
host: process.env.DOKKU_HOST,
|
|
894
|
+
dryRun: opts.dryRun ?? false
|
|
895
|
+
});
|
|
896
|
+
}
|
|
897
|
+
program.command("up [apps...]").description("Create/update apps and services to match config").option("-f, --file <path>", "Config file", "dokku-compose.yml").option("--dry-run", "Print commands without executing").option("--fail-fast", "Stop on first error").action(async (apps, opts) => {
|
|
898
|
+
const config = loadConfig(opts.file);
|
|
899
|
+
const runner = makeRunner(opts);
|
|
900
|
+
await runUp(runner, config, apps);
|
|
901
|
+
if (opts.dryRun) {
|
|
902
|
+
console.log("\n# Commands that would run:");
|
|
903
|
+
for (const cmd of runner.dryRunLog) console.log(`dokku ${cmd}`);
|
|
904
|
+
}
|
|
905
|
+
});
|
|
906
|
+
program.command("down [apps...]").description("Destroy apps and services (requires --force)").option("-f, --file <path>", "Config file", "dokku-compose.yml").option("--force", "Required to destroy apps").action(async (apps, opts) => {
|
|
907
|
+
if (!opts.force) {
|
|
908
|
+
console.error("--force required");
|
|
909
|
+
process.exit(1);
|
|
910
|
+
}
|
|
911
|
+
const config = loadConfig(opts.file);
|
|
912
|
+
const runner = makeRunner({});
|
|
913
|
+
await runDown(runner, config, apps, { force: true });
|
|
914
|
+
});
|
|
915
|
+
program.command("validate [file]").description("Validate dokku-compose.yml without touching the server").action((file = "dokku-compose.yml") => {
|
|
916
|
+
const result = validate(file);
|
|
917
|
+
for (const w of result.warnings) console.warn(`WARN: ${w}`);
|
|
918
|
+
for (const e of result.errors) console.error(`ERROR: ${e}`);
|
|
919
|
+
if (result.errors.length > 0) {
|
|
920
|
+
console.error(`
|
|
921
|
+
${result.errors.length} error(s), ${result.warnings.length} warning(s)`);
|
|
922
|
+
process.exit(1);
|
|
923
|
+
}
|
|
924
|
+
if (result.warnings.length > 0) {
|
|
925
|
+
console.log(`
|
|
926
|
+
0 errors, ${result.warnings.length} warning(s)`);
|
|
927
|
+
} else {
|
|
928
|
+
console.log("Valid.");
|
|
929
|
+
}
|
|
930
|
+
});
|
|
931
|
+
program.command("export").description("Export server state to dokku-compose.yml format").option("--app <app>", "Export only a specific app").option("-o, --output <path>", "Write to file instead of stdout").action(async (opts) => {
|
|
932
|
+
const runner = makeRunner({});
|
|
933
|
+
const result = await runExport(runner, {
|
|
934
|
+
appFilter: opts.app ? [opts.app] : void 0
|
|
935
|
+
});
|
|
936
|
+
const out = yaml3.dump(result, { lineWidth: 120 });
|
|
937
|
+
if (opts.output) {
|
|
938
|
+
fs3.writeFileSync(opts.output, out);
|
|
939
|
+
console.error(`Written to ${opts.output}`);
|
|
940
|
+
} else {
|
|
941
|
+
process.stdout.write(out);
|
|
942
|
+
}
|
|
943
|
+
});
|
|
944
|
+
program.command("diff").description("Show what is out of sync between config and server").option("-f, --file <path>", "Config file", "dokku-compose.yml").option("--verbose", "Show git-style +/- diff").action(async (opts) => {
|
|
945
|
+
const desired = loadConfig(opts.file);
|
|
946
|
+
const runner = makeRunner({});
|
|
947
|
+
const current = await runExport(runner, {
|
|
948
|
+
appFilter: Object.keys(desired.apps)
|
|
949
|
+
});
|
|
950
|
+
const diff = computeDiff(desired, current);
|
|
951
|
+
const output = opts.verbose ? formatVerbose(diff) : formatSummary(diff);
|
|
952
|
+
process.stdout.write(output);
|
|
953
|
+
process.exit(diff.inSync ? 0 : 1);
|
|
954
|
+
});
|
|
955
|
+
program.command("ps [apps...]").description("Show status of configured apps").option("-f, --file <path>", "Config file", "dokku-compose.yml").action(async (apps, opts) => {
|
|
956
|
+
const config = loadConfig(opts.file);
|
|
957
|
+
const runner = makeRunner({});
|
|
958
|
+
const { runPs } = await import("./ps-33II4UU3.js");
|
|
959
|
+
await runPs(runner, config, apps);
|
|
960
|
+
});
|
|
961
|
+
program.command("init [apps...]").description("Create a starter dokku-compose.yml").option("-f, --file <path>", "Config file", "dokku-compose.yml").action(async (apps, opts) => {
|
|
962
|
+
const { runInit } = await import("./init-GIXEVLNW.js");
|
|
963
|
+
runInit(opts.file, apps);
|
|
964
|
+
});
|
|
965
|
+
program.parse();
|