@yannelli/zoomies 0.0.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 +97 -0
- package/dist/cli/client.d.ts +77 -0
- package/dist/cli/client.d.ts.map +1 -0
- package/dist/cli/client.js +300 -0
- package/dist/cli/client.js.map +1 -0
- package/dist/cli/commands/certs.d.ts +11 -0
- package/dist/cli/commands/certs.d.ts.map +1 -0
- package/dist/cli/commands/certs.js +110 -0
- package/dist/cli/commands/certs.js.map +1 -0
- package/dist/cli/commands/flags.d.ts +29 -0
- package/dist/cli/commands/flags.d.ts.map +1 -0
- package/dist/cli/commands/flags.js +104 -0
- package/dist/cli/commands/flags.js.map +1 -0
- package/dist/cli/commands/reload.d.ts +11 -0
- package/dist/cli/commands/reload.d.ts.map +1 -0
- package/dist/cli/commands/reload.js +35 -0
- package/dist/cli/commands/reload.js.map +1 -0
- package/dist/cli/commands/sites.d.ts +11 -0
- package/dist/cli/commands/sites.d.ts.map +1 -0
- package/dist/cli/commands/sites.js +221 -0
- package/dist/cli/commands/sites.js.map +1 -0
- package/dist/cli/commands/status.d.ts +10 -0
- package/dist/cli/commands/status.d.ts.map +1 -0
- package/dist/cli/commands/status.js +41 -0
- package/dist/cli/commands/status.js.map +1 -0
- package/dist/cli/commands/upstreams.d.ts +21 -0
- package/dist/cli/commands/upstreams.d.ts.map +1 -0
- package/dist/cli/commands/upstreams.js +248 -0
- package/dist/cli/commands/upstreams.js.map +1 -0
- package/dist/cli/dispatcher.d.ts +45 -0
- package/dist/cli/dispatcher.d.ts.map +1 -0
- package/dist/cli/dispatcher.js +192 -0
- package/dist/cli/dispatcher.js.map +1 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +42 -0
- package/dist/index.js.map +1 -0
- package/dist/server/api/db-context.d.ts +50 -0
- package/dist/server/api/db-context.d.ts.map +1 -0
- package/dist/server/api/db-context.js +76 -0
- package/dist/server/api/db-context.js.map +1 -0
- package/dist/server/api/error-mapping.d.ts +19 -0
- package/dist/server/api/error-mapping.d.ts.map +1 -0
- package/dist/server/api/error-mapping.js +56 -0
- package/dist/server/api/error-mapping.js.map +1 -0
- package/dist/server/api/handlers/site-cert.d.ts +49 -0
- package/dist/server/api/handlers/site-cert.d.ts.map +1 -0
- package/dist/server/api/handlers/site-cert.js +54 -0
- package/dist/server/api/handlers/site-cert.js.map +1 -0
- package/dist/server/api/handlers/sites.d.ts +67 -0
- package/dist/server/api/handlers/sites.d.ts.map +1 -0
- package/dist/server/api/handlers/sites.js +78 -0
- package/dist/server/api/handlers/sites.js.map +1 -0
- package/dist/server/api/handlers/upstreams.d.ts +64 -0
- package/dist/server/api/handlers/upstreams.d.ts.map +1 -0
- package/dist/server/api/handlers/upstreams.js +97 -0
- package/dist/server/api/handlers/upstreams.js.map +1 -0
- package/dist/server/auth/require-token.d.ts +24 -0
- package/dist/server/auth/require-token.d.ts.map +1 -0
- package/dist/server/auth/require-token.js +98 -0
- package/dist/server/auth/require-token.js.map +1 -0
- package/dist/server/certs/acme-account.d.ts +37 -0
- package/dist/server/certs/acme-account.d.ts.map +1 -0
- package/dist/server/certs/acme-account.js +49 -0
- package/dist/server/certs/acme-account.js.map +1 -0
- package/dist/server/certs/challenge-store.d.ts +53 -0
- package/dist/server/certs/challenge-store.d.ts.map +1 -0
- package/dist/server/certs/challenge-store.js +66 -0
- package/dist/server/certs/challenge-store.js.map +1 -0
- package/dist/server/certs/issue.d.ts +106 -0
- package/dist/server/certs/issue.d.ts.map +1 -0
- package/dist/server/certs/issue.js +107 -0
- package/dist/server/certs/issue.js.map +1 -0
- package/dist/server/certs/renew.d.ts +34 -0
- package/dist/server/certs/renew.d.ts.map +1 -0
- package/dist/server/certs/renew.js +36 -0
- package/dist/server/certs/renew.js.map +1 -0
- package/dist/server/certs/scheduler.d.ts +68 -0
- package/dist/server/certs/scheduler.d.ts.map +1 -0
- package/dist/server/certs/scheduler.js +76 -0
- package/dist/server/certs/scheduler.js.map +1 -0
- package/dist/server/db/connection.d.ts +10 -0
- package/dist/server/db/connection.d.ts.map +1 -0
- package/dist/server/db/connection.js +16 -0
- package/dist/server/db/connection.js.map +1 -0
- package/dist/server/db/migrate.d.ts +12 -0
- package/dist/server/db/migrate.d.ts.map +1 -0
- package/dist/server/db/migrate.js +37 -0
- package/dist/server/db/migrate.js.map +1 -0
- package/dist/server/db/migrations/0001_init.sql +42 -0
- package/dist/server/domain/cert.d.ts +17 -0
- package/dist/server/domain/cert.d.ts.map +1 -0
- package/dist/server/domain/cert.js +22 -0
- package/dist/server/domain/cert.js.map +1 -0
- package/dist/server/domain/errors.d.ts +36 -0
- package/dist/server/domain/errors.d.ts.map +1 -0
- package/dist/server/domain/errors.js +37 -0
- package/dist/server/domain/errors.js.map +1 -0
- package/dist/server/domain/site.d.ts +15 -0
- package/dist/server/domain/site.d.ts.map +1 -0
- package/dist/server/domain/site.js +25 -0
- package/dist/server/domain/site.js.map +1 -0
- package/dist/server/domain/upstream.d.ts +25 -0
- package/dist/server/domain/upstream.d.ts.map +1 -0
- package/dist/server/domain/upstream.js +24 -0
- package/dist/server/domain/upstream.js.map +1 -0
- package/dist/server/index.d.ts +2 -0
- package/dist/server/index.d.ts.map +1 -0
- package/dist/server/index.js +4 -0
- package/dist/server/index.js.map +1 -0
- package/dist/server/reload/atomic-write.d.ts +44 -0
- package/dist/server/reload/atomic-write.d.ts.map +1 -0
- package/dist/server/reload/atomic-write.js +151 -0
- package/dist/server/reload/atomic-write.js.map +1 -0
- package/dist/server/reload/health-probe.d.ts +62 -0
- package/dist/server/reload/health-probe.d.ts.map +1 -0
- package/dist/server/reload/health-probe.js +105 -0
- package/dist/server/reload/health-probe.js.map +1 -0
- package/dist/server/reload/reload.d.ts +118 -0
- package/dist/server/reload/reload.d.ts.map +1 -0
- package/dist/server/reload/reload.js +232 -0
- package/dist/server/reload/reload.js.map +1 -0
- package/dist/server/renderer/render-bundle.d.ts +18 -0
- package/dist/server/renderer/render-bundle.d.ts.map +1 -0
- package/dist/server/renderer/render-bundle.js +32 -0
- package/dist/server/renderer/render-bundle.js.map +1 -0
- package/dist/server/renderer/render-site.d.ts +5 -0
- package/dist/server/renderer/render-site.d.ts.map +1 -0
- package/dist/server/renderer/render-site.js +144 -0
- package/dist/server/renderer/render-site.js.map +1 -0
- package/dist/server/repositories/cert-repository.d.ts +19 -0
- package/dist/server/repositories/cert-repository.d.ts.map +1 -0
- package/dist/server/repositories/cert-repository.js +112 -0
- package/dist/server/repositories/cert-repository.js.map +1 -0
- package/dist/server/repositories/site-repository.d.ts +17 -0
- package/dist/server/repositories/site-repository.d.ts.map +1 -0
- package/dist/server/repositories/site-repository.js +122 -0
- package/dist/server/repositories/site-repository.js.map +1 -0
- package/dist/server/repositories/upstream-repository.d.ts +22 -0
- package/dist/server/repositories/upstream-repository.d.ts.map +1 -0
- package/dist/server/repositories/upstream-repository.js +142 -0
- package/dist/server/repositories/upstream-repository.js.map +1 -0
- package/dist/server/validator/nginx-binary.d.ts +9 -0
- package/dist/server/validator/nginx-binary.d.ts.map +1 -0
- package/dist/server/validator/nginx-binary.js +11 -0
- package/dist/server/validator/nginx-binary.js.map +1 -0
- package/dist/server/validator/validate.d.ts +29 -0
- package/dist/server/validator/validate.d.ts.map +1 -0
- package/dist/server/validator/validate.js +69 -0
- package/dist/server/validator/validate.js.map +1 -0
- package/dist/server/worker/main.d.ts +43 -0
- package/dist/server/worker/main.d.ts.map +1 -0
- package/dist/server/worker/main.js +181 -0
- package/dist/server/worker/main.js.map +1 -0
- package/dist/version.d.ts +2 -0
- package/dist/version.d.ts.map +1 -0
- package/dist/version.js +2 -0
- package/dist/version.js.map +1 -0
- package/package.json +84 -0
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import { open, readFile, rename, stat, unlink, writeFile } from 'node:fs/promises';
|
|
2
|
+
import { dirname } from 'node:path';
|
|
3
|
+
import { NotFoundError } from '../domain/errors.js';
|
|
4
|
+
/**
|
|
5
|
+
* Suffix appended to the target path for the temp file inside the atomic
|
|
6
|
+
* write/rename dance. Kept predictable (not random) — if two writers race
|
|
7
|
+
* on the same path that is the orchestrator's problem to serialize.
|
|
8
|
+
*/
|
|
9
|
+
const NEW_SUFFIX = '.new';
|
|
10
|
+
/**
|
|
11
|
+
* Read the current contents and mode of `path`, returning a snapshot we can
|
|
12
|
+
* later restore. Distinguishes "file did not exist" from "file existed and
|
|
13
|
+
* was empty" — both are valid prior states with very different rollbacks.
|
|
14
|
+
*/
|
|
15
|
+
async function snapshot(path) {
|
|
16
|
+
try {
|
|
17
|
+
const [contents, info] = await Promise.all([readFile(path), stat(path)]);
|
|
18
|
+
return { existed: true, contents, mode: info.mode };
|
|
19
|
+
}
|
|
20
|
+
catch (err) {
|
|
21
|
+
if (err.code === 'ENOENT') {
|
|
22
|
+
return { existed: false };
|
|
23
|
+
}
|
|
24
|
+
throw err;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* `fsync` the directory that contains `path` so the rename we just did is
|
|
29
|
+
* durable across power loss. POSIX semantics: a successful `rename` is only
|
|
30
|
+
* guaranteed to survive a crash once the parent directory has been synced.
|
|
31
|
+
*
|
|
32
|
+
* On Windows opening a directory for fsync fails — Linux/macOS are the real
|
|
33
|
+
* targets for this control plane, so we catch and ignore the error rather
|
|
34
|
+
* than make the utility unusable for local dev on Windows.
|
|
35
|
+
*/
|
|
36
|
+
async function fsyncDir(path) {
|
|
37
|
+
const dir = dirname(path);
|
|
38
|
+
try {
|
|
39
|
+
const handle = await open(dir, 'r');
|
|
40
|
+
try {
|
|
41
|
+
await handle.sync();
|
|
42
|
+
}
|
|
43
|
+
finally {
|
|
44
|
+
await handle.close();
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
// Best-effort durability. Don't mask the caller's success on Windows.
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Write `contents` to `${path}.new`, fsync it, then rename it over `path`,
|
|
53
|
+
* then fsync the parent directory. This is the core atomic-write dance used
|
|
54
|
+
* for both the initial write and the rollback restore.
|
|
55
|
+
*
|
|
56
|
+
* The `.new` file is always in the same directory as the target so that
|
|
57
|
+
* `rename` is an in-filesystem operation (atomic on POSIX). If a previous
|
|
58
|
+
* crashed run left a stale `${path}.new`, `writeFile` overwrites it.
|
|
59
|
+
*
|
|
60
|
+
* `mode` is forwarded to `writeFile` so we can preserve the prior file's
|
|
61
|
+
* permissions when restoring on rollback.
|
|
62
|
+
*/
|
|
63
|
+
async function atomicReplace(path, contents, mode) {
|
|
64
|
+
const tmp = `${path}${NEW_SUFFIX}`;
|
|
65
|
+
await writeFile(tmp, contents, mode === undefined ? undefined : { mode });
|
|
66
|
+
const handle = await open(tmp, 'r+');
|
|
67
|
+
try {
|
|
68
|
+
await handle.sync();
|
|
69
|
+
}
|
|
70
|
+
finally {
|
|
71
|
+
await handle.close();
|
|
72
|
+
}
|
|
73
|
+
await rename(tmp, path);
|
|
74
|
+
await fsyncDir(path);
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Atomically write `contents` to `path`, returning a rollback handle that
|
|
78
|
+
* can restore the prior state.
|
|
79
|
+
*
|
|
80
|
+
* Algorithm:
|
|
81
|
+
* 1. Snapshot the prior state of `path` (contents + mode, or "didn't
|
|
82
|
+
* exist") into memory.
|
|
83
|
+
* 2. Write `contents` to `${path}.new` in the same directory.
|
|
84
|
+
* 3. fsync the `.new` file so its data is durable before rename.
|
|
85
|
+
* 4. `rename(${path}.new, path)` — atomic on POSIX same-filesystem.
|
|
86
|
+
* 5. fsync the parent directory so the rename itself is durable.
|
|
87
|
+
*
|
|
88
|
+
* Rollback (`restore()`):
|
|
89
|
+
* - If the prior state was "didn't exist", `unlink(path)`.
|
|
90
|
+
* - Otherwise run the same atomic-replace dance with the stashed bytes
|
|
91
|
+
* and mode.
|
|
92
|
+
* - Idempotent: a second `restore()` call is a no-op.
|
|
93
|
+
*/
|
|
94
|
+
export async function writeAtomic(path, contents) {
|
|
95
|
+
const prior = await snapshot(path);
|
|
96
|
+
await atomicReplace(path, contents);
|
|
97
|
+
let restored = false;
|
|
98
|
+
return {
|
|
99
|
+
async restore() {
|
|
100
|
+
if (restored) {
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
restored = true;
|
|
104
|
+
if (!prior.existed) {
|
|
105
|
+
try {
|
|
106
|
+
await unlink(path);
|
|
107
|
+
}
|
|
108
|
+
catch (err) {
|
|
109
|
+
if (err.code === 'ENOENT') {
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
throw err;
|
|
113
|
+
}
|
|
114
|
+
await fsyncDir(path);
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
await atomicReplace(path, prior.contents, prior.mode);
|
|
118
|
+
},
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Atomically delete `path`, returning a rollback handle that restores the
|
|
123
|
+
* file byte-exact (including its mode).
|
|
124
|
+
*
|
|
125
|
+
* Throws {@link NotFoundError} if the target path does not exist — unlike
|
|
126
|
+
* `writeAtomic`, delete is meaningless against an absent file and the
|
|
127
|
+
* orchestrator should treat that as a precondition failure.
|
|
128
|
+
*
|
|
129
|
+
* Rollback (`restore()`):
|
|
130
|
+
* - Atomically re-creates the file with the stashed contents and mode.
|
|
131
|
+
* - Idempotent: a second `restore()` is a no-op.
|
|
132
|
+
*/
|
|
133
|
+
export async function deleteAtomic(path) {
|
|
134
|
+
const prior = await snapshot(path);
|
|
135
|
+
if (!prior.existed) {
|
|
136
|
+
throw new NotFoundError(`cannot delete: file does not exist at ${path}`);
|
|
137
|
+
}
|
|
138
|
+
await unlink(path);
|
|
139
|
+
await fsyncDir(path);
|
|
140
|
+
let restored = false;
|
|
141
|
+
return {
|
|
142
|
+
async restore() {
|
|
143
|
+
if (restored) {
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
restored = true;
|
|
147
|
+
await atomicReplace(path, prior.contents, prior.mode);
|
|
148
|
+
},
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
//# sourceMappingURL=atomic-write.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"atomic-write.js","sourceRoot":"","sources":["../../../src/server/reload/atomic-write.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HTTP health probe with exponential backoff.
|
|
3
|
+
*
|
|
4
|
+
* Used by the reload orchestrator to confirm NGINX is still serving traffic
|
|
5
|
+
* after a config reload. Failures are returned as a result object rather than
|
|
6
|
+
* thrown: the orchestrator decides whether a failed probe triggers a
|
|
7
|
+
* rollback, and we don't want a thrown error to short-circuit cleanup paths.
|
|
8
|
+
*
|
|
9
|
+
* The implementation uses native `fetch` (Node 22+) and `AbortSignal.timeout`
|
|
10
|
+
* so there is no runtime dependency to add.
|
|
11
|
+
*/
|
|
12
|
+
/**
|
|
13
|
+
* Caller-supplied configuration for {@link probeHealth}.
|
|
14
|
+
*
|
|
15
|
+
* The defaults are tuned for a freshly-reloaded NGINX: five attempts with
|
|
16
|
+
* exponential backoff starting at 200ms gives roughly 6.2 seconds of total
|
|
17
|
+
* wall-clock budget, which comfortably exceeds the time NGINX needs to
|
|
18
|
+
* finish reloading worker processes on a healthy host.
|
|
19
|
+
*/
|
|
20
|
+
export interface HealthProbeOptions {
|
|
21
|
+
/** Target URL — typically `http://127.0.0.1/healthz` or similar. */
|
|
22
|
+
url: string;
|
|
23
|
+
/** Maximum number of HTTP attempts. Defaults to 5. */
|
|
24
|
+
maxAttempts?: number;
|
|
25
|
+
/**
|
|
26
|
+
* Base delay (ms) for the exponential backoff between attempts. The actual
|
|
27
|
+
* delay before attempt `n` (1-indexed) is `baseDelayMs * 2^(n - 1)`.
|
|
28
|
+
* Defaults to 200ms (so: 200, 400, 800, 1600, 3200 with the default
|
|
29
|
+
* `maxAttempts`).
|
|
30
|
+
*/
|
|
31
|
+
baseDelayMs?: number;
|
|
32
|
+
/** Per-attempt timeout (ms). Defaults to 2000. */
|
|
33
|
+
timeoutMs?: number;
|
|
34
|
+
/**
|
|
35
|
+
* Predicate that decides whether a response is healthy. Defaults to "any
|
|
36
|
+
* 2xx". The orchestrator can override this for endpoints that return e.g.
|
|
37
|
+
* 204 on success.
|
|
38
|
+
*/
|
|
39
|
+
acceptStatus?: (status: number) => boolean;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Outcome of a probe run. `ok` is the only field the orchestrator needs to
|
|
43
|
+
* branch on — the other fields exist so failures can be surfaced to
|
|
44
|
+
* operators with enough context to debug.
|
|
45
|
+
*/
|
|
46
|
+
export interface HealthProbeResult {
|
|
47
|
+
ok: boolean;
|
|
48
|
+
attempts: number;
|
|
49
|
+
lastStatus?: number;
|
|
50
|
+
lastError?: string;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Probes an HTTP endpoint until it returns an acceptable status or
|
|
54
|
+
* `maxAttempts` is exhausted.
|
|
55
|
+
*
|
|
56
|
+
* The function never throws — transport errors, aborts, and non-acceptable
|
|
57
|
+
* statuses are all reported via the returned {@link HealthProbeResult}.
|
|
58
|
+
* Backoff uses the formula `baseDelayMs * 2^(attempt - 1)` and is skipped
|
|
59
|
+
* after the final attempt (no point waiting if we're not retrying).
|
|
60
|
+
*/
|
|
61
|
+
export declare function probeHealth(opts: HealthProbeOptions): Promise<HealthProbeResult>;
|
|
62
|
+
//# sourceMappingURL=health-probe.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"health-probe.d.ts","sourceRoot":"","sources":["../../../src/server/reload/health-probe.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAoBH;;;;;;;GAOG;AACH,MAAM,WAAW,kBAAkB;IACjC,oEAAoE;IACpE,GAAG,EAAE,MAAM,CAAC;IACZ,sDAAsD;IACtD,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB;;;;;OAKG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,kDAAkD;IAClD,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB;;;;OAIG;IACH,YAAY,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,OAAO,CAAC;CAC5C;AAED;;;;GAIG;AACH,MAAM,WAAW,iBAAiB;IAChC,EAAE,EAAE,OAAO,CAAC;IACZ,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAqBD;;;;;;;;GAQG;AACH,wBAAsB,WAAW,CAAC,IAAI,EAAE,kBAAkB,GAAG,OAAO,CAAC,iBAAiB,CAAC,CAsDtF"}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HTTP health probe with exponential backoff.
|
|
3
|
+
*
|
|
4
|
+
* Used by the reload orchestrator to confirm NGINX is still serving traffic
|
|
5
|
+
* after a config reload. Failures are returned as a result object rather than
|
|
6
|
+
* thrown: the orchestrator decides whether a failed probe triggers a
|
|
7
|
+
* rollback, and we don't want a thrown error to short-circuit cleanup paths.
|
|
8
|
+
*
|
|
9
|
+
* The implementation uses native `fetch` (Node 22+) and `AbortSignal.timeout`
|
|
10
|
+
* so there is no runtime dependency to add.
|
|
11
|
+
*/
|
|
12
|
+
/** Default `acceptStatus` — any 2xx is treated as a healthy response. */
|
|
13
|
+
function isTwoXx(status) {
|
|
14
|
+
return status >= 200 && status < 300;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Sleeps for the given number of milliseconds.
|
|
18
|
+
*
|
|
19
|
+
* Wraps `setTimeout` in a Promise so that callers can `await` it. We avoid
|
|
20
|
+
* pulling in `timers/promises` so the implementation stays trivial and is
|
|
21
|
+
* easy to control from tests via Vitest's fake timers.
|
|
22
|
+
*/
|
|
23
|
+
function sleep(ms) {
|
|
24
|
+
return new Promise((resolve) => {
|
|
25
|
+
setTimeout(resolve, ms);
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Extracts a string message from an unknown thrown value. `fetch` and
|
|
30
|
+
* `AbortSignal` throw a mix of `DOMException`, `TypeError`, and `Error`
|
|
31
|
+
* subclasses; we normalize so `lastError` is always a useful string.
|
|
32
|
+
*/
|
|
33
|
+
function describeError(err) {
|
|
34
|
+
if (err instanceof Error) {
|
|
35
|
+
// `AbortError` / `TimeoutError` thrown by `AbortSignal.timeout` are
|
|
36
|
+
// DOMException subclasses on some runtimes — their `name` is the
|
|
37
|
+
// discriminator. Surface both name and message so timeouts are
|
|
38
|
+
// distinguishable from generic network errors.
|
|
39
|
+
if (err.name === 'TimeoutError' || err.name === 'AbortError') {
|
|
40
|
+
return `${err.name}: ${err.message || 'request aborted'}`;
|
|
41
|
+
}
|
|
42
|
+
return err.message;
|
|
43
|
+
}
|
|
44
|
+
return String(err);
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Probes an HTTP endpoint until it returns an acceptable status or
|
|
48
|
+
* `maxAttempts` is exhausted.
|
|
49
|
+
*
|
|
50
|
+
* The function never throws — transport errors, aborts, and non-acceptable
|
|
51
|
+
* statuses are all reported via the returned {@link HealthProbeResult}.
|
|
52
|
+
* Backoff uses the formula `baseDelayMs * 2^(attempt - 1)` and is skipped
|
|
53
|
+
* after the final attempt (no point waiting if we're not retrying).
|
|
54
|
+
*/
|
|
55
|
+
export async function probeHealth(opts) {
|
|
56
|
+
const maxAttempts = opts.maxAttempts ?? 5;
|
|
57
|
+
const baseDelayMs = opts.baseDelayMs ?? 200;
|
|
58
|
+
const timeoutMs = opts.timeoutMs ?? 2000;
|
|
59
|
+
const acceptStatus = opts.acceptStatus ?? isTwoXx;
|
|
60
|
+
let lastStatus;
|
|
61
|
+
let lastError;
|
|
62
|
+
for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
|
|
63
|
+
// `AbortSignal.timeout` ties the per-attempt deadline to a fresh signal
|
|
64
|
+
// each iteration so a slow attempt cannot bleed its abort into the next
|
|
65
|
+
// one. On Node 22 the underlying timer is unref'd, which is the
|
|
66
|
+
// behaviour we want — a hung probe must not keep the process alive.
|
|
67
|
+
const signal = AbortSignal.timeout(timeoutMs);
|
|
68
|
+
try {
|
|
69
|
+
const response = await fetch(opts.url, { signal });
|
|
70
|
+
lastStatus = response.status;
|
|
71
|
+
lastError = undefined;
|
|
72
|
+
if (acceptStatus(response.status)) {
|
|
73
|
+
const result = {
|
|
74
|
+
ok: true,
|
|
75
|
+
attempts: attempt,
|
|
76
|
+
lastStatus: response.status,
|
|
77
|
+
};
|
|
78
|
+
return result;
|
|
79
|
+
}
|
|
80
|
+
// Non-acceptable status falls through to the retry path.
|
|
81
|
+
}
|
|
82
|
+
catch (err) {
|
|
83
|
+
lastError = describeError(err);
|
|
84
|
+
// Network failures / aborts also fall through to retry.
|
|
85
|
+
}
|
|
86
|
+
// Skip backoff after the final attempt — we're about to return failure
|
|
87
|
+
// either way, so the sleep would just delay the bad news.
|
|
88
|
+
if (attempt < maxAttempts) {
|
|
89
|
+
const delay = baseDelayMs * 2 ** (attempt - 1);
|
|
90
|
+
await sleep(delay);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
const failure = {
|
|
94
|
+
ok: false,
|
|
95
|
+
attempts: maxAttempts,
|
|
96
|
+
};
|
|
97
|
+
if (lastStatus !== undefined) {
|
|
98
|
+
failure.lastStatus = lastStatus;
|
|
99
|
+
}
|
|
100
|
+
if (lastError !== undefined) {
|
|
101
|
+
failure.lastError = lastError;
|
|
102
|
+
}
|
|
103
|
+
return failure;
|
|
104
|
+
}
|
|
105
|
+
//# sourceMappingURL=health-probe.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"health-probe.js","sourceRoot":"","sources":["../../../src/server/reload/health-probe.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reload orchestrator — the single chokepoint that turns a freshly rendered
|
|
3
|
+
* bundle into a live NGINX configuration.
|
|
4
|
+
*
|
|
5
|
+
* The orchestrator is the only place in the codebase allowed to mutate the
|
|
6
|
+
* managed sites directory or send a signal to NGINX. It is deliberately
|
|
7
|
+
* dependency-injected: every external side effect (validate, write, delete,
|
|
8
|
+
* reload, probe, listdir) is exposed via {@link ReloadDeps} so tests can drive
|
|
9
|
+
* each failure path without touching the real filesystem or nginx binary.
|
|
10
|
+
*
|
|
11
|
+
* Flow (each step short-circuits on failure with a labeled {@link ApplyResult}):
|
|
12
|
+
*
|
|
13
|
+
* 1. Validate the candidate via `nginx -t` — pure check, no disk writes.
|
|
14
|
+
* 2. Diff disk against `rendered` to compute the writes + orphan deletes.
|
|
15
|
+
* 3. Apply writes/deletes, accumulating rollback handles in order.
|
|
16
|
+
* 4. SIGHUP NGINX. If it refuses, roll back everything and reload again.
|
|
17
|
+
* 5. Probe the configured health URL. If it fails, roll back + reload again.
|
|
18
|
+
* 6. Success: discard rollback handles.
|
|
19
|
+
*
|
|
20
|
+
* On any step that triggers a rollback we re-run the reload so NGINX matches
|
|
21
|
+
* the disk we just restored. If that second reload also fails, we log both
|
|
22
|
+
* stderrs and still return the original failure — operator intervention is
|
|
23
|
+
* needed anyway, and recursing would hide the real cause.
|
|
24
|
+
*/
|
|
25
|
+
import { type ValidationResult } from '../validator/validate.js';
|
|
26
|
+
import { deleteAtomic as deleteAtomicImpl, writeAtomic as writeAtomicImpl } from './atomic-write.js';
|
|
27
|
+
import { type HealthProbeOptions, type HealthProbeResult } from './health-probe.js';
|
|
28
|
+
/**
|
|
29
|
+
* Minimal subset of `execa`'s result we care about. We only need to know
|
|
30
|
+
* whether the call succeeded and what NGINX wrote to stderr so we can surface
|
|
31
|
+
* it on failure. Keeping the shape narrow lets tests construct fakes without
|
|
32
|
+
* pretending to be the full execa contract.
|
|
33
|
+
*/
|
|
34
|
+
export interface NginxReloadResult {
|
|
35
|
+
exitCode: number | null;
|
|
36
|
+
stderr: string;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Dependency-injection seam. Production code constructs the defaults inside
|
|
40
|
+
* {@link applyDesiredState} when `opts.deps` is omitted; tests pass an
|
|
41
|
+
* exhaustive object so the orchestrator's behaviour is fully observable.
|
|
42
|
+
*
|
|
43
|
+
* `listManagedFiles` returns absolute paths of the `*.conf` files currently
|
|
44
|
+
* sitting in `sitesDir`. The default implementation handles ENOENT (treating
|
|
45
|
+
* it as an empty directory) so the first run before the operator has created
|
|
46
|
+
* the directory does not fail.
|
|
47
|
+
*/
|
|
48
|
+
export interface ReloadDeps {
|
|
49
|
+
validate: (text: string) => Promise<ValidationResult>;
|
|
50
|
+
reload: (bin: string, args: readonly string[]) => Promise<NginxReloadResult>;
|
|
51
|
+
probe: (opts: HealthProbeOptions) => Promise<HealthProbeResult>;
|
|
52
|
+
listManagedFiles: (sitesDir: string) => Promise<string[]>;
|
|
53
|
+
writeAtomic: typeof writeAtomicImpl;
|
|
54
|
+
deleteAtomic: typeof deleteAtomicImpl;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Caller-supplied configuration for {@link applyDesiredState}. The required
|
|
58
|
+
* fields are the two paths/URLs the orchestrator cannot know on its own;
|
|
59
|
+
* everything else is either a defaulted knob or a test seam.
|
|
60
|
+
*/
|
|
61
|
+
export interface ApplyDesiredStateOptions {
|
|
62
|
+
sitesDir: string;
|
|
63
|
+
healthCheckUrl: string;
|
|
64
|
+
healthCheckOptions?: Partial<Omit<HealthProbeOptions, 'url'>>;
|
|
65
|
+
/** Default: `['-s', 'reload']`. */
|
|
66
|
+
nginxReloadArgs?: readonly string[];
|
|
67
|
+
/** Test seam — production code never passes this. */
|
|
68
|
+
deps?: Partial<ReloadDeps>;
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Discriminator for {@link ApplyResult}. Names match the flow steps above so
|
|
72
|
+
* an operator reading a log line can see exactly where the apply stopped.
|
|
73
|
+
* `success` is the only positive value.
|
|
74
|
+
*/
|
|
75
|
+
export type ApplyStep = 'validate' | 'write' | 'reload' | 'probe' | 'success';
|
|
76
|
+
/**
|
|
77
|
+
* Outcome of a single `applyDesiredState` call.
|
|
78
|
+
*
|
|
79
|
+
* - On success: `{ ok: true, step: 'success' }`.
|
|
80
|
+
* - On failure: `{ ok: false, step: <where it stopped>, ... }` with whichever
|
|
81
|
+
* contextual fields the failing step produced (e.g. the failed
|
|
82
|
+
* {@link ValidationResult} or {@link HealthProbeResult}).
|
|
83
|
+
*
|
|
84
|
+
* The orchestrator never throws on operational failures — Route Handlers and
|
|
85
|
+
* the CLI both need a predictable result object to map onto HTTP statuses /
|
|
86
|
+
* exit codes. Programmer errors (e.g. a bad rollback) are still propagated.
|
|
87
|
+
*/
|
|
88
|
+
export interface ApplyResult {
|
|
89
|
+
ok: boolean;
|
|
90
|
+
step: ApplyStep;
|
|
91
|
+
message?: string;
|
|
92
|
+
validation?: ValidationResult;
|
|
93
|
+
probe?: HealthProbeResult;
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Apply a freshly rendered bundle to the managed sites directory.
|
|
97
|
+
*
|
|
98
|
+
* Contract:
|
|
99
|
+
* - `rendered` is the output of `renderBundle`: a map from site id to the
|
|
100
|
+
* NGINX `server { ... }` snippet for that site. Site ids are presumed
|
|
101
|
+
* safe (UUIDs per Phase 1); we do not sanitize them against `..`.
|
|
102
|
+
* - `opts.sitesDir` is the absolute path of the directory Zoomies owns.
|
|
103
|
+
* Anything `*.conf` in there that is not a current site id is an orphan
|
|
104
|
+
* and will be deleted.
|
|
105
|
+
* - `opts.healthCheckUrl` is the post-reload smoke test target.
|
|
106
|
+
*
|
|
107
|
+
* Behaviour:
|
|
108
|
+
* - Pure validation failure -> no disk side effects.
|
|
109
|
+
* - Disk-apply failure -> rolled back to the pre-call state, no reload.
|
|
110
|
+
* - Reload or probe failure -> rolled back AND a second reload kicks NGINX
|
|
111
|
+
* back to the pre-call state. If that second reload also fails the
|
|
112
|
+
* result still reports the original step (reload/probe) — operator
|
|
113
|
+
* intervention is required either way.
|
|
114
|
+
*
|
|
115
|
+
* Returns an {@link ApplyResult}. Never throws on operational failures.
|
|
116
|
+
*/
|
|
117
|
+
export declare function applyDesiredState(rendered: ReadonlyMap<string, string>, opts: ApplyDesiredStateOptions): Promise<ApplyResult>;
|
|
118
|
+
//# sourceMappingURL=reload.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"reload.d.ts","sourceRoot":"","sources":["../../../src/server/reload/reload.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AAOH,OAAO,EAAkB,KAAK,gBAAgB,EAAE,MAAM,0BAA0B,CAAC;AAEjF,OAAO,EAEL,YAAY,IAAI,gBAAgB,EAChC,WAAW,IAAI,eAAe,EAC/B,MAAM,mBAAmB,CAAC;AAC3B,OAAO,EAAE,KAAK,kBAAkB,EAAE,KAAK,iBAAiB,EAAe,MAAM,mBAAmB,CAAC;AAEjG;;;;;GAKG;AACH,MAAM,WAAW,iBAAiB;IAChC,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,MAAM,EAAE,MAAM,CAAC;CAChB;AAED;;;;;;;;;GASG;AACH,MAAM,WAAW,UAAU;IACzB,QAAQ,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,gBAAgB,CAAC,CAAC;IACtD,MAAM,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,SAAS,MAAM,EAAE,KAAK,OAAO,CAAC,iBAAiB,CAAC,CAAC;IAC7E,KAAK,EAAE,CAAC,IAAI,EAAE,kBAAkB,KAAK,OAAO,CAAC,iBAAiB,CAAC,CAAC;IAChE,gBAAgB,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,EAAE,CAAC,CAAC;IAC1D,WAAW,EAAE,OAAO,eAAe,CAAC;IACpC,YAAY,EAAE,OAAO,gBAAgB,CAAC;CACvC;AAED;;;;GAIG;AACH,MAAM,WAAW,wBAAwB;IACvC,QAAQ,EAAE,MAAM,CAAC;IACjB,cAAc,EAAE,MAAM,CAAC;IACvB,kBAAkB,CAAC,EAAE,OAAO,CAAC,IAAI,CAAC,kBAAkB,EAAE,KAAK,CAAC,CAAC,CAAC;IAC9D,mCAAmC;IACnC,eAAe,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IACpC,qDAAqD;IACrD,IAAI,CAAC,EAAE,OAAO,CAAC,UAAU,CAAC,CAAC;CAC5B;AAED;;;;GAIG;AACH,MAAM,MAAM,SAAS,GAAG,UAAU,GAAG,OAAO,GAAG,QAAQ,GAAG,OAAO,GAAG,SAAS,CAAC;AAE9E;;;;;;;;;;;GAWG;AACH,MAAM,WAAW,WAAW;IAC1B,EAAE,EAAE,OAAO,CAAC;IACZ,IAAI,EAAE,SAAS,CAAC;IAChB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,UAAU,CAAC,EAAE,gBAAgB,CAAC;IAC9B,KAAK,CAAC,EAAE,iBAAiB,CAAC;CAC3B;AA8HD;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,wBAAsB,iBAAiB,CACrC,QAAQ,EAAE,WAAW,CAAC,MAAM,EAAE,MAAM,CAAC,EACrC,IAAI,EAAE,wBAAwB,GAC7B,OAAO,CAAC,WAAW,CAAC,CA8EtB"}
|