dokku-compose 0.12.0 → 0.13.1
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 +43 -7
- package/dist/chunk-3W53KPE4.js +89 -0
- package/dist/index.js +585 -148
- package/dist/{ps-33II4UU3.js → ps-IWBYZRDO.js} +5 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -30,7 +30,7 @@ Like Docker Compose, but for Dokku.
|
|
|
30
30
|
npm install -g dokku-compose
|
|
31
31
|
```
|
|
32
32
|
|
|
33
|
-
Requires Node.js >= 20. See the [Installation Reference](docs/reference/install.md) for details.
|
|
33
|
+
Requires Node.js >= 20 and a server running Dokku >= 0.38.22 (every server command stops on an older one; dokku-compose never installs or upgrades Dokku). See the [Installation Reference](docs/reference/install.md) for details.
|
|
34
34
|
|
|
35
35
|
### 1. Export your existing server
|
|
36
36
|
|
|
@@ -101,9 +101,32 @@ Running `up` again produces no changes — every step checks current state befor
|
|
|
101
101
|
| `--dry-run` | Print commands without executing |
|
|
102
102
|
| `--sensitive` | Show sensitive values in output (masked by default) |
|
|
103
103
|
| `--fail-fast` | Stop on first error (default: continue to next app) |
|
|
104
|
+
| `--prune` | `up` only: also reset settings the file no longer declares (read `diff` first) |
|
|
104
105
|
| `--remove-orphans` | Destroy services and networks not in config |
|
|
105
106
|
| `--verbose` | Show git-style +/- diff (diff command only) |
|
|
106
107
|
|
|
108
|
+
### Settings removed from the file
|
|
109
|
+
|
|
110
|
+
`diff` marks with a red `-` every setting the server still has that the file no longer declares
|
|
111
|
+
(managed env keys, nginx, logs, registry, scheduler, checks, `ps` and network properties, build
|
|
112
|
+
args, and entries missing from a declared list), including when the whole block is gone. Only
|
|
113
|
+
settable keys count, never read-only report lines, and a `-` counts as drift. It also warns when
|
|
114
|
+
an app has no `links:` but has services linked on the server, because `up` unlinks them.
|
|
115
|
+
|
|
116
|
+
`up` always unsets env keys removed from the file. For every other removal it leaves the setting in place and prints a one-line count per app of the leftovers it noticed in the blocks you declare; it does not look at removed whole blocks or checks. Run `dokku-compose diff` for the full list.
|
|
117
|
+
|
|
118
|
+
**`up --prune`** resets those leftovers. Read `diff` first: every `-` it shows for the resources below is what `--prune` will reset, and there is no prompt (`diff` is the preview, the flag is the consent). Add `--dry-run` to print the reset commands without running them.
|
|
119
|
+
|
|
120
|
+
| Resource | Reset command |
|
|
121
|
+
|----------|---------------|
|
|
122
|
+
| `nginx`, `logs`, `registry`, `ps` | `<plugin>:set <app> <key>` with no value (Dokku's default); nginx then rebuilds the proxy config |
|
|
123
|
+
| `scheduler` | `scheduler:set <app> selected` |
|
|
124
|
+
| `network` properties | `network:set <app> <key>` with no value (`attach_post_create` becomes `attach-post-create`, and so on) |
|
|
125
|
+
| `checks` | `checks:enable <app> <types>` for the process types of a removed `disabled`/`skipped` list that the file declares in no list (a type the file does declare is left, with a warning naming the command to run by hand), `checks:set <app> wait-to-retire` |
|
|
126
|
+
| `build.args` | `docker-options:remove <app> build "--build-arg KEY=VALUE"` for each stale key, by the exact option text (every occurrence); other build options stay |
|
|
127
|
+
|
|
128
|
+
It covers a block that is gone from the file as well as keys removed from a declared block, and it reads the app once more for a block the file no longer has. `--prune` never destroys an app, service or network, never uninstalls a plugin, never changes `scale`, and does not clear whole `domains`, `ports`, `storage` or `networks` lists because their key is missing (those already prune inside a block the file declares). `proxy` is not a removal. `networks:` owns `attach-post-deploy`: `--prune` does not touch it, and a missing `networks:` key does not clear it (that could detach a running app from its network on the next deploy); `networks: []` does. After `up --prune`, `diff` shows no `-` for the resources above, except for checks that were skipped with a warning (their `-` lines stay).
|
|
129
|
+
|
|
107
130
|
### Errors
|
|
108
131
|
|
|
109
132
|
A failing Dokku command (mutation) is never reported as a success. On `up` and `down`, a
|
|
@@ -123,7 +146,7 @@ All features are idempotent — running `up` twice produces no changes.
|
|
|
123
146
|
|---------|-------------|-----------|
|
|
124
147
|
| Apps | Create and destroy Dokku apps | [apps](docs/reference/apps.md) |
|
|
125
148
|
| Dokku Version | Warn when the server's Dokku version is older than the pinned floor | [dokku](docs/reference/dokku.md) |
|
|
126
|
-
| Environment Variables | Set config vars per app or globally, with full convergence | [config](docs/reference/config.md) |
|
|
149
|
+
| Environment Variables | Set config vars per app or globally, with full convergence (keys removed from the YAML are unset) | [config](docs/reference/config.md) |
|
|
127
150
|
| Build | Dockerfile path, build context, app.json, build args | [builder](docs/reference/builder.md) |
|
|
128
151
|
| Docker Options | Custom Docker options per phase (build/deploy/run) | [docker_options](docs/reference/docker_options.md) |
|
|
129
152
|
| Networks | Create shared Docker networks, attach to apps | [network](docs/reference/network.md) |
|
|
@@ -131,16 +154,29 @@ All features are idempotent — running `up` twice produces no changes.
|
|
|
131
154
|
| Port Mappings | Map external ports to container ports | [ports](docs/reference/ports.md) |
|
|
132
155
|
| SSL Certificates | Add or remove SSL certs | [certs](docs/reference/certs.md) |
|
|
133
156
|
| Proxy | Enable/disable proxy, select implementation | [proxy](docs/reference/proxy.md) |
|
|
134
|
-
| Storage | Persistent bind mounts with full convergence | [storage](docs/reference/storage.md) |
|
|
157
|
+
| Storage | Persistent bind mounts with full convergence (keys removed from the YAML are unset) | [storage](docs/reference/storage.md) |
|
|
135
158
|
| Nginx | Set any nginx property per app or globally | [nginx](docs/reference/nginx.md) |
|
|
136
159
|
| Zero-Downtime Checks | Configure deploy checks, disable per process type | [checks](docs/reference/checks.md) |
|
|
137
160
|
| Log Management | Log retention and vector sink configuration | [logs](docs/reference/logs.md) |
|
|
138
|
-
| Plugins | Install Dokku plugins
|
|
161
|
+
| Plugins | Install Dokku plugins at pinned versions, updating forward only | [plugins](docs/reference/plugins.md) |
|
|
162
|
+
| Process Scale & ps | Declare process counts (`ps:scale`) and ps properties (`ps:set`) | [scale](docs/reference/scale.md) |
|
|
139
163
|
| Docker Auth | Authenticate the host docker daemon to private registries (e.g. ghcr.io) for `git:from-image` flows | [docker_auth](docs/reference/docker_auth.md) |
|
|
140
164
|
| Postgres | Postgres services with optional S3 backups | [postgres](docs/reference/postgres.md) |
|
|
141
165
|
| Redis | Redis service instances | [redis](docs/reference/redis.md) |
|
|
142
166
|
| Service Links | Link postgres/redis services to apps | [plugins](docs/reference/plugins.md#linking-services-to-apps-appsapplinks) |
|
|
143
167
|
|
|
168
|
+
## Variable interpolation
|
|
169
|
+
|
|
170
|
+
`${VAR}` in a string value is replaced with the environment variable `VAR`. The file is parsed as YAML first and substituted afterwards, value by value, so a secret can contain any character (`"`, `'`, `#`, `\`, `:`, newlines) and arrives in the value unchanged; it can never change the file's structure. Keys and comments are never substituted. Every unset or empty variable is collected and reported together:
|
|
171
|
+
|
|
172
|
+
```
|
|
173
|
+
Unset environment variable(s) referenced in config: R2_HOST, GITHUB_TOKEN
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
To write a literal `${`, use `$${`: `"$${NOT_A_VAR}"` loads as `${NOT_A_VAR}`.
|
|
177
|
+
|
|
178
|
+
Because substitution happens after parsing, a value that is a bare `${VAR}` is always a string, and a variable cannot inject YAML (extra keys, lists, quoting). Earlier versions substituted into the raw text, so anything that relied on that must now be written in the file.
|
|
179
|
+
|
|
144
180
|
## Execution Modes
|
|
145
181
|
|
|
146
182
|
```bash
|
|
@@ -197,7 +233,7 @@ dokku-compose/
|
|
|
197
233
|
│ │ └── validate.ts # validate command (offline)
|
|
198
234
|
│ └── tests/
|
|
199
235
|
│ ├── fixtures/ # Test YAML configs
|
|
200
|
-
│ └── *.test.ts # Unit tests
|
|
236
|
+
│ └── *.test.ts # Unit tests, beside src/modules and src/resources
|
|
201
237
|
└── dokku-compose.yml.example
|
|
202
238
|
```
|
|
203
239
|
|
|
@@ -215,8 +251,8 @@ bun install
|
|
|
215
251
|
# Run all tests
|
|
216
252
|
bun test
|
|
217
253
|
|
|
218
|
-
# Run a specific
|
|
219
|
-
bun test src/
|
|
254
|
+
# Run a specific file's tests
|
|
255
|
+
bun test src/resources/config.test.ts
|
|
220
256
|
```
|
|
221
257
|
|
|
222
258
|
Tests use [Bun's test runner](https://bun.sh/docs/cli/test) with a mocked `Runner` — no real Dokku server needed.
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
// src/modules/version.ts
|
|
2
|
+
import { createRequire } from "module";
|
|
3
|
+
|
|
4
|
+
// src/core/logger.ts
|
|
5
|
+
import chalk from "chalk";
|
|
6
|
+
function logAction(context, message) {
|
|
7
|
+
process.stdout.write(chalk.blue(`[${context.padEnd(12)}]`) + ` ${message}`);
|
|
8
|
+
}
|
|
9
|
+
function logDone() {
|
|
10
|
+
console.log(`... ${chalk.green("done")}`);
|
|
11
|
+
}
|
|
12
|
+
function logSkip() {
|
|
13
|
+
console.log(`... ${chalk.yellow("already configured")}`);
|
|
14
|
+
}
|
|
15
|
+
function logError(context, message) {
|
|
16
|
+
console.error(chalk.red(`[${context.padEnd(12)}] ERROR: ${message}`));
|
|
17
|
+
}
|
|
18
|
+
function logWarn(context, message) {
|
|
19
|
+
console.warn(chalk.yellow(`[${context.padEnd(12)}] WARN: ${message}`));
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// src/modules/version.ts
|
|
23
|
+
var MIN_DOKKU_VERSION = "0.38.22";
|
|
24
|
+
var SEMVER_RE = /^(\d+)\.(\d+)\.(\d+)/;
|
|
25
|
+
function parseSemver(input) {
|
|
26
|
+
const m = input.match(SEMVER_RE);
|
|
27
|
+
if (!m) throw new Error(`Cannot parse version: ${input}`);
|
|
28
|
+
return [Number(m[1]), Number(m[2]), Number(m[3])];
|
|
29
|
+
}
|
|
30
|
+
function extractServerVersion(output) {
|
|
31
|
+
const match = output.match(/(\d+\.\d+\.\d+)/);
|
|
32
|
+
return match ? match[1] : null;
|
|
33
|
+
}
|
|
34
|
+
function compareSemver(a, b) {
|
|
35
|
+
const [aMaj, aMin, aPatch] = parseSemver(a);
|
|
36
|
+
const [bMaj, bMin, bPatch] = parseSemver(b);
|
|
37
|
+
if (aMaj !== bMaj) return aMaj < bMaj ? -1 : 1;
|
|
38
|
+
if (aMin !== bMin) return aMin < bMin ? -1 : 1;
|
|
39
|
+
if (aPatch !== bPatch) return aPatch < bPatch ? -1 : 1;
|
|
40
|
+
return 0;
|
|
41
|
+
}
|
|
42
|
+
function toolVersion() {
|
|
43
|
+
const require2 = createRequire(import.meta.url);
|
|
44
|
+
for (const p of ["../package.json", "../../package.json"]) {
|
|
45
|
+
try {
|
|
46
|
+
return require2(p).version;
|
|
47
|
+
} catch {
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return "unknown";
|
|
51
|
+
}
|
|
52
|
+
async function assertDokkuSupported(ctx) {
|
|
53
|
+
const output = await ctx.query("version");
|
|
54
|
+
const server = extractServerVersion(output);
|
|
55
|
+
if (!server) {
|
|
56
|
+
throw new Error(`Cannot parse Dokku server version from output: ${output}`);
|
|
57
|
+
}
|
|
58
|
+
if (compareSemver(server, MIN_DOKKU_VERSION) === -1) {
|
|
59
|
+
throw new Error(
|
|
60
|
+
`dokku-compose ${toolVersion()} needs Dokku >= ${MIN_DOKKU_VERSION}; this server runs ${server}. Upgrade Dokku first.`
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
return server;
|
|
64
|
+
}
|
|
65
|
+
function compareToFile(server, file) {
|
|
66
|
+
if (!file) return "none";
|
|
67
|
+
const c = compareSemver(server, file);
|
|
68
|
+
return c === 0 ? "equal" : c < 0 ? "older" : "newer";
|
|
69
|
+
}
|
|
70
|
+
function enforceFileVersion(server, file) {
|
|
71
|
+
const rel = compareToFile(server, file);
|
|
72
|
+
if (rel === "older") {
|
|
73
|
+
throw new Error(`Dokku server is v${server} but the file requires >= v${file}. Upgrade Dokku or lower dokku.version.`);
|
|
74
|
+
}
|
|
75
|
+
if (rel === "newer") {
|
|
76
|
+
logWarn("dokku", `server is v${server}, newer than dokku.version ${file} in the file; update the file when ready`);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export {
|
|
81
|
+
logAction,
|
|
82
|
+
logDone,
|
|
83
|
+
logSkip,
|
|
84
|
+
logError,
|
|
85
|
+
logWarn,
|
|
86
|
+
assertDokkuSupported,
|
|
87
|
+
compareToFile,
|
|
88
|
+
enforceFileVersion
|
|
89
|
+
};
|
package/dist/index.js
CHANGED
|
@@ -1,4 +1,14 @@
|
|
|
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-3W53KPE4.js";
|
|
2
12
|
|
|
3
13
|
// src/index.ts
|
|
4
14
|
import { Command } from "commander";
|
|
@@ -53,6 +63,12 @@ var AppSchema = z.object({
|
|
|
53
63
|
logs: z.record(z.string(), z.union([z.string(), z.number()])).optional(),
|
|
54
64
|
registry: z.record(z.string(), z.union([z.string(), z.boolean()])).optional(),
|
|
55
65
|
scheduler: z.string().optional(),
|
|
66
|
+
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(),
|
|
67
|
+
ps: z.object({
|
|
68
|
+
"procfile-path": z.union([z.string(), z.number()]).optional(),
|
|
69
|
+
"restart-policy": z.union([z.string(), z.number()]).optional(),
|
|
70
|
+
"stop-timeout-seconds": z.union([z.string(), z.number()]).optional()
|
|
71
|
+
}).strict().optional(),
|
|
56
72
|
checks: ChecksSchema.optional(),
|
|
57
73
|
build: z.object({
|
|
58
74
|
dockerfile: z.string().optional(),
|
|
@@ -117,16 +133,29 @@ function parseConfig(raw) {
|
|
|
117
133
|
}
|
|
118
134
|
|
|
119
135
|
// src/core/config.ts
|
|
120
|
-
function
|
|
136
|
+
function interpolateValue(value, missing) {
|
|
137
|
+
if (typeof value === "string") {
|
|
138
|
+
return value.replace(/\$\$\{|\$\{([^}]+)\}/g, (match, name) => {
|
|
139
|
+
if (name === void 0) return "${";
|
|
140
|
+
const v = process.env[name];
|
|
141
|
+
if (v === void 0 || v === "") {
|
|
142
|
+
missing.push(name);
|
|
143
|
+
return "";
|
|
144
|
+
}
|
|
145
|
+
return v;
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
if (Array.isArray(value)) return value.map((v) => interpolateValue(v, missing));
|
|
149
|
+
if (value !== null && typeof value === "object") {
|
|
150
|
+
const out = {};
|
|
151
|
+
for (const [k, v] of Object.entries(value)) out[k] = interpolateValue(v, missing);
|
|
152
|
+
return out;
|
|
153
|
+
}
|
|
154
|
+
return value;
|
|
155
|
+
}
|
|
156
|
+
function interpolateEnvVars(raw) {
|
|
121
157
|
const missing = [];
|
|
122
|
-
const result =
|
|
123
|
-
const value = process.env[name];
|
|
124
|
-
if (value === void 0 || value === "") {
|
|
125
|
-
missing.push(name);
|
|
126
|
-
return "";
|
|
127
|
-
}
|
|
128
|
-
return value;
|
|
129
|
-
});
|
|
158
|
+
const result = interpolateValue(raw, missing);
|
|
130
159
|
if (missing.length > 0) {
|
|
131
160
|
const unique = Array.from(new Set(missing));
|
|
132
161
|
throw new Error(
|
|
@@ -140,7 +169,7 @@ function loadConfig(filePath) {
|
|
|
140
169
|
throw new Error(`Config file not found: ${filePath}`);
|
|
141
170
|
}
|
|
142
171
|
const content = fs.readFileSync(filePath, "utf8");
|
|
143
|
-
const raw = yaml.load(
|
|
172
|
+
const raw = interpolateEnvVars(yaml.load(content));
|
|
144
173
|
return parseConfig(raw);
|
|
145
174
|
}
|
|
146
175
|
|
|
@@ -279,6 +308,11 @@ function createRunner(opts = {}) {
|
|
|
279
308
|
const { stdout } = await execDokku(args);
|
|
280
309
|
return stdout.trim();
|
|
281
310
|
},
|
|
311
|
+
async queryStrict(...args) {
|
|
312
|
+
const { stdout, ok, exitCode, stderr } = await execDokku(args);
|
|
313
|
+
if (!ok) throw new DokkuCommandError(args, exitCode, stderr);
|
|
314
|
+
return stdout.trim();
|
|
315
|
+
},
|
|
282
316
|
async check(...args) {
|
|
283
317
|
const { ok } = await execDokku(args);
|
|
284
318
|
return ok;
|
|
@@ -307,6 +341,9 @@ function createContext(runner) {
|
|
|
307
341
|
}
|
|
308
342
|
return cache.get(key);
|
|
309
343
|
},
|
|
344
|
+
queryStrict(...args) {
|
|
345
|
+
return runner.queryStrict(...args);
|
|
346
|
+
},
|
|
310
347
|
check(...args) {
|
|
311
348
|
return runner.check(...args);
|
|
312
349
|
},
|
|
@@ -355,36 +392,48 @@ function computeChange(before, after) {
|
|
|
355
392
|
}
|
|
356
393
|
return { before, after, changed: before !== after };
|
|
357
394
|
}
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
process.stdout.write(chalk.blue(`[${context.padEnd(12)}]`) + ` ${message}`);
|
|
363
|
-
}
|
|
364
|
-
function logDone() {
|
|
365
|
-
console.log(`... ${chalk.green("done")}`);
|
|
366
|
-
}
|
|
367
|
-
function logSkip() {
|
|
368
|
-
console.log(`... ${chalk.yellow("already configured")}`);
|
|
369
|
-
}
|
|
370
|
-
function logError(context, message) {
|
|
371
|
-
console.error(chalk.red(`[${context.padEnd(12)}] ERROR: ${message}`));
|
|
372
|
-
}
|
|
373
|
-
function logWarn(context, message) {
|
|
374
|
-
console.warn(chalk.yellow(`[${context.padEnd(12)}] WARN: ${message}`));
|
|
395
|
+
function computeAdditiveChange(before, after) {
|
|
396
|
+
const change = computeChange(before ?? {}, after);
|
|
397
|
+
change.changed = Object.keys(change.added).length > 0 || Object.keys(change.modified).length > 0;
|
|
398
|
+
return change;
|
|
375
399
|
}
|
|
376
400
|
|
|
377
401
|
// src/core/reconcile.ts
|
|
378
|
-
async function reconcile(resource, ctx, target, desired) {
|
|
379
|
-
|
|
402
|
+
async function reconcile(resource, ctx, target, desired, onRemovals, prune = false) {
|
|
403
|
+
const canPrune = prune && !!resource.prune && !!resource.removals;
|
|
404
|
+
const pruneFrom = async (current) => {
|
|
405
|
+
const removed = resource.removals(current, desired);
|
|
406
|
+
if (removed.length === 0) return;
|
|
407
|
+
logAction(target, `${resource.key} (prune)`);
|
|
408
|
+
await resource.prune(ctx, target, removed, current, desired);
|
|
409
|
+
logDone();
|
|
410
|
+
};
|
|
411
|
+
if (desired === void 0) {
|
|
412
|
+
if (canPrune) await pruneFrom(await resource.read(ctx, target));
|
|
413
|
+
return;
|
|
414
|
+
}
|
|
380
415
|
logAction(target, `${resource.key}`);
|
|
381
416
|
if (resource.forceApply) {
|
|
382
417
|
await resource.onChange(ctx, target, { before: void 0, after: desired, changed: true });
|
|
383
418
|
logDone();
|
|
419
|
+
if (canPrune) await pruneFrom(await resource.read(ctx, target));
|
|
384
420
|
return;
|
|
385
421
|
}
|
|
386
422
|
const before = await resource.read(ctx, target);
|
|
387
|
-
|
|
423
|
+
if (onRemovals && resource.removals) onRemovals(resource.removals(before, desired));
|
|
424
|
+
if (canPrune) {
|
|
425
|
+
const change2 = resource.additive ? computeAdditiveChange(before, desired) : computeChange(before, desired);
|
|
426
|
+
const pending = resource.removals(before, desired).length > 0;
|
|
427
|
+
if (change2.changed) {
|
|
428
|
+
await resource.onChange(ctx, target, change2, pending);
|
|
429
|
+
logDone();
|
|
430
|
+
} else {
|
|
431
|
+
logSkip();
|
|
432
|
+
}
|
|
433
|
+
await pruneFrom(before);
|
|
434
|
+
return;
|
|
435
|
+
}
|
|
436
|
+
const change = resource.additive ? computeAdditiveChange(before, desired) : computeChange(before, desired);
|
|
388
437
|
if (!change.changed) {
|
|
389
438
|
logSkip();
|
|
390
439
|
return;
|
|
@@ -520,9 +569,69 @@ async function isAppDeployed(ctx, target) {
|
|
|
520
569
|
const raw = await ctx.query("ps:report", target, "--deployed");
|
|
521
570
|
return raw.trim() === "true";
|
|
522
571
|
}
|
|
572
|
+
var NGINX_KEYS = [
|
|
573
|
+
"access-log-format",
|
|
574
|
+
"access-log-path",
|
|
575
|
+
"bind-address-ipv4",
|
|
576
|
+
"bind-address-ipv6",
|
|
577
|
+
"client-body-timeout",
|
|
578
|
+
"client-header-timeout",
|
|
579
|
+
"client-max-body-size",
|
|
580
|
+
"disable-custom-config",
|
|
581
|
+
"error-log-path",
|
|
582
|
+
"hsts",
|
|
583
|
+
"hsts-include-subdomains",
|
|
584
|
+
"hsts-max-age",
|
|
585
|
+
"hsts-preload",
|
|
586
|
+
"keepalive-timeout",
|
|
587
|
+
"lingering-timeout",
|
|
588
|
+
"proxy-buffer-size",
|
|
589
|
+
"proxy-buffering",
|
|
590
|
+
"proxy-buffers",
|
|
591
|
+
"proxy-busy-buffers-size",
|
|
592
|
+
"proxy-connect-timeout",
|
|
593
|
+
"proxy-keepalive",
|
|
594
|
+
"proxy-read-timeout",
|
|
595
|
+
"proxy-send-timeout",
|
|
596
|
+
"send-timeout",
|
|
597
|
+
"underscore-in-headers",
|
|
598
|
+
"x-forwarded-for-value",
|
|
599
|
+
"x-forwarded-port-value",
|
|
600
|
+
"x-forwarded-proto-value",
|
|
601
|
+
"x-forwarded-ssl"
|
|
602
|
+
];
|
|
603
|
+
var LOGS_KEYS = ["app-label-alias", "max-size", "vector-cron-sink", "vector-sink"];
|
|
604
|
+
var REGISTRY_KEYS = [
|
|
605
|
+
"auth-servers",
|
|
606
|
+
"image-repo",
|
|
607
|
+
"image-repo-template",
|
|
608
|
+
"push-extra-tags",
|
|
609
|
+
"push-on-release",
|
|
610
|
+
"server"
|
|
611
|
+
];
|
|
612
|
+
function propertyRemovals(allowed) {
|
|
613
|
+
return (current, desired) => Object.keys(current ?? {}).filter((k) => allowed.includes(k) && !(desired && k in desired));
|
|
614
|
+
}
|
|
615
|
+
function propertyPrune(setCmd, afterPrune = []) {
|
|
616
|
+
return async (ctx, target, removed) => {
|
|
617
|
+
for (const key of removed) await ctx.run(setCmd, target, key);
|
|
618
|
+
for (const cmd of afterPrune) {
|
|
619
|
+
try {
|
|
620
|
+
await ctx.run(cmd, target);
|
|
621
|
+
} catch (err) {
|
|
622
|
+
if (await isAppDeployed(ctx, target)) throw err;
|
|
623
|
+
logWarn(target, `${cmd} failed on an undeployed app \u2014 its proxy config will be built on first deploy`);
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
};
|
|
627
|
+
}
|
|
523
628
|
function propertyResource(opts) {
|
|
524
629
|
return {
|
|
525
630
|
key: opts.key,
|
|
631
|
+
// Keys only on the server never make the block "changed"; diff shows them as removals.
|
|
632
|
+
additive: true,
|
|
633
|
+
removals: propertyRemovals(opts.settable),
|
|
634
|
+
prune: propertyPrune(opts.setCmd, opts.afterChange),
|
|
526
635
|
async read(ctx, target) {
|
|
527
636
|
const raw = await ctx.query(`${opts.namespace}:report`, target);
|
|
528
637
|
return parseReport(raw, opts.namespace);
|
|
@@ -531,11 +640,11 @@ function propertyResource(opts) {
|
|
|
531
640
|
const raw = await ctx.query(`${opts.namespace}:report`);
|
|
532
641
|
return parseBulkReport(raw, opts.namespace);
|
|
533
642
|
},
|
|
534
|
-
async onChange(ctx, target, change) {
|
|
643
|
+
async onChange(ctx, target, change, deferFollowUp = false) {
|
|
535
644
|
for (const [key, value] of Object.entries({ ...change.added, ...change.modified })) {
|
|
536
645
|
await ctx.run(opts.setCmd, target, key, String(value));
|
|
537
646
|
}
|
|
538
|
-
if (opts.afterChange) {
|
|
647
|
+
if (opts.afterChange && !deferFollowUp) {
|
|
539
648
|
for (const cmd of opts.afterChange) {
|
|
540
649
|
try {
|
|
541
650
|
await ctx.run(cmd, target);
|
|
@@ -552,20 +661,28 @@ var Nginx = propertyResource({
|
|
|
552
661
|
key: "nginx",
|
|
553
662
|
namespace: "nginx",
|
|
554
663
|
setCmd: "nginx:set",
|
|
664
|
+
settable: NGINX_KEYS,
|
|
555
665
|
afterChange: ["proxy:build-config"]
|
|
556
666
|
});
|
|
557
667
|
var Logs = propertyResource({
|
|
558
668
|
key: "logs",
|
|
559
669
|
namespace: "logs",
|
|
560
|
-
setCmd: "logs:set"
|
|
670
|
+
setCmd: "logs:set",
|
|
671
|
+
settable: LOGS_KEYS
|
|
561
672
|
});
|
|
562
673
|
var Registry = propertyResource({
|
|
563
674
|
key: "registry",
|
|
564
675
|
namespace: "registry",
|
|
565
|
-
setCmd: "registry:set"
|
|
676
|
+
setCmd: "registry:set",
|
|
677
|
+
settable: REGISTRY_KEYS
|
|
566
678
|
});
|
|
567
679
|
var Scheduler = {
|
|
568
680
|
key: "scheduler",
|
|
681
|
+
// A scheduler selected on the app but not declared in the file.
|
|
682
|
+
removals: (current, desired) => desired === void 0 && current ? ["scheduler"] : [],
|
|
683
|
+
prune: async (ctx, target) => {
|
|
684
|
+
await ctx.run("scheduler:set", target, "selected");
|
|
685
|
+
},
|
|
569
686
|
async read(ctx, target) {
|
|
570
687
|
const raw = await ctx.query("scheduler:report", target);
|
|
571
688
|
const report = parseReport(raw, "scheduler");
|
|
@@ -606,21 +723,38 @@ var Proxy = {
|
|
|
606
723
|
}
|
|
607
724
|
};
|
|
608
725
|
|
|
726
|
+
// src/core/config-json.ts
|
|
727
|
+
function parseConfigJson(raw, what) {
|
|
728
|
+
let parsed;
|
|
729
|
+
try {
|
|
730
|
+
parsed = JSON.parse(raw);
|
|
731
|
+
} catch {
|
|
732
|
+
throw new Error(`${what}: config:export did not return JSON`);
|
|
733
|
+
}
|
|
734
|
+
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
735
|
+
throw new Error(`${what}: config:export did not return a JSON object`);
|
|
736
|
+
}
|
|
737
|
+
const result = {};
|
|
738
|
+
for (const [k, v] of Object.entries(parsed)) result[k] = String(v);
|
|
739
|
+
return result;
|
|
740
|
+
}
|
|
741
|
+
|
|
609
742
|
// src/resources/config.ts
|
|
610
743
|
var MANAGED_KEYS_VAR = "DOKKU_COMPOSE_MANAGED_KEYS";
|
|
744
|
+
var MISSING = "\0missing";
|
|
611
745
|
var Config = {
|
|
612
746
|
key: "env",
|
|
747
|
+
removals: (current, desired) => Object.keys(current ?? {}).filter((k) => !(desired && typeof desired === "object" && k in desired)),
|
|
613
748
|
read: async (ctx, target) => {
|
|
614
749
|
const managedRaw = await ctx.query("config:get", target, MANAGED_KEYS_VAR);
|
|
615
750
|
const managedKeys = managedRaw.trim() ? managedRaw.trim().split(",").filter(Boolean) : [];
|
|
616
751
|
const result = {};
|
|
752
|
+
for (const k of managedKeys) result[k] = MISSING;
|
|
617
753
|
if (managedKeys.length > 0) {
|
|
618
|
-
const raw = await ctx.
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
if (
|
|
622
|
-
result[match[1]] = match[2];
|
|
623
|
-
}
|
|
754
|
+
const raw = await ctx.queryStrict("config:export", "--format", "json", target);
|
|
755
|
+
const current = parseConfigJson(raw, target);
|
|
756
|
+
for (const k of managedKeys) {
|
|
757
|
+
if (k in current) result[k] = current[k];
|
|
624
758
|
}
|
|
625
759
|
}
|
|
626
760
|
return result;
|
|
@@ -630,9 +764,12 @@ var Config = {
|
|
|
630
764
|
if (removed.length > 0) {
|
|
631
765
|
await ctx.run("config:unset", "--no-restart", target, ...removed);
|
|
632
766
|
}
|
|
633
|
-
const toSet = { ...added, ...modified };
|
|
634
767
|
const allDesiredKeys = Object.keys(change.after);
|
|
635
|
-
|
|
768
|
+
if (allDesiredKeys.length === 0) {
|
|
769
|
+
await ctx.run("config:unset", "--no-restart", target, MANAGED_KEYS_VAR);
|
|
770
|
+
return;
|
|
771
|
+
}
|
|
772
|
+
const toSet = { ...added, ...modified };
|
|
636
773
|
if (Object.keys(toSet).length > 0 || removed.length > 0) {
|
|
637
774
|
const pairs = Object.entries(change.after).map(([k, v]) => `${k}=${v}`);
|
|
638
775
|
await ctx.run(
|
|
@@ -640,7 +777,7 @@ var Config = {
|
|
|
640
777
|
"--no-restart",
|
|
641
778
|
target,
|
|
642
779
|
...pairs,
|
|
643
|
-
`${MANAGED_KEYS_VAR}=${
|
|
780
|
+
`${MANAGED_KEYS_VAR}=${allDesiredKeys.join(",")}`
|
|
644
781
|
);
|
|
645
782
|
}
|
|
646
783
|
}
|
|
@@ -675,10 +812,17 @@ var Certs = {
|
|
|
675
812
|
};
|
|
676
813
|
|
|
677
814
|
// src/resources/builder.ts
|
|
815
|
+
function parseBuildOptions(dockerOptsBuild) {
|
|
816
|
+
const out = [];
|
|
817
|
+
for (const raw of dockerOptsBuild.trim().split(/\s+(?=--\S)/)) {
|
|
818
|
+
const m = raw.match(/^--build-arg\s+([^=\s]+)=?(.*)$/s);
|
|
819
|
+
if (m) out.push({ key: m[1], value: m[2], raw });
|
|
820
|
+
}
|
|
821
|
+
return out;
|
|
822
|
+
}
|
|
678
823
|
function parseBuildArgs(dockerOptsBuild) {
|
|
679
824
|
const args = {};
|
|
680
|
-
const
|
|
681
|
-
for (const m of matches) args[m[1]] = m[2];
|
|
825
|
+
for (const o of parseBuildOptions(dockerOptsBuild)) args[o.key] = o.value;
|
|
682
826
|
return args;
|
|
683
827
|
}
|
|
684
828
|
function buildConfigFromReports(builderReport, dockerfileReport, appJsonReport, dockerOptsBuild) {
|
|
@@ -695,6 +839,19 @@ function buildConfigFromReports(builderReport, dockerfileReport, appJsonReport,
|
|
|
695
839
|
}
|
|
696
840
|
var Builder = {
|
|
697
841
|
key: "build",
|
|
842
|
+
additive: true,
|
|
843
|
+
// Build args (`--build-arg KEY=...` build docker options) whose KEY is not in `build.args`.
|
|
844
|
+
removals: (current, desired) => Object.keys(current?.args ?? {}).filter((k) => !(desired?.args && k in desired.args)).map((k) => `args.${k}`),
|
|
845
|
+
// Remove exactly the stale `--build-arg` options; other build options stay.
|
|
846
|
+
prune: async (ctx, target, removed) => {
|
|
847
|
+
const stale = new Set(removed.map((l) => l.slice("args.".length)));
|
|
848
|
+
const report = parseReport(await ctx.query("docker-options:report", target), "docker-options");
|
|
849
|
+
const options = parseBuildOptions(report["build"] ?? "").filter((o) => stale.has(o.key));
|
|
850
|
+
for (const o of options) await ctx.run("docker-options:remove", target, "build", o.raw);
|
|
851
|
+
for (const k of stale) {
|
|
852
|
+
if (!options.some((o) => o.key === k)) logWarn(target, `build arg '${k}' not found as an exact docker option; left as is`);
|
|
853
|
+
}
|
|
854
|
+
},
|
|
698
855
|
async read(ctx, target) {
|
|
699
856
|
const [builderRaw, dockerfileRaw, appJsonRaw, dockerOptsRaw] = await Promise.all([
|
|
700
857
|
ctx.query("builder:report", target),
|
|
@@ -766,6 +923,11 @@ function dockerOptsFromReport(report) {
|
|
|
766
923
|
}
|
|
767
924
|
var DockerOptions = {
|
|
768
925
|
key: "docker_options",
|
|
926
|
+
// Pruned by `up` within a declared block only, so a missing block is not shown.
|
|
927
|
+
removals: (current, desired) => !desired ? [] : PHASES.flatMap((phase) => {
|
|
928
|
+
const wanted = new Set(desired?.[phase] ?? []);
|
|
929
|
+
return (current?.[phase] ?? []).filter((o) => !wanted.has(o)).map((o) => `${phase}: ${o}`);
|
|
930
|
+
}),
|
|
769
931
|
async read(ctx, target) {
|
|
770
932
|
const raw = await ctx.query("docker-options:report", target);
|
|
771
933
|
return dockerOptsFromReport(parseReport(raw, "docker-options"));
|
|
@@ -793,6 +955,68 @@ var DockerOptions = {
|
|
|
793
955
|
}
|
|
794
956
|
};
|
|
795
957
|
|
|
958
|
+
// src/resources/ps.ts
|
|
959
|
+
var PS_KEYS = ["procfile-path", "restart-policy", "stop-timeout-seconds"];
|
|
960
|
+
function parseScale(raw) {
|
|
961
|
+
const result = {};
|
|
962
|
+
let parsed;
|
|
963
|
+
try {
|
|
964
|
+
parsed = JSON.parse(raw);
|
|
965
|
+
} catch {
|
|
966
|
+
return result;
|
|
967
|
+
}
|
|
968
|
+
if (!Array.isArray(parsed)) return result;
|
|
969
|
+
for (const e of parsed) {
|
|
970
|
+
if (e && typeof e.process_type === "string" && e.quantity !== void 0) {
|
|
971
|
+
result[e.process_type] = String(e.quantity);
|
|
972
|
+
}
|
|
973
|
+
}
|
|
974
|
+
return result;
|
|
975
|
+
}
|
|
976
|
+
function parsePsReport(raw) {
|
|
977
|
+
const result = {};
|
|
978
|
+
let parsed;
|
|
979
|
+
try {
|
|
980
|
+
parsed = JSON.parse(raw);
|
|
981
|
+
} catch {
|
|
982
|
+
return result;
|
|
983
|
+
}
|
|
984
|
+
if (!parsed || typeof parsed !== "object") return result;
|
|
985
|
+
for (const key of PS_KEYS) {
|
|
986
|
+
const v = parsed[key];
|
|
987
|
+
if (v !== void 0 && v !== null && String(v) !== "") result[key] = String(v);
|
|
988
|
+
}
|
|
989
|
+
return result;
|
|
990
|
+
}
|
|
991
|
+
var Scale = {
|
|
992
|
+
key: "scale",
|
|
993
|
+
additive: true,
|
|
994
|
+
async read(ctx, target) {
|
|
995
|
+
return parseScale(await ctx.query("ps:scale", "--format", "json", target));
|
|
996
|
+
},
|
|
997
|
+
async onChange(ctx, target, change) {
|
|
998
|
+
const pairs = Object.entries({ ...change.added, ...change.modified }).map(([proc, count]) => `${proc}=${count}`);
|
|
999
|
+
if (pairs.length === 0) return;
|
|
1000
|
+
await ctx.run("ps:scale", "--skip-deploy", target, ...pairs);
|
|
1001
|
+
}
|
|
1002
|
+
};
|
|
1003
|
+
var Ps = {
|
|
1004
|
+
key: "ps",
|
|
1005
|
+
additive: true,
|
|
1006
|
+
removals: (current, desired) => PS_KEYS.filter((k) => current?.[k] !== void 0 && !(desired && k in desired)),
|
|
1007
|
+
prune: async (ctx, target, removed) => {
|
|
1008
|
+
for (const key of removed) await ctx.run("ps:set", target, key);
|
|
1009
|
+
},
|
|
1010
|
+
async read(ctx, target) {
|
|
1011
|
+
return parsePsReport(await ctx.queryStrict("ps:report", target, "--format", "json"));
|
|
1012
|
+
},
|
|
1013
|
+
async onChange(ctx, target, change) {
|
|
1014
|
+
for (const [key, value] of Object.entries({ ...change.added, ...change.modified })) {
|
|
1015
|
+
await ctx.run("ps:set", target, key, String(value));
|
|
1016
|
+
}
|
|
1017
|
+
}
|
|
1018
|
+
};
|
|
1019
|
+
|
|
796
1020
|
// src/resources/git.ts
|
|
797
1021
|
var Git = {
|
|
798
1022
|
key: "git",
|
|
@@ -820,7 +1044,33 @@ var Git = {
|
|
|
820
1044
|
var Checks = {
|
|
821
1045
|
key: "checks",
|
|
822
1046
|
forceApply: true,
|
|
823
|
-
|
|
1047
|
+
// Applying never reads (forceApply); the read only finds settings to report as removed.
|
|
1048
|
+
// Settable state: the per-process lists and `wait-to-retire`. A list of "none" is empty.
|
|
1049
|
+
read: async (ctx, target) => {
|
|
1050
|
+
const report = parseReport(await ctx.query("checks:report", target), "checks");
|
|
1051
|
+
const current = {};
|
|
1052
|
+
for (const [reportKey, key] of [["disabled-list", "disabled"], ["skipped-list", "skipped"]]) {
|
|
1053
|
+
const v = report[reportKey];
|
|
1054
|
+
if (v && v !== "none" && v !== "_all_") current[key] = v;
|
|
1055
|
+
}
|
|
1056
|
+
if (report["wait-to-retire"]) current["wait-to-retire"] = report["wait-to-retire"];
|
|
1057
|
+
return current;
|
|
1058
|
+
},
|
|
1059
|
+
removals: (current, desired) => desired === false ? [] : Object.keys(current || {}).filter((k) => !(desired && k in desired)),
|
|
1060
|
+
prune: async (ctx, target, removed, current, desired) => {
|
|
1061
|
+
const cur = current;
|
|
1062
|
+
const split = (v) => (v ?? "").split(",").map((t) => t.trim()).filter(Boolean);
|
|
1063
|
+
const declared = new Set(desired && desired !== false ? [...desired.disabled ?? [], ...desired.skipped ?? []] : []);
|
|
1064
|
+
const stale = new Set(removed.filter((k) => k === "disabled" || k === "skipped").flatMap((k) => split(cur[k])));
|
|
1065
|
+
const enable = [...stale].filter((t) => !declared.has(t));
|
|
1066
|
+
for (const t of stale) {
|
|
1067
|
+
if (declared.has(t)) {
|
|
1068
|
+
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`);
|
|
1069
|
+
}
|
|
1070
|
+
}
|
|
1071
|
+
if (enable.length > 0) await ctx.run("checks:enable", target, ...enable);
|
|
1072
|
+
if (removed.includes("wait-to-retire")) await ctx.run("checks:set", target, "wait-to-retire");
|
|
1073
|
+
},
|
|
824
1074
|
onChange: async (ctx, target, { after }) => {
|
|
825
1075
|
if (after === false) {
|
|
826
1076
|
await ctx.run("checks:disable", target);
|
|
@@ -861,6 +1111,12 @@ var Networks = {
|
|
|
861
1111
|
};
|
|
862
1112
|
var NetworkProps = {
|
|
863
1113
|
key: "network",
|
|
1114
|
+
additive: true,
|
|
1115
|
+
removals: (current, desired) => Object.keys(current ?? {}).filter((k) => !(desired && k in desired)),
|
|
1116
|
+
// Dokku's property names are hyphenated; the schema's are underscored.
|
|
1117
|
+
prune: async (ctx, target, removed) => {
|
|
1118
|
+
for (const key of removed) await ctx.run("network:set", target, key.replaceAll("_", "-"));
|
|
1119
|
+
},
|
|
864
1120
|
read: async (ctx, target) => {
|
|
865
1121
|
const raw = await ctx.query("network:report", target);
|
|
866
1122
|
const report = parseReport(raw, "network");
|
|
@@ -907,19 +1163,90 @@ var NetworkProps = {
|
|
|
907
1163
|
};
|
|
908
1164
|
|
|
909
1165
|
// src/modules/plugins.ts
|
|
1166
|
+
function parsePluginList(raw) {
|
|
1167
|
+
const result = /* @__PURE__ */ new Map();
|
|
1168
|
+
let parsed;
|
|
1169
|
+
try {
|
|
1170
|
+
parsed = JSON.parse(raw);
|
|
1171
|
+
} catch {
|
|
1172
|
+
throw new Error(`plugin:list --format json did not return JSON (does this Dokku support --format json?): ${raw.slice(0, 200)}`);
|
|
1173
|
+
}
|
|
1174
|
+
if (!Array.isArray(parsed)) {
|
|
1175
|
+
throw new Error(`plugin:list --format json did not return a JSON array: ${raw.slice(0, 200)}`);
|
|
1176
|
+
}
|
|
1177
|
+
for (const p of parsed) {
|
|
1178
|
+
if (p && typeof p.name === "string") {
|
|
1179
|
+
result.set(p.name, {
|
|
1180
|
+
version: typeof p.version === "string" ? p.version : "",
|
|
1181
|
+
committish: typeof p.committish === "string" ? p.committish : ""
|
|
1182
|
+
});
|
|
1183
|
+
}
|
|
1184
|
+
}
|
|
1185
|
+
return result;
|
|
1186
|
+
}
|
|
1187
|
+
var SEMVER = /^(\d+)\.(\d+)\.(\d+)$/;
|
|
1188
|
+
function compareVersions(a, b) {
|
|
1189
|
+
const na = a.replace(/^v/, "");
|
|
1190
|
+
const nb = b.replace(/^v/, "");
|
|
1191
|
+
const ma = na.match(SEMVER);
|
|
1192
|
+
const mb = nb.match(SEMVER);
|
|
1193
|
+
if (!ma || !mb) return na === nb ? 0 : void 0;
|
|
1194
|
+
for (let i = 1; i <= 3; i++) {
|
|
1195
|
+
const d = Number(ma[i]) - Number(mb[i]);
|
|
1196
|
+
if (d !== 0) return d < 0 ? -1 : 1;
|
|
1197
|
+
}
|
|
1198
|
+
return 0;
|
|
1199
|
+
}
|
|
1200
|
+
var SHA = /^[0-9a-f]{7,40}$/i;
|
|
1201
|
+
function planPlugins(listOutput, plugins) {
|
|
1202
|
+
const installed = parsePluginList(listOutput);
|
|
1203
|
+
return Object.entries(plugins).map(([name, config]) => {
|
|
1204
|
+
const current = installed.get(name);
|
|
1205
|
+
const desired = config.version;
|
|
1206
|
+
if (current === void 0) return { name, action: "install", desired };
|
|
1207
|
+
const installedVersion = current.version || current.committish.slice(0, 7) || void 0;
|
|
1208
|
+
if (desired === void 0) return { name, action: "ok", installed: installedVersion };
|
|
1209
|
+
if (SHA.test(desired) && current.committish) {
|
|
1210
|
+
if (current.committish.toLowerCase().startsWith(desired.toLowerCase())) {
|
|
1211
|
+
return { name, action: "ok", installed: installedVersion, desired };
|
|
1212
|
+
}
|
|
1213
|
+
}
|
|
1214
|
+
if (!current.version) return { name, action: "unknown", installed: installedVersion, desired };
|
|
1215
|
+
const cmp = compareVersions(current.version, desired);
|
|
1216
|
+
const action = cmp === void 0 ? "unknown" : cmp < 0 ? "update" : cmp > 0 ? "newer" : "ok";
|
|
1217
|
+
return { name, action, installed: installedVersion, desired };
|
|
1218
|
+
});
|
|
1219
|
+
}
|
|
910
1220
|
async function ensurePlugins(ctx, plugins) {
|
|
911
|
-
const
|
|
912
|
-
const
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
1221
|
+
const plans = planPlugins(await ctx.queryStrict("plugin:list", "--format", "json"), plugins);
|
|
1222
|
+
for (const plan of plans) {
|
|
1223
|
+
const config = plugins[plan.name];
|
|
1224
|
+
switch (plan.action) {
|
|
1225
|
+
case "install":
|
|
1226
|
+
logAction("plugins", `Installing ${plan.name}`);
|
|
1227
|
+
await ctx.run(
|
|
1228
|
+
"plugin:install",
|
|
1229
|
+
config.url,
|
|
1230
|
+
...config.version ? ["--committish", config.version] : [],
|
|
1231
|
+
"--name",
|
|
1232
|
+
plan.name
|
|
1233
|
+
);
|
|
1234
|
+
logDone();
|
|
1235
|
+
break;
|
|
1236
|
+
case "update":
|
|
1237
|
+
logAction("plugins", `Updating ${plan.name}`);
|
|
1238
|
+
await ctx.run("plugin:update", plan.name, config.version);
|
|
1239
|
+
logDone();
|
|
1240
|
+
break;
|
|
1241
|
+
case "newer":
|
|
1242
|
+
logWarn("plugins", `${plan.name} is ${plan.installed}, newer than the declared ${plan.desired}; leaving it (never downgrades)`);
|
|
1243
|
+
break;
|
|
1244
|
+
case "unknown":
|
|
1245
|
+
logWarn("plugins", `${plan.name} is ${plan.installed} and the declared ${plan.desired} cannot be compared; leaving it`);
|
|
1246
|
+
break;
|
|
1247
|
+
default:
|
|
1248
|
+
break;
|
|
920
1249
|
}
|
|
921
|
-
await ctx.run("plugin:install", config.url, "--name", name);
|
|
922
|
-
logDone();
|
|
923
1250
|
}
|
|
924
1251
|
}
|
|
925
1252
|
|
|
@@ -1125,40 +1452,6 @@ async function exportRedis(ctx) {
|
|
|
1125
1452
|
return services;
|
|
1126
1453
|
}
|
|
1127
1454
|
|
|
1128
|
-
// src/modules/version.ts
|
|
1129
|
-
var SEMVER_RE = /^(\d+)\.(\d+)\.(\d+)/;
|
|
1130
|
-
function parseSemver(input) {
|
|
1131
|
-
const m = input.match(SEMVER_RE);
|
|
1132
|
-
if (!m) throw new Error(`Cannot parse version: ${input}`);
|
|
1133
|
-
return [Number(m[1]), Number(m[2]), Number(m[3])];
|
|
1134
|
-
}
|
|
1135
|
-
function extractServerVersion(output) {
|
|
1136
|
-
const match = output.match(/(\d+\.\d+\.\d+)/);
|
|
1137
|
-
return match ? match[1] : null;
|
|
1138
|
-
}
|
|
1139
|
-
function compareSemver(a, b) {
|
|
1140
|
-
const [aMaj, aMin, aPatch] = parseSemver(a);
|
|
1141
|
-
const [bMaj, bMin, bPatch] = parseSemver(b);
|
|
1142
|
-
if (aMaj !== bMaj) return aMaj < bMaj ? -1 : 1;
|
|
1143
|
-
if (aMin !== bMin) return aMin < bMin ? -1 : 1;
|
|
1144
|
-
if (aPatch !== bPatch) return aPatch < bPatch ? -1 : 1;
|
|
1145
|
-
return 0;
|
|
1146
|
-
}
|
|
1147
|
-
async function ensureDokkuVersion(ctx, pinned) {
|
|
1148
|
-
if (!pinned) return;
|
|
1149
|
-
const output = await ctx.query("version");
|
|
1150
|
-
const server = extractServerVersion(output);
|
|
1151
|
-
if (!server) {
|
|
1152
|
-
throw new Error(`Cannot parse Dokku server version from output: ${output}`);
|
|
1153
|
-
}
|
|
1154
|
-
if (compareSemver(server, pinned) === -1) {
|
|
1155
|
-
logWarn(
|
|
1156
|
-
"dokku",
|
|
1157
|
-
`server is v${server} but dokku-compose.yml pins >= v${pinned}. Some features may be unavailable.`
|
|
1158
|
-
);
|
|
1159
|
-
}
|
|
1160
|
-
}
|
|
1161
|
-
|
|
1162
1455
|
// src/modules/links.ts
|
|
1163
1456
|
function resolveServicePlugin(name, config) {
|
|
1164
1457
|
if (config.postgres?.[name]) return { plugin: "postgres", config: config.postgres[name] };
|
|
@@ -1248,31 +1541,57 @@ var UpFailedError = class extends Error {
|
|
|
1248
1541
|
this.name = "UpFailedError";
|
|
1249
1542
|
}
|
|
1250
1543
|
};
|
|
1251
|
-
|
|
1544
|
+
var AppStepsError = class extends AggregateError {
|
|
1545
|
+
constructor(errors) {
|
|
1546
|
+
super(errors, errors.map((e) => e instanceof Error ? e.message : String(e)).join("; "));
|
|
1547
|
+
this.name = "AppStepsError";
|
|
1548
|
+
}
|
|
1549
|
+
};
|
|
1550
|
+
async function processApp(ctx, config, app, appConfig, failFast = false, prune = false) {
|
|
1551
|
+
const errors = [];
|
|
1552
|
+
let leftOnServer = 0;
|
|
1553
|
+
const count = prune ? void 0 : (removed) => {
|
|
1554
|
+
leftOnServer += removed.length;
|
|
1555
|
+
};
|
|
1556
|
+
const step = async (fn) => {
|
|
1557
|
+
try {
|
|
1558
|
+
await fn();
|
|
1559
|
+
} catch (error) {
|
|
1560
|
+
if (failFast) throw error;
|
|
1561
|
+
logError(app, error instanceof Error ? error.message : String(error));
|
|
1562
|
+
errors.push(error);
|
|
1563
|
+
}
|
|
1564
|
+
};
|
|
1252
1565
|
await reconcile(Apps, ctx, app, true);
|
|
1253
|
-
await reconcile(Domains, ctx, app, appConfig.domains);
|
|
1254
|
-
await reconcile(Networks, ctx, app, appConfig.networks);
|
|
1255
|
-
await reconcile(NetworkProps, ctx, app, appConfig.network);
|
|
1256
|
-
await reconcile(Proxy, ctx, app, appConfig.proxy?.enabled);
|
|
1257
|
-
await reconcile(Ports, ctx, app, appConfig.ports);
|
|
1566
|
+
await step(() => reconcile(Domains, ctx, app, appConfig.domains));
|
|
1567
|
+
await step(() => reconcile(Networks, ctx, app, appConfig.networks));
|
|
1568
|
+
await step(() => reconcile(NetworkProps, ctx, app, appConfig.network, count, prune));
|
|
1569
|
+
await step(() => reconcile(Proxy, ctx, app, appConfig.proxy?.enabled));
|
|
1570
|
+
await step(() => reconcile(Ports, ctx, app, appConfig.ports));
|
|
1258
1571
|
if (config.postgres || config.redis) {
|
|
1259
|
-
await ensureAppLinks(ctx, app, appConfig.links ?? [], config);
|
|
1260
|
-
}
|
|
1261
|
-
await reconcile(Certs, ctx, app, appConfig.ssl);
|
|
1262
|
-
await reconcile(Storage, ctx, app, appConfig.storage);
|
|
1263
|
-
await reconcile(Nginx, ctx, app, appConfig.nginx);
|
|
1264
|
-
await reconcile(Checks, ctx, app, appConfig.checks);
|
|
1265
|
-
await reconcile(Logs, ctx, app, appConfig.logs);
|
|
1266
|
-
await reconcile(Registry, ctx, app, appConfig.registry);
|
|
1267
|
-
await reconcile(Scheduler, ctx, app, appConfig.scheduler);
|
|
1268
|
-
await reconcile(
|
|
1269
|
-
await reconcile(
|
|
1270
|
-
await reconcile(
|
|
1271
|
-
await reconcile(
|
|
1572
|
+
await step(() => ensureAppLinks(ctx, app, appConfig.links ?? [], config));
|
|
1573
|
+
}
|
|
1574
|
+
await step(() => reconcile(Certs, ctx, app, appConfig.ssl));
|
|
1575
|
+
await step(() => reconcile(Storage, ctx, app, appConfig.storage));
|
|
1576
|
+
await step(() => reconcile(Nginx, ctx, app, appConfig.nginx, count, prune));
|
|
1577
|
+
await step(() => reconcile(Checks, ctx, app, appConfig.checks, void 0, prune));
|
|
1578
|
+
await step(() => reconcile(Logs, ctx, app, appConfig.logs, count, prune));
|
|
1579
|
+
await step(() => reconcile(Registry, ctx, app, appConfig.registry, count, prune));
|
|
1580
|
+
await step(() => reconcile(Scheduler, ctx, app, appConfig.scheduler, count, prune));
|
|
1581
|
+
await step(() => reconcile(Ps, ctx, app, appConfig.ps, count, prune));
|
|
1582
|
+
await step(() => reconcile(Scale, ctx, app, appConfig.scale));
|
|
1583
|
+
await step(() => reconcile(Config, ctx, app, appConfig.env || {}));
|
|
1584
|
+
await step(() => reconcile(Builder, ctx, app, appConfig.build, count, prune));
|
|
1585
|
+
await step(() => reconcile(Git, ctx, app, appConfig.git ?? config.git));
|
|
1586
|
+
await step(() => reconcile(DockerOptions, ctx, app, appConfig.docker_options));
|
|
1587
|
+
if (leftOnServer > 0) {
|
|
1588
|
+
logWarn(app, `${leftOnServer} setting(s) no longer in the compose file were left on the server; run 'dokku-compose diff' to see them`);
|
|
1589
|
+
}
|
|
1590
|
+
if (errors.length > 0) throw new AppStepsError(errors);
|
|
1272
1591
|
}
|
|
1273
1592
|
async function runUp(ctx, config, appFilter, opts = {}) {
|
|
1274
1593
|
const apps = appFilter.length > 0 ? appFilter : Object.keys(config.apps);
|
|
1275
|
-
await
|
|
1594
|
+
enforceFileVersion(await assertDokkuSupported(ctx), config.dokku?.version);
|
|
1276
1595
|
if (config.plugins) await ensurePlugins(ctx, config.plugins);
|
|
1277
1596
|
if (config.docker_auth) await ensureDockerAuth(ctx, config.docker_auth);
|
|
1278
1597
|
if (config.domains !== void 0) await ensureGlobalDomains(ctx, config.domains);
|
|
@@ -1288,11 +1607,12 @@ async function runUp(ctx, config, appFilter, opts = {}) {
|
|
|
1288
1607
|
const appConfig = config.apps[app];
|
|
1289
1608
|
if (!appConfig) continue;
|
|
1290
1609
|
try {
|
|
1291
|
-
await processApp(ctx, config, app, appConfig);
|
|
1610
|
+
await processApp(ctx, config, app, appConfig, opts.failFast, opts.prune);
|
|
1292
1611
|
} catch (error) {
|
|
1293
1612
|
if (opts.failFast) throw error;
|
|
1294
|
-
|
|
1295
|
-
|
|
1613
|
+
if (!(error instanceof AppStepsError)) {
|
|
1614
|
+
logError(app, error instanceof Error ? error.message : String(error));
|
|
1615
|
+
}
|
|
1296
1616
|
failures.push({ app, error });
|
|
1297
1617
|
}
|
|
1298
1618
|
}
|
|
@@ -1331,6 +1651,7 @@ var DownFailedError = class extends Error {
|
|
|
1331
1651
|
};
|
|
1332
1652
|
async function runDown(ctx, config, appFilter, opts) {
|
|
1333
1653
|
const apps = appFilter.length > 0 ? appFilter : Object.keys(config.apps);
|
|
1654
|
+
await assertDokkuSupported(ctx);
|
|
1334
1655
|
const failures = [];
|
|
1335
1656
|
for (const app of apps) {
|
|
1336
1657
|
const appConfig = config.apps[app];
|
|
@@ -1382,6 +1703,8 @@ var CONFIG_RESOURCES = [
|
|
|
1382
1703
|
Logs,
|
|
1383
1704
|
Registry,
|
|
1384
1705
|
Scheduler,
|
|
1706
|
+
Ps,
|
|
1707
|
+
Scale,
|
|
1385
1708
|
Config
|
|
1386
1709
|
];
|
|
1387
1710
|
var BUILD_RESOURCES = [
|
|
@@ -1398,8 +1721,7 @@ var ALL_APP_RESOURCES = [
|
|
|
1398
1721
|
// src/commands/export.ts
|
|
1399
1722
|
async function runExport(ctx, opts) {
|
|
1400
1723
|
const config = { apps: {} };
|
|
1401
|
-
|
|
1402
|
-
if (version2) config.dokku = { version: version2 };
|
|
1724
|
+
config.dokku = { version: await assertDokkuSupported(ctx) };
|
|
1403
1725
|
const apps = opts.appFilter?.length ? opts.appFilter : await exportApps(ctx);
|
|
1404
1726
|
const networks = await exportNetworks(ctx);
|
|
1405
1727
|
if (networks.length > 0) config.networks = networks;
|
|
@@ -1439,7 +1761,7 @@ async function runExport(ctx, opts) {
|
|
|
1439
1761
|
}
|
|
1440
1762
|
|
|
1441
1763
|
// src/commands/diff.ts
|
|
1442
|
-
import
|
|
1764
|
+
import chalk from "chalk";
|
|
1443
1765
|
function diffSslStatus(desired, current) {
|
|
1444
1766
|
if (typeof desired !== "object" || desired === null) return void 0;
|
|
1445
1767
|
if (current === void 0) return void 0;
|
|
@@ -1447,13 +1769,24 @@ function diffSslStatus(desired, current) {
|
|
|
1447
1769
|
if (current === false || current === null) return "missing";
|
|
1448
1770
|
return void 0;
|
|
1449
1771
|
}
|
|
1772
|
+
function hasAdditionsOrChanges(change) {
|
|
1773
|
+
const c = change;
|
|
1774
|
+
if (c.added === void 0) return c.changed;
|
|
1775
|
+
const added = Array.isArray(c.added) ? c.added.length : Object.keys(c.added).length;
|
|
1776
|
+
return added > 0 || Object.keys(c.modified ?? {}).length > 0;
|
|
1777
|
+
}
|
|
1450
1778
|
function isEmptyValue(value) {
|
|
1451
1779
|
return value === null || value === void 0 || Array.isArray(value) && value.length === 0 || typeof value === "object" && Object.keys(value).length === 0;
|
|
1452
1780
|
}
|
|
1453
1781
|
async function computeDiff(ctx, config) {
|
|
1454
|
-
const result = { apps: {}, services: {}, inSync: true, readWarnings: [] };
|
|
1782
|
+
const result = { apps: {}, services: {}, plugins: {}, inSync: true, readWarnings: [] };
|
|
1455
1783
|
const appNames = Object.keys(config.apps);
|
|
1456
|
-
await
|
|
1784
|
+
const serverVersion = await assertDokkuSupported(ctx);
|
|
1785
|
+
const fileVersion = config.dokku?.version;
|
|
1786
|
+
if (fileVersion && compareToFile(serverVersion, fileVersion) !== "equal") {
|
|
1787
|
+
result.dokku = { file: fileVersion, server: serverVersion };
|
|
1788
|
+
result.inSync = false;
|
|
1789
|
+
}
|
|
1457
1790
|
const appExists = /* @__PURE__ */ new Map();
|
|
1458
1791
|
await Promise.all(
|
|
1459
1792
|
appNames.map(async (app) => {
|
|
@@ -1477,14 +1810,15 @@ async function computeDiff(ctx, config) {
|
|
|
1477
1810
|
const appDiff = {};
|
|
1478
1811
|
for (const resource of ALL_APP_RESOURCES) {
|
|
1479
1812
|
if (resource.key.startsWith("_")) continue;
|
|
1480
|
-
if (resource.forceApply) continue;
|
|
1481
1813
|
let desired;
|
|
1482
1814
|
if (resource.key === "proxy") {
|
|
1483
1815
|
desired = appConfig.proxy?.enabled;
|
|
1484
1816
|
} else {
|
|
1485
1817
|
desired = appConfig[resource.key];
|
|
1486
1818
|
}
|
|
1487
|
-
|
|
1819
|
+
const checkRemovalsOnly = desired === void 0;
|
|
1820
|
+
if (checkRemovalsOnly && (!resource.removals || !appExists.get(app))) continue;
|
|
1821
|
+
if (resource.forceApply && !resource.removals) continue;
|
|
1488
1822
|
if (!appExists.get(app)) {
|
|
1489
1823
|
appDiff[resource.key] = { status: "missing", desired, current: void 0 };
|
|
1490
1824
|
result.inSync = false;
|
|
@@ -1493,6 +1827,21 @@ async function computeDiff(ctx, config) {
|
|
|
1493
1827
|
const bulk = prefetched.get(resource.key);
|
|
1494
1828
|
let current;
|
|
1495
1829
|
let bulkUnreadable = false;
|
|
1830
|
+
if (checkRemovalsOnly) {
|
|
1831
|
+
try {
|
|
1832
|
+
current = bulk ? bulk.get(app) : await resource.read(ctx, app);
|
|
1833
|
+
if (bulk === null || current === void 0) current = await resource.read(ctx, app);
|
|
1834
|
+
} catch (err) {
|
|
1835
|
+
result.readWarnings.push(`could not read '${resource.key}' for app '${app}' to look for removed settings: ${err instanceof Error ? err.message : String(err)}`);
|
|
1836
|
+
continue;
|
|
1837
|
+
}
|
|
1838
|
+
const removed2 = resource.removals(current, void 0);
|
|
1839
|
+
if (removed2.length > 0) {
|
|
1840
|
+
appDiff[resource.key] = { status: "extra", current, removed: removed2 };
|
|
1841
|
+
result.inSync = false;
|
|
1842
|
+
}
|
|
1843
|
+
continue;
|
|
1844
|
+
}
|
|
1496
1845
|
if (bulk === null) {
|
|
1497
1846
|
bulkUnreadable = true;
|
|
1498
1847
|
current = await resource.read(ctx, app);
|
|
@@ -1511,9 +1860,23 @@ async function computeDiff(ctx, config) {
|
|
|
1511
1860
|
if (sslStatus !== "in-sync") result.inSync = false;
|
|
1512
1861
|
continue;
|
|
1513
1862
|
}
|
|
1514
|
-
|
|
1515
|
-
if (
|
|
1863
|
+
let removed = resource.removals ? resource.removals(current, desired) : [];
|
|
1864
|
+
if (resource.forceApply) {
|
|
1865
|
+
if (removed.length > 0) {
|
|
1866
|
+
appDiff[resource.key] = { status: "extra", desired, current, removed };
|
|
1867
|
+
result.inSync = false;
|
|
1868
|
+
}
|
|
1869
|
+
continue;
|
|
1870
|
+
}
|
|
1871
|
+
const change = resource.additive ? computeAdditiveChange(current, desired) : computeChange(current, desired);
|
|
1872
|
+
if (!resource.removals && Array.isArray(current) && "removed" in change) {
|
|
1873
|
+
removed = change.removed;
|
|
1874
|
+
}
|
|
1875
|
+
if (!change.changed && removed.length === 0) {
|
|
1516
1876
|
appDiff[resource.key] = { status: "in-sync", desired, current };
|
|
1877
|
+
} else if (removed.length > 0 && !hasAdditionsOrChanges(change)) {
|
|
1878
|
+
appDiff[resource.key] = { status: "extra", desired, current, removed };
|
|
1879
|
+
result.inSync = false;
|
|
1517
1880
|
} else if (bulkUnreadable && isEmptyValue(current)) {
|
|
1518
1881
|
appDiff[resource.key] = { status: "unknown", desired, current };
|
|
1519
1882
|
result.inSync = false;
|
|
@@ -1526,9 +1889,34 @@ async function computeDiff(ctx, config) {
|
|
|
1526
1889
|
appDiff[resource.key] = { status: "changed", desired, current };
|
|
1527
1890
|
result.inSync = false;
|
|
1528
1891
|
}
|
|
1892
|
+
if (removed.length > 0 && appDiff[resource.key].status !== "extra") {
|
|
1893
|
+
appDiff[resource.key].removed = removed;
|
|
1894
|
+
}
|
|
1895
|
+
}
|
|
1896
|
+
if (appConfig.links === void 0 && appExists.get(app)) {
|
|
1897
|
+
const linked = [];
|
|
1898
|
+
for (const [plugin, services] of [["postgres", config.postgres], ["redis", config.redis]]) {
|
|
1899
|
+
for (const svc of Object.keys(services ?? {})) {
|
|
1900
|
+
if (await ctx.check(`${plugin}:linked`, svc, app)) linked.push(svc);
|
|
1901
|
+
}
|
|
1902
|
+
}
|
|
1903
|
+
if (linked.length > 0) {
|
|
1904
|
+
appDiff["links"] = { status: "extra", current: linked, removed: linked, note: "no links: in the file; up will unlink these" };
|
|
1905
|
+
result.inSync = false;
|
|
1906
|
+
}
|
|
1529
1907
|
}
|
|
1530
1908
|
result.apps[app] = appDiff;
|
|
1531
1909
|
}
|
|
1910
|
+
if (config.plugins) {
|
|
1911
|
+
const plans = planPlugins(await ctx.queryStrict("plugin:list", "--format", "json"), config.plugins);
|
|
1912
|
+
for (const plan of plans) {
|
|
1913
|
+
if (plan.action === "ok") continue;
|
|
1914
|
+
const status = plan.action === "install" ? "missing" : plan.action === "unknown" ? "unknown" : "changed";
|
|
1915
|
+
const note = plan.action === "newer" ? "installed is newer; up warns and leaves it" : plan.action === "unknown" ? "versions cannot be compared; up leaves it" : void 0;
|
|
1916
|
+
result.plugins[plan.name] = { status, desired: plan.desired, current: plan.installed, note };
|
|
1917
|
+
if (plan.action !== "unknown") result.inSync = false;
|
|
1918
|
+
}
|
|
1919
|
+
}
|
|
1532
1920
|
for (const svc of Object.keys(config.postgres ?? {})) {
|
|
1533
1921
|
const exists = await ctx.check("postgres:exists", svc);
|
|
1534
1922
|
result.services[svc] = { status: exists ? "in-sync" : "missing" };
|
|
@@ -1542,14 +1930,16 @@ async function computeDiff(ctx, config) {
|
|
|
1542
1930
|
return result;
|
|
1543
1931
|
}
|
|
1544
1932
|
function maskDiffResult(diff) {
|
|
1545
|
-
const masked = { apps: {}, services: diff.services, inSync: diff.inSync, readWarnings: diff.readWarnings };
|
|
1933
|
+
const masked = { apps: {}, services: diff.services, plugins: diff.plugins, dokku: diff.dokku, inSync: diff.inSync, readWarnings: diff.readWarnings };
|
|
1546
1934
|
for (const [app, appDiff] of Object.entries(diff.apps)) {
|
|
1547
1935
|
const maskedAppDiff = {};
|
|
1548
1936
|
for (const [feature, d] of Object.entries(appDiff)) {
|
|
1549
1937
|
maskedAppDiff[feature] = {
|
|
1550
1938
|
status: d.status,
|
|
1551
1939
|
desired: maskSensitiveData(d.desired),
|
|
1552
|
-
current: maskSensitiveData(d.current)
|
|
1940
|
+
current: maskSensitiveData(d.current),
|
|
1941
|
+
...d.removed ? { removed: d.removed } : {},
|
|
1942
|
+
...d.note ? { note: d.note } : {}
|
|
1553
1943
|
};
|
|
1554
1944
|
}
|
|
1555
1945
|
masked.apps[app] = maskedAppDiff;
|
|
@@ -1564,41 +1954,60 @@ function formatSummary(diff) {
|
|
|
1564
1954
|
lines.push(` app: ${app}`);
|
|
1565
1955
|
lines.push(` (in sync)`);
|
|
1566
1956
|
} else {
|
|
1567
|
-
lines.push(` app: ${
|
|
1957
|
+
lines.push(` app: ${chalk.bold(app)}`);
|
|
1568
1958
|
for (const [feature, d] of changes) {
|
|
1569
|
-
const sym = d.status === "missing" ?
|
|
1959
|
+
const sym = d.status === "missing" ? chalk.green("+") : d.status === "unknown" ? chalk.red("?") : d.status === "extra" ? chalk.red("-") : chalk.yellow("~");
|
|
1570
1960
|
lines.push(` ${sym} ${feature}: ${formatFeatureSummary(d)}`);
|
|
1571
1961
|
}
|
|
1572
1962
|
}
|
|
1573
1963
|
}
|
|
1964
|
+
if (diff.dokku) {
|
|
1965
|
+
lines.push(` dokku`);
|
|
1966
|
+
lines.push(` ${chalk.yellow("~")} version: ${diff.dokku.file} (file) \u2192 ${diff.dokku.server} (server)`);
|
|
1967
|
+
}
|
|
1968
|
+
for (const [name, d] of Object.entries(diff.plugins)) {
|
|
1969
|
+
const sym = d.status === "missing" ? chalk.green("+") : d.status === "unknown" ? chalk.red("?") : chalk.yellow("~");
|
|
1970
|
+
const detail = d.status === "missing" ? `not installed (want ${d.desired ?? "any"})` : `${d.current} \u2192 ${d.desired}`;
|
|
1971
|
+
lines.push(` plugin: ${name}`);
|
|
1972
|
+
lines.push(` ${sym} version: ${detail}${d.note ? ` (${d.note})` : ""}`);
|
|
1973
|
+
}
|
|
1574
1974
|
for (const [svc, d] of Object.entries(diff.services)) {
|
|
1575
1975
|
if (d.status === "missing") {
|
|
1576
1976
|
lines.push(` services:`);
|
|
1577
|
-
lines.push(` ${
|
|
1977
|
+
lines.push(` ${chalk.green("+")} ${svc}: not provisioned`);
|
|
1578
1978
|
}
|
|
1579
1979
|
}
|
|
1580
|
-
const total = Object.values(diff.apps).flatMap((a) => Object.values(a)).filter((d) => d.status !== "in-sync").length + Object.values(diff.services).filter((d) => d.status !== "in-sync").length;
|
|
1980
|
+
const total = Object.values(diff.apps).flatMap((a) => Object.values(a)).filter((d) => d.status !== "in-sync").length + Object.values(diff.services).filter((d) => d.status !== "in-sync").length + Object.values(diff.plugins).filter((d) => d.status !== "unknown").length + (diff.dokku ? 1 : 0);
|
|
1581
1981
|
if (diff.readWarnings.length > 0) {
|
|
1582
1982
|
lines.push("");
|
|
1583
|
-
lines.push(
|
|
1584
|
-
for (const w of diff.readWarnings) lines.push(
|
|
1983
|
+
lines.push(chalk.red(" Could not read some server state \u2014 treated as unknown, not missing:"));
|
|
1984
|
+
for (const w of diff.readWarnings) lines.push(chalk.red(` - ${w}`));
|
|
1585
1985
|
}
|
|
1586
1986
|
lines.push("");
|
|
1587
1987
|
if (total === 0) {
|
|
1588
|
-
lines.push(
|
|
1988
|
+
lines.push(chalk.green(" Everything in sync."));
|
|
1589
1989
|
} else {
|
|
1590
|
-
lines.push(
|
|
1990
|
+
lines.push(chalk.yellow(` ${total} resource(s) out of sync.`));
|
|
1591
1991
|
}
|
|
1592
1992
|
lines.push("");
|
|
1593
1993
|
return lines.join("\n");
|
|
1594
1994
|
}
|
|
1595
1995
|
function formatFeatureSummary(d) {
|
|
1996
|
+
const gone = d.removed?.length ? `not in the file: ${d.removed.join(", ")}` : "";
|
|
1997
|
+
if (d.status === "extra") return `${gone}${d.note ? ` (${d.note})` : ""}`;
|
|
1998
|
+
const base = formatChange(d);
|
|
1999
|
+
return gone ? `${base}; ${gone}` : base;
|
|
2000
|
+
}
|
|
2001
|
+
function formatChange(d) {
|
|
1596
2002
|
if (d.status === "unknown") return "(could not read server state \u2014 not reported as missing)";
|
|
1597
2003
|
if (d.status === "missing") return "(not set on server)";
|
|
1598
2004
|
if (Array.isArray(d.desired) && Array.isArray(d.current)) {
|
|
1599
2005
|
return `${d.current.length} \u2192 ${d.desired.length} items`;
|
|
1600
2006
|
}
|
|
1601
|
-
return `${JSON.stringify(d.current)} \u2192 ${JSON.stringify(d.desired)}`;
|
|
2007
|
+
return `${JSON.stringify(d.current, showUnset)} \u2192 ${JSON.stringify(d.desired, showUnset)}`;
|
|
2008
|
+
}
|
|
2009
|
+
function showUnset(_key, value) {
|
|
2010
|
+
return value === "\0missing" ? "(unset)" : value;
|
|
1602
2011
|
}
|
|
1603
2012
|
function formatVerbose(diff) {
|
|
1604
2013
|
const lines = [""];
|
|
@@ -1608,25 +2017,41 @@ function formatVerbose(diff) {
|
|
|
1608
2017
|
for (const [feature, d] of changes) {
|
|
1609
2018
|
lines.push(`@@ app: ${app} / ${feature} @@`);
|
|
1610
2019
|
if (d.status === "unknown") {
|
|
1611
|
-
lines.push(
|
|
2020
|
+
lines.push(chalk.red(` ? could not read server state \u2014 not reported as missing`));
|
|
2021
|
+
continue;
|
|
2022
|
+
}
|
|
2023
|
+
if (d.status === "extra") {
|
|
2024
|
+
for (const r of d.removed ?? []) lines.push(chalk.red(`- ${r}`));
|
|
2025
|
+
if (d.note) lines.push(` (${d.note})`);
|
|
1612
2026
|
continue;
|
|
1613
2027
|
}
|
|
1614
|
-
const currentLines = d.current !== void 0 ? JSON.stringify(d.current,
|
|
1615
|
-
const desiredLines = JSON.stringify(d.desired,
|
|
1616
|
-
for (const line of currentLines) lines.push(
|
|
1617
|
-
for (const line of desiredLines) lines.push(
|
|
2028
|
+
const currentLines = d.current !== void 0 ? JSON.stringify(d.current, showUnset, 2).split("\n") : [];
|
|
2029
|
+
const desiredLines = JSON.stringify(d.desired, showUnset, 2).split("\n");
|
|
2030
|
+
for (const line of currentLines) lines.push(chalk.red(`- ${line}`));
|
|
2031
|
+
for (const line of desiredLines) lines.push(chalk.green(`+ ${line}`));
|
|
2032
|
+
for (const r of d.removed ?? []) lines.push(chalk.red(`- ${r} (not in the file)`));
|
|
1618
2033
|
}
|
|
1619
2034
|
}
|
|
2035
|
+
if (diff.dokku) {
|
|
2036
|
+
lines.push(`@@ dokku version @@`);
|
|
2037
|
+
lines.push(chalk.red(`- ${diff.dokku.server} (server)`));
|
|
2038
|
+
lines.push(chalk.green(`+ ${diff.dokku.file} (file)`));
|
|
2039
|
+
}
|
|
2040
|
+
for (const [name, d] of Object.entries(diff.plugins)) {
|
|
2041
|
+
lines.push(`@@ plugin: ${name} @@`);
|
|
2042
|
+
if (d.current !== void 0) lines.push(chalk.red(`- ${d.current}`));
|
|
2043
|
+
lines.push(chalk.green(`+ ${d.desired ?? "(any version)"}`) + (d.note ? ` (${d.note})` : ""));
|
|
2044
|
+
}
|
|
1620
2045
|
for (const [svc, d] of Object.entries(diff.services)) {
|
|
1621
2046
|
if (d.status === "missing") {
|
|
1622
2047
|
lines.push(`@@ services @@`);
|
|
1623
|
-
lines.push(
|
|
2048
|
+
lines.push(chalk.green(`+ ${svc}`));
|
|
1624
2049
|
}
|
|
1625
2050
|
}
|
|
1626
2051
|
if (diff.readWarnings.length > 0) {
|
|
1627
2052
|
lines.push("");
|
|
1628
|
-
lines.push(
|
|
1629
|
-
for (const w of diff.readWarnings) lines.push(
|
|
2053
|
+
lines.push(chalk.red("Could not read some server state \u2014 treated as unknown, not missing:"));
|
|
2054
|
+
for (const w of diff.readWarnings) lines.push(chalk.red(` - ${w}`));
|
|
1630
2055
|
}
|
|
1631
2056
|
lines.push("");
|
|
1632
2057
|
return lines.join("\n");
|
|
@@ -1683,7 +2108,7 @@ function validate(filePath) {
|
|
|
1683
2108
|
}
|
|
1684
2109
|
|
|
1685
2110
|
// src/index.ts
|
|
1686
|
-
import
|
|
2111
|
+
import chalk2 from "chalk";
|
|
1687
2112
|
var require2 = createRequire(import.meta.url);
|
|
1688
2113
|
var { version } = require2("../package.json");
|
|
1689
2114
|
var program = new Command().name("dokku-compose").version(version);
|
|
@@ -1693,12 +2118,12 @@ function makeRunner(opts) {
|
|
|
1693
2118
|
dryRun: opts.dryRun ?? false
|
|
1694
2119
|
});
|
|
1695
2120
|
}
|
|
1696
|
-
program.command("up [apps...]").description("Create/update apps and services to match config").option("-f, --file <path>", "Config file", "dokku-compose.yml").option("--dry-run", "Print commands without executing").option("--sensitive", "Show sensitive values in dry-run output (masked by default)").option("--fail-fast", "Stop on first error").action(async (apps, opts) => {
|
|
2121
|
+
program.command("up [apps...]").description("Create/update apps and services to match config").option("-f, --file <path>", "Config file", "dokku-compose.yml").option("--dry-run", "Print commands without executing").option("--sensitive", "Show sensitive values in dry-run output (masked by default)").option("--fail-fast", "Stop on first error").option("--prune", "Also reset settings the file no longer declares (read `diff` first)").action(async (apps, opts) => {
|
|
1697
2122
|
const config = loadConfig(opts.file);
|
|
1698
2123
|
const runner = makeRunner(opts);
|
|
1699
2124
|
const ctx = createContext(runner);
|
|
1700
2125
|
try {
|
|
1701
|
-
await runUp(ctx, config, apps, { failFast: opts.failFast ?? false });
|
|
2126
|
+
await runUp(ctx, config, apps, { failFast: opts.failFast ?? false, prune: opts.prune ?? false });
|
|
1702
2127
|
if (opts.dryRun) {
|
|
1703
2128
|
console.log("\n# Commands that would run:");
|
|
1704
2129
|
for (const cmd of runner.dryRunLog) {
|
|
@@ -1707,7 +2132,7 @@ program.command("up [apps...]").description("Create/update apps and services to
|
|
|
1707
2132
|
}
|
|
1708
2133
|
}
|
|
1709
2134
|
} catch (error) {
|
|
1710
|
-
console.error(
|
|
2135
|
+
console.error(chalk2.red(`
|
|
1711
2136
|
${error.message}`));
|
|
1712
2137
|
process.exitCode = 1;
|
|
1713
2138
|
} finally {
|
|
@@ -1725,7 +2150,7 @@ program.command("down [apps...]").description("Destroy apps and services (requir
|
|
|
1725
2150
|
try {
|
|
1726
2151
|
await runDown(ctx, config, apps, { force: true, failFast: opts.failFast ?? false });
|
|
1727
2152
|
} catch (error) {
|
|
1728
|
-
console.error(
|
|
2153
|
+
console.error(chalk2.red(`
|
|
1729
2154
|
${error.message}`));
|
|
1730
2155
|
process.exitCode = 1;
|
|
1731
2156
|
} finally {
|
|
@@ -1763,6 +2188,10 @@ program.command("export").description("Export server state to dokku-compose.yml
|
|
|
1763
2188
|
} else {
|
|
1764
2189
|
process.stdout.write(out);
|
|
1765
2190
|
}
|
|
2191
|
+
} catch (error) {
|
|
2192
|
+
console.error(chalk2.red(`
|
|
2193
|
+
${error.message}`));
|
|
2194
|
+
process.exitCode = 1;
|
|
1766
2195
|
} finally {
|
|
1767
2196
|
await ctx.close();
|
|
1768
2197
|
}
|
|
@@ -1777,6 +2206,10 @@ program.command("diff").description("Show what is out of sync between config and
|
|
|
1777
2206
|
const output = opts.verbose ? formatVerbose(diff) : formatSummary(diff);
|
|
1778
2207
|
process.stdout.write(output);
|
|
1779
2208
|
process.exitCode = rawDiff.inSync ? 0 : 1;
|
|
2209
|
+
} catch (error) {
|
|
2210
|
+
console.error(chalk2.red(`
|
|
2211
|
+
${error.message}`));
|
|
2212
|
+
process.exitCode = 1;
|
|
1780
2213
|
} finally {
|
|
1781
2214
|
await ctx.close();
|
|
1782
2215
|
}
|
|
@@ -1785,8 +2218,12 @@ program.command("ps [apps...]").description("Show status of configured apps").op
|
|
|
1785
2218
|
const config = loadConfig(opts.file);
|
|
1786
2219
|
const runner = makeRunner({});
|
|
1787
2220
|
try {
|
|
1788
|
-
const { runPs } = await import("./ps-
|
|
2221
|
+
const { runPs } = await import("./ps-IWBYZRDO.js");
|
|
1789
2222
|
await runPs(runner, config, apps);
|
|
2223
|
+
} catch (error) {
|
|
2224
|
+
console.error(chalk2.red(`
|
|
2225
|
+
${error.message}`));
|
|
2226
|
+
process.exitCode = 1;
|
|
1790
2227
|
} finally {
|
|
1791
2228
|
await runner.close();
|
|
1792
2229
|
}
|
|
@@ -1,6 +1,11 @@
|
|
|
1
|
+
import {
|
|
2
|
+
assertDokkuSupported
|
|
3
|
+
} from "./chunk-3W53KPE4.js";
|
|
4
|
+
|
|
1
5
|
// src/commands/ps.ts
|
|
2
6
|
async function runPs(runner, config, appFilter) {
|
|
3
7
|
const apps = appFilter.length > 0 ? appFilter : Object.keys(config.apps);
|
|
8
|
+
await assertDokkuSupported(runner);
|
|
4
9
|
for (const app of apps) {
|
|
5
10
|
const exists = await runner.check("apps:exists", app);
|
|
6
11
|
if (!exists) {
|