@wpelevator/tug 0.6.0 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +24 -22
- package/dist/agent-credential.d.ts +39 -0
- package/dist/agent-credential.js +135 -0
- package/dist/agent.d.ts +15 -9
- package/dist/agent.js +126 -104
- package/dist/backup-check.d.ts +11 -0
- package/dist/backup-check.js +10 -0
- package/dist/backup-config.d.ts +5 -0
- package/dist/backup-config.js +23 -3
- package/dist/backup-destination.d.ts +1 -1
- package/dist/backup-destination.js +1 -1
- package/dist/backup-restore.js +28 -8
- package/dist/backup-scheduler.d.ts +7 -1
- package/dist/backup-scheduler.js +21 -6
- package/dist/cli-build.js +70 -4
- package/dist/cli-config.d.ts +9 -5
- package/dist/cli-config.js +89 -34
- package/dist/cli-install.js +1 -1
- package/dist/cli-live-transport.d.ts +3 -2
- package/dist/cli-live-transport.js +9 -5
- package/dist/cli.js +276 -142
- package/dist/compose.d.ts +3 -1
- package/dist/compose.js +14 -2
- package/dist/control-credential.d.ts +12 -0
- package/dist/control-credential.js +46 -0
- package/dist/control.d.ts +18 -1
- package/dist/control.js +586 -202
- package/dist/credentials.d.ts +111 -0
- package/dist/{tokens.js → credentials.js} +70 -54
- package/dist/db.d.ts +20 -5
- package/dist/db.js +3 -3
- package/dist/diagnostics.d.ts +44 -0
- package/dist/diagnostics.js +65 -0
- package/dist/ingress.js +4 -3
- package/dist/main.js +35 -0
- package/dist/normalize.d.ts +6 -0
- package/dist/normalize.js +16 -2
- package/dist/pki.d.ts +119 -0
- package/dist/pki.js +339 -0
- package/dist/protocol.d.ts +53 -22
- package/dist/status-report.d.ts +11 -0
- package/dist/status-report.js +147 -0
- package/dist/templates/agent.env +1 -3
- package/dist/templates/control-compose.yml +4 -3
- package/dist/templates/control-install.sh +8 -2
- package/dist/tls-server.d.ts +11 -0
- package/dist/tls-server.js +23 -0
- package/package.json +10 -8
- package/dist/tokens.d.ts +0 -91
package/dist/compose.d.ts
CHANGED
|
@@ -23,7 +23,7 @@ export declare function bindPorts(ports: PortEntry[], ip: string, loopbackPort?:
|
|
|
23
23
|
* A service with no placement nodes runs on every node in `nodes`.
|
|
24
24
|
*/
|
|
25
25
|
export declare function loopbackIngressServices(state: DesiredState, node: string, ingressNodes: readonly string[], nodes: readonly string[]): string[];
|
|
26
|
-
/** Where
|
|
26
|
+
/** Where the agent's container definitions bind published ports that name no host IP. */
|
|
27
27
|
export interface PortBind {
|
|
28
28
|
address: string;
|
|
29
29
|
loopback?: ReadonlySet<string>;
|
|
@@ -34,6 +34,8 @@ export declare function addressBoundServices(compose: ComposeFile, names: Iterab
|
|
|
34
34
|
export declare function isRolling(svc: ComposeService | undefined): boolean;
|
|
35
35
|
/** Service keys the agent can create a container from; anything else is rejected with its name. */
|
|
36
36
|
export declare const SUPPORTED_SERVICE_KEYS: readonly string[];
|
|
37
|
+
/** Lower-cased ingress hostnames a Compose definition claims, by `service` name. */
|
|
38
|
+
export declare function ingressHostsOf(compose: ComposeFile): Map<string, string>;
|
|
37
39
|
export declare function validateCompose(compose: ComposeFile | undefined, nodes?: string[]): asserts compose is ComposeFile;
|
|
38
40
|
export interface BuildDesiredStateOptions {
|
|
39
41
|
project: string;
|
package/dist/compose.js
CHANGED
|
@@ -279,6 +279,16 @@ function validateTopLevel(compose, errors) {
|
|
|
279
279
|
errors.push(`volumes.${key}: ${field} is not supported`);
|
|
280
280
|
}
|
|
281
281
|
}
|
|
282
|
+
/** Lower-cased ingress hostnames a Compose definition claims, by `service` name. */
|
|
283
|
+
export function ingressHostsOf(compose) {
|
|
284
|
+
const hosts = new Map();
|
|
285
|
+
for (const [name, svc] of Object.entries(compose.services || {})) {
|
|
286
|
+
const host = svc['x-tug']?.ingress?.host;
|
|
287
|
+
if (typeof host === 'string')
|
|
288
|
+
hosts.set(host.toLowerCase(), name);
|
|
289
|
+
}
|
|
290
|
+
return hosts;
|
|
291
|
+
}
|
|
282
292
|
export function validateCompose(compose, nodes = []) {
|
|
283
293
|
if (!compose?.services || typeof compose.services !== 'object')
|
|
284
294
|
throw new Error('compose file must define services');
|
|
@@ -290,7 +300,9 @@ export function validateCompose(compose, nodes = []) {
|
|
|
290
300
|
const placement = lp.placement || {};
|
|
291
301
|
const selected = placement.nodes || [];
|
|
292
302
|
const hasLabels = Array.isArray(placement.labels) && placement.labels.length > 0;
|
|
293
|
-
const isGlobal = lp.global === true
|
|
303
|
+
const isGlobal = lp.global === true;
|
|
304
|
+
if ('global' in placement)
|
|
305
|
+
errors.push(`${name}: x-tug.placement.global is not supported; use x-tug.global`);
|
|
294
306
|
if (nodes.length > 1 && selected.length === 0 && !hasLabels && !isGlobal)
|
|
295
307
|
errors.push(`${name}: multi-node deployments require x-tug.placement.nodes or placement.labels`);
|
|
296
308
|
if (lp.replicas &&
|
|
@@ -313,7 +325,7 @@ export function buildDesiredState({ project, version, compose, revision, default
|
|
|
313
325
|
for (const [name, src] of Object.entries(compose.services || {})) {
|
|
314
326
|
const lp = src['x-tug'] || {};
|
|
315
327
|
let placement = lp.placement || (defaultNode ? { nodes: [defaultNode] } : {});
|
|
316
|
-
const global = lp.global === true
|
|
328
|
+
const global = lp.global === true;
|
|
317
329
|
const labels = placement.labels || [];
|
|
318
330
|
if (nodes && (!placement.nodes || !placement.nodes.length) && (labels.length || global)) {
|
|
319
331
|
const matched = nodes
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export interface IssueArgs {
|
|
2
|
+
name: string;
|
|
3
|
+
role: string;
|
|
4
|
+
replace: boolean;
|
|
5
|
+
}
|
|
6
|
+
export declare function parseIssueArgs(argv: string[]): IssueArgs;
|
|
7
|
+
/**
|
|
8
|
+
* Host-side issuance (`docker exec ... control issue-credential`), independent of the API and of any running listener. It
|
|
9
|
+
* is create-only unless `replace` is set, which replaces the named credential's key; a running control closes the old
|
|
10
|
+
* key's sessions within a second. Returns the credential once; control keeps no private-key copy.
|
|
11
|
+
*/
|
|
12
|
+
export declare function issueHostCredential(args: IssueArgs): Promise<string>;
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { openDb } from "./db.js";
|
|
3
|
+
import { Pki, encodeCredential } from "./pki.js";
|
|
4
|
+
import { ROLES, createCredential, isRole, parseCredentialRequest } from "./credentials.js";
|
|
5
|
+
export function parseIssueArgs(argv) {
|
|
6
|
+
const args = { name: 'root', role: 'admin', replace: false };
|
|
7
|
+
for (let i = 0; i < argv.length; i++) {
|
|
8
|
+
if (argv[i] === '--name')
|
|
9
|
+
args.name = argv[++i];
|
|
10
|
+
else if (argv[i] === '--role')
|
|
11
|
+
args.role = argv[++i];
|
|
12
|
+
else if (argv[i] === '--replace')
|
|
13
|
+
args.replace = true;
|
|
14
|
+
else
|
|
15
|
+
throw new Error(`unknown argument ${argv[i]}`);
|
|
16
|
+
}
|
|
17
|
+
if (!isRole(args.role))
|
|
18
|
+
throw new Error(`--role must be one of ${ROLES.join(', ')}`);
|
|
19
|
+
return args;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Host-side issuance (`docker exec ... control issue-credential`), independent of the API and of any running listener. It
|
|
23
|
+
* is create-only unless `replace` is set, which replaces the named credential's key; a running control closes the old
|
|
24
|
+
* key's sessions within a second. Returns the credential once; control keeps no private-key copy.
|
|
25
|
+
*/
|
|
26
|
+
export async function issueHostCredential(args) {
|
|
27
|
+
const dataDir = process.env.TUG_DATA_DIR || './data';
|
|
28
|
+
const publicUrl = process.env.TUG_PUBLIC_URL;
|
|
29
|
+
if (!publicUrl)
|
|
30
|
+
throw new Error('TUG_PUBLIC_URL is required');
|
|
31
|
+
const db = await openDb(path.join(dataDir, 'tug.db'));
|
|
32
|
+
try {
|
|
33
|
+
const fresh = !db.get('SELECT 1 FROM operator_credentials') && !db.get('SELECT 1 FROM nodes');
|
|
34
|
+
const pki = await Pki.load(path.join(dataDir, 'pki'), fresh);
|
|
35
|
+
const request = parseCredentialRequest({ name: args.name, role: args.role });
|
|
36
|
+
const created = await createCredential(db, pki, new URL(publicUrl).origin, request, 'host', {
|
|
37
|
+
replace: args.replace,
|
|
38
|
+
});
|
|
39
|
+
if (args.replace)
|
|
40
|
+
db.run('INSERT INTO events(id,level,type,message,metadata_json,created_at,actor) VALUES(?,?,?,?,?,?,?)', `evt_${Date.now().toString(36)}`, 'info', 'credential.recover', `credential ${args.name} replaced on the control host`, '{}', new Date().toISOString(), 'host');
|
|
41
|
+
return encodeCredential(created.credential);
|
|
42
|
+
}
|
|
43
|
+
finally {
|
|
44
|
+
db.close();
|
|
45
|
+
}
|
|
46
|
+
}
|
package/dist/control.d.ts
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import express from 'express';
|
|
3
3
|
import { resolveImage } from './registry.ts';
|
|
4
|
+
import type { Request } from 'express';
|
|
4
5
|
import { saveImageArchive, inspectImage, validateImageArchive } from './artifacts.ts';
|
|
5
6
|
import https from 'node:https';
|
|
6
7
|
import http from 'node:http';
|
|
7
8
|
import { openDb } from './db.ts';
|
|
9
|
+
import type { Db } from './db.ts';
|
|
8
10
|
import type { NodeRow } from './db.ts';
|
|
9
11
|
import { buildImage, run as runCommand } from './build.ts';
|
|
10
12
|
declare module 'express-serve-static-core' {
|
|
@@ -25,5 +27,20 @@ export declare function createApp(options?: {
|
|
|
25
27
|
openDatabase?: typeof openDb;
|
|
26
28
|
/** Minimum time between event prunes triggered by agent reports; default one hour. */ eventPruneIntervalMs?: number;
|
|
27
29
|
/** Override the live open/agent startup timeout in milliseconds. */ liveStartTimeoutMs?: number;
|
|
30
|
+
/** Credential-free HTTPS origin clients use to reach control; defaults to `TUG_PUBLIC_URL`. */ publicUrl?: string;
|
|
31
|
+
/**
|
|
32
|
+
* Replaces certificate-based identity resolution. Only for in-process tests that skip TLS; production never sets it.
|
|
33
|
+
* It returns the principal URI and key hash a connection would have presented.
|
|
34
|
+
*/
|
|
35
|
+
identity?: (req: Request, db: Db) => {
|
|
36
|
+
uri: string;
|
|
37
|
+
keyHash: string;
|
|
38
|
+
} | null;
|
|
28
39
|
}): Promise<import("express-serve-static-core").Express>;
|
|
29
|
-
|
|
40
|
+
/** Attaches the agent and operator WebSocket upgrade handling to `server`; `listen` uses it on the mTLS listener. */
|
|
41
|
+
export declare function attachAgentStream(app: express.Express, server: http.Server | https.Server): void;
|
|
42
|
+
/**
|
|
43
|
+
* Starts the single mTLS listener. Chain validation rejects missing or foreign client certificates during the handshake;
|
|
44
|
+
* the secureConnection gate then checks current database state, and every request checks it again.
|
|
45
|
+
*/
|
|
46
|
+
export declare function listen(app: express.Express, port?: number): Promise<https.Server>;
|