@wpelevator/tug 0.9.0 → 0.10.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 +16 -10
- package/dist/agent.d.ts +5 -7
- package/dist/agent.js +44 -66
- package/dist/backup-job.js +17 -5
- package/dist/backup-restore.js +4 -4
- package/dist/backup-scheduler.js +2 -0
- package/dist/cli-build.d.ts +2 -0
- package/dist/cli-build.js +14 -1
- package/dist/cli-config.d.ts +4 -1
- package/dist/cli-config.js +28 -23
- package/dist/cli-install.d.ts +7 -4
- package/dist/cli-install.js +21 -23
- package/dist/cli-status.d.ts +29 -0
- package/dist/cli-status.js +61 -0
- package/dist/cli.js +59 -66
- package/dist/container-spec.d.ts +5 -0
- package/dist/container-spec.js +11 -2
- package/dist/control-credential.d.ts +2 -0
- package/dist/control-credential.js +4 -4
- package/dist/control.d.ts +1 -1
- package/dist/control.js +90 -137
- package/dist/credentials.js +1 -1
- package/dist/db.d.ts +0 -10
- package/dist/db.js +1 -2
- package/dist/ingress.d.ts +1 -2
- package/dist/ingress.js +1 -1
- package/dist/main.js +1 -1
- package/dist/node-address.d.ts +2 -0
- package/dist/node-address.js +6 -0
- package/dist/pki.d.ts +1 -1
- package/dist/pki.js +2 -2
- package/dist/protocol.d.ts +2 -34
- package/dist/reconciler.js +40 -7
- package/dist/runners.d.ts +0 -6
- package/dist/runners.js +1 -6
- package/dist/templates/control-compose.yml +1 -2
- package/package.json +1 -1
- package/dist/mesh.d.ts +0 -74
- package/dist/mesh.js +0 -219
package/dist/control.js
CHANGED
|
@@ -23,8 +23,8 @@ import { ensureDir, errMessage, id, now } from "./util.js";
|
|
|
23
23
|
import { ENROLL_DEFAULT_TTL_MS, NODE_LIFETIME_MS, Pki, SERVER_LIFETIME_MS, encodeCredential, identityUri, namesKey, parseIdentityUri, peerIdentity, serverNames, writeFileDurable, } from "./pki.js";
|
|
24
24
|
import { EXPIRY_WARN_DAYS, certificateExpiry, diagnoseNode } from "./diagnostics.js";
|
|
25
25
|
import { serverTlsOptions } from "./tls-server.js";
|
|
26
|
-
import {
|
|
27
|
-
import { buildIngressWork, resolveServiceEndpoints } from "./ingress.js";
|
|
26
|
+
import { isNodeHost } from "./node-address.js";
|
|
27
|
+
import { buildIngressWork, ingressConfigHash, resolveServiceEndpoints } from "./ingress.js";
|
|
28
28
|
import { parseStatusReport } from "./status-report.js";
|
|
29
29
|
import { ACTIVE_DEPLOYMENT_STATES, LIVE_MAX_FRAME_BYTES, LIVE_MAX_BUFFERED_BYTES, LIVE_MAX_SESSIONS, LIVE_START_TIMEOUT_MS, BACKUP_COMMAND_TYPES, } from "./protocol.js";
|
|
30
30
|
import { BACKUP_SECRET_PROJECT, isReservedProject, parseBackupDestination, projectRepository, repositoryPassword, redactSecrets, } from "./backup-destination.js";
|
|
@@ -431,14 +431,8 @@ export async function createApp(options = {}) {
|
|
|
431
431
|
});
|
|
432
432
|
const dbFile = path.join(DATA_DIR, 'tug.db');
|
|
433
433
|
const masterKeyFile = path.join(DATA_DIR, 'master.key');
|
|
434
|
-
|
|
435
|
-
const MESH = process.env.TUG_MESH === 'true';
|
|
436
|
-
const MESH_CIDR = process.env.TUG_MESH_CIDR || DEFAULT_MESH_CIDR;
|
|
437
|
-
const WG_PORT = Number(process.env.TUG_WG_PORT || DEFAULT_WG_PORT);
|
|
438
|
-
const SERVICE_DNS = (process.env.TUG_SERVICE_NAMES ?? String(MESH)) === 'true';
|
|
434
|
+
const SERVICE_DNS = process.env.TUG_SERVICE_NAMES !== 'false';
|
|
439
435
|
const ACME_EMAIL = process.env.TUG_ACME_EMAIL || undefined;
|
|
440
|
-
if (MESH)
|
|
441
|
-
parseCidr(MESH_CIDR);
|
|
442
436
|
const db = await (options.openDatabase || openDb)(dbFile);
|
|
443
437
|
const wakeHub = new WakeHub();
|
|
444
438
|
// Events are an operator-facing log, not a record of truth, so keep them bounded. `0` turns a limit off; anything that is
|
|
@@ -479,14 +473,16 @@ export async function createApp(options = {}) {
|
|
|
479
473
|
const masterKey = await loadOrCreateMasterKey(masterKeyFile);
|
|
480
474
|
// A CA is created only for a fresh installation; existing state with missing or mismatched CA material is a startup error.
|
|
481
475
|
const pki = await Pki.load(path.join(DATA_DIR, 'pki'), !db.get('SELECT 1 FROM operator_credentials') && !db.get('SELECT 1 FROM nodes'));
|
|
482
|
-
const
|
|
483
|
-
|
|
476
|
+
const configuredControlUrl = options.controlUrl ?? process.env.TUG_CONTROL_URL;
|
|
477
|
+
const controlUrl = () => {
|
|
478
|
+
const url = configuredControlUrl;
|
|
484
479
|
if (!url)
|
|
485
|
-
throw httpError(500, '
|
|
480
|
+
throw httpError(500, 'TUG_CONTROL_URL is not set');
|
|
486
481
|
return new URL(url).origin;
|
|
487
482
|
};
|
|
488
483
|
const app = express();
|
|
489
484
|
app.locals.pki = pki;
|
|
485
|
+
app.locals.controlUrl = configuredControlUrl;
|
|
490
486
|
app.locals.dataDir = DATA_DIR;
|
|
491
487
|
/** Resolves a presented certificate (principal URI and key hash) to a principal from current database state. */
|
|
492
488
|
function resolvePrincipal(uri, keyHash) {
|
|
@@ -685,7 +681,40 @@ export async function createApp(options = {}) {
|
|
|
685
681
|
? JSON.parse(byNode.get(node)?.status_json || '{}').observedRevision === state.revision
|
|
686
682
|
: byNode.get(node)?.observed_version === state.version) &&
|
|
687
683
|
['serving', 'skipped'].includes(byNode.get(node)?.health ?? ''));
|
|
688
|
-
|
|
684
|
+
// For projects using managed ingress, wait until each enabled ingress node has applied
|
|
685
|
+
// the current routing configuration. Container health alone does not prove a switch.
|
|
686
|
+
const ingressServices = Object.entries(state.compose.services || {})
|
|
687
|
+
.filter(([, service]) => service['x-tug']?.ingress)
|
|
688
|
+
.map(([name]) => name);
|
|
689
|
+
// A revision that removes its last route must wait for that removal too.
|
|
690
|
+
const previous = db.get(`SELECT ds.state_json FROM desired_states ds JOIN deployments d ON d.id=ds.deployment_id
|
|
691
|
+
WHERE d.project_id=? AND d.id<>? AND d.state='succeeded' ORDER BY d.created_at DESC LIMIT 1`, projectId, project.current_deployment_id);
|
|
692
|
+
const previouslyRouted = previous
|
|
693
|
+
? Object.values(JSON.parse(previous.state_json).compose.services || {}).some((service) => service['x-tug']?.ingress)
|
|
694
|
+
: false;
|
|
695
|
+
const ingressNodes = ingressServices.length || previouslyRouted
|
|
696
|
+
? db.all('SELECT * FROM nodes WHERE ingress=1 AND disabled_at IS NULL')
|
|
697
|
+
: [];
|
|
698
|
+
const staleBefore = new Date(Date.now() - Number(process.env.TUG_STALE_SECONDS || 60) * 1000).toISOString();
|
|
699
|
+
const ingressReady = !ingressNodes.length ||
|
|
700
|
+
ingressNodes.every((node) => {
|
|
701
|
+
const work = ingressFor(node, currentDesiredStates());
|
|
702
|
+
if (!ingressServices.every((service) => work.routes.some((route) => route.project === projectId && route.service === service && route.upstreams.length)))
|
|
703
|
+
return false;
|
|
704
|
+
if (!node.ingress_status_json)
|
|
705
|
+
return false;
|
|
706
|
+
try {
|
|
707
|
+
const status = JSON.parse(node.ingress_status_json);
|
|
708
|
+
return (status.state === 'serving' &&
|
|
709
|
+
status.configHash === ingressConfigHash(work) &&
|
|
710
|
+
typeof status.reportedAt === 'string' &&
|
|
711
|
+
status.reportedAt >= staleBefore);
|
|
712
|
+
}
|
|
713
|
+
catch {
|
|
714
|
+
return false;
|
|
715
|
+
}
|
|
716
|
+
});
|
|
717
|
+
if (ok && ingressReady)
|
|
689
718
|
db.run('UPDATE deployments SET state=?,updated_at=? WHERE id=? AND state=?', 'succeeded', now(), project.current_deployment_id, 'rolling_out');
|
|
690
719
|
}
|
|
691
720
|
/** Events control writes when it rolls back on its own; they are attributed to a reserved system actor. */
|
|
@@ -937,41 +966,7 @@ export async function createApp(options = {}) {
|
|
|
937
966
|
/** A node whose agent has been seen before but not within TUG_STALE_SECONDS; never-seen (just registered) nodes stay schedulable. */
|
|
938
967
|
const isStale = (row) => !!row.last_seen_at &&
|
|
939
968
|
Date.parse(row.last_seen_at) < Date.now() - Number(process.env.TUG_STALE_SECONDS || 60) * 1000;
|
|
940
|
-
const NODE_INFO_SQL = 'SELECT id,name,address,
|
|
941
|
-
/** Allocates the node's stable overlay IP on first registration (retrying if a concurrent register took it). */
|
|
942
|
-
function ensureOverlayIp(nodeId) {
|
|
943
|
-
if (!MESH)
|
|
944
|
-
return;
|
|
945
|
-
for (let attempt = 0; attempt < 5; attempt++) {
|
|
946
|
-
const row = db.get('SELECT overlay_ip FROM nodes WHERE id=?', nodeId);
|
|
947
|
-
if (row?.overlay_ip)
|
|
948
|
-
return;
|
|
949
|
-
const used = db
|
|
950
|
-
.all('SELECT overlay_ip FROM nodes WHERE overlay_ip IS NOT NULL')
|
|
951
|
-
.map((r) => r.overlay_ip);
|
|
952
|
-
try {
|
|
953
|
-
db.run('UPDATE nodes SET overlay_ip=? WHERE id=? AND overlay_ip IS NULL', allocateOverlayIp(MESH_CIDR, used), nodeId);
|
|
954
|
-
return;
|
|
955
|
-
}
|
|
956
|
-
catch (e) {
|
|
957
|
-
if (!isUniqueViolation(e))
|
|
958
|
-
throw e;
|
|
959
|
-
}
|
|
960
|
-
}
|
|
961
|
-
throw new Error('could not allocate an overlay address');
|
|
962
|
-
}
|
|
963
|
-
/** Stores (or refreshes) the node's WireGuard peer entry; the private key never reaches control. */
|
|
964
|
-
function syncWgPeer(nodeId, publicKey) {
|
|
965
|
-
if (!MESH)
|
|
966
|
-
return;
|
|
967
|
-
const node = db.get('SELECT * FROM nodes WHERE id=?', nodeId);
|
|
968
|
-
const existing = db.get('SELECT * FROM wg_peers WHERE node_id=?', nodeId);
|
|
969
|
-
const key = publicKey ?? existing?.public_key;
|
|
970
|
-
if (!node?.overlay_ip || !key)
|
|
971
|
-
return;
|
|
972
|
-
db.run(`INSERT INTO wg_peers(node_id,public_key,overlay_ip,endpoint,allowed_ips,updated_at) VALUES(?,?,?,?,?,?)
|
|
973
|
-
ON CONFLICT(node_id) DO UPDATE SET public_key=excluded.public_key,overlay_ip=excluded.overlay_ip,endpoint=excluded.endpoint,allowed_ips=excluded.allowed_ips,updated_at=excluded.updated_at`, nodeId, key, node.overlay_ip, wgEndpoint(node.public_address, WG_PORT) ?? null, `${node.overlay_ip}/32`, now());
|
|
974
|
-
}
|
|
969
|
+
const NODE_INFO_SQL = 'SELECT id,name,address,labels_json,status,created_at,updated_at FROM nodes WHERE id=?';
|
|
975
970
|
const NODE_RENEW_MIN_INTERVAL_MS = () => Number(process.env.TUG_RENEW_MIN_INTERVAL_MS ?? 60 * 60 * 1000);
|
|
976
971
|
const nodeInfo = (nodeId) => {
|
|
977
972
|
const row = db.get(NODE_INFO_SQL, nodeId);
|
|
@@ -981,8 +976,6 @@ export async function createApp(options = {}) {
|
|
|
981
976
|
id: row.id,
|
|
982
977
|
name: row.name,
|
|
983
978
|
address: row.address,
|
|
984
|
-
public_address: row.public_address,
|
|
985
|
-
overlay_ip: row.overlay_ip,
|
|
986
979
|
labels: row.labels_json ? JSON.parse(row.labels_json) : [],
|
|
987
980
|
status: row.status,
|
|
988
981
|
created_at: row.created_at,
|
|
@@ -996,22 +989,16 @@ export async function createApp(options = {}) {
|
|
|
996
989
|
throw httpError(400, 'node name must be 1-63 letters, numbers, dots, underscores or hyphens');
|
|
997
990
|
if (input.address && !isNodeHost(input.address))
|
|
998
991
|
throw httpError(400, 'address must be an IP address or hostname without a port');
|
|
999
|
-
if (input.publicAddress && !isNodeEndpoint(input.publicAddress))
|
|
1000
|
-
throw httpError(400, 'publicAddress must be an IP address or hostname with an optional port');
|
|
1001
|
-
if (input.wgPublicKey !== undefined && !isWgKey(input.wgPublicKey))
|
|
1002
|
-
throw httpError(400, 'wgPublicKey must be a base64 WireGuard public key');
|
|
1003
992
|
const ts = now();
|
|
1004
|
-
db.run(`INSERT INTO nodes(id,name,address,
|
|
1005
|
-
VALUES(
|
|
1006
|
-
ensureOverlayIp(input.id);
|
|
1007
|
-
syncWgPeer(input.id, input.wgPublicKey);
|
|
993
|
+
db.run(`INSERT INTO nodes(id,name,address,status,labels_json,key_hash,cert_not_after,created_at,updated_at)
|
|
994
|
+
VALUES(?,?,?,?,?,?,?,?,?)`, input.id, input.name, input.address || null, 'registered', JSON.stringify(Array.isArray(input.labels) ? input.labels : []), input.keyHash, input.notAfter.toISOString(), ts, ts);
|
|
1008
995
|
wakeAll();
|
|
1009
996
|
}
|
|
1010
997
|
const identityResponse = (nodeId, cert) => ({
|
|
1011
998
|
node: nodeInfo(nodeId),
|
|
1012
999
|
certificate: cert,
|
|
1013
1000
|
ca: pki.caPem,
|
|
1014
|
-
url:
|
|
1001
|
+
url: controlUrl(),
|
|
1015
1002
|
});
|
|
1016
1003
|
// The node generates its key and CSR; an admin posts the CSR here. The private key is never transmitted.
|
|
1017
1004
|
api('post', '/api/v1/nodes/:name/identity', null, async (req, res) => {
|
|
@@ -1047,7 +1034,6 @@ export async function createApp(options = {}) {
|
|
|
1047
1034
|
keyHash: signed.keyHash,
|
|
1048
1035
|
notAfter: signed.notAfter,
|
|
1049
1036
|
address: body.address,
|
|
1050
|
-
publicAddress: body.publicAddress,
|
|
1051
1037
|
labels: body.labels,
|
|
1052
1038
|
});
|
|
1053
1039
|
}
|
|
@@ -1055,13 +1041,18 @@ export async function createApp(options = {}) {
|
|
|
1055
1041
|
else {
|
|
1056
1042
|
if (!existing)
|
|
1057
1043
|
return res.status(404).json({ error: 'node not found' });
|
|
1058
|
-
|
|
1044
|
+
if (body.address !== undefined && !isNodeHost(body.address))
|
|
1045
|
+
throw httpError(400, 'address must be an IP address or hostname without a port');
|
|
1046
|
+
const address = body.address === undefined ? existing.address : body.address;
|
|
1047
|
+
db.run("UPDATE nodes SET key_hash=?,disabled_at=NULL,status='registered',cert_not_after=?,address=?,updated_at=? WHERE id=?", signed.keyHash, signed.notAfter.toISOString(), address || null, now(), existing.id);
|
|
1059
1048
|
audit(req.actor, {
|
|
1060
1049
|
type: 'node.replaced',
|
|
1061
1050
|
message: `node ${name} key replaced by ${req.actor.name}`,
|
|
1062
1051
|
metadata: { node: name },
|
|
1063
1052
|
});
|
|
1064
1053
|
reconcileSockets();
|
|
1054
|
+
endpointSnapshot = JSON.stringify(endpointsFor(currentDesiredStates()));
|
|
1055
|
+
wakeAll();
|
|
1065
1056
|
}
|
|
1066
1057
|
res.json(identityResponse(nodeId, signed.cert));
|
|
1067
1058
|
}
|
|
@@ -1082,7 +1073,7 @@ export async function createApp(options = {}) {
|
|
|
1082
1073
|
return res.status(400).json({ error: 'labels must be a list of strings' });
|
|
1083
1074
|
const ttlSeconds = Math.max(1, Number(req.body.ttlSeconds ?? ENROLL_DEFAULT_TTL_MS / 1000));
|
|
1084
1075
|
const rowId = id('enr');
|
|
1085
|
-
const issued = await pki.issueClient(identityUri('enroll', rowId), ttlSeconds * 1000,
|
|
1076
|
+
const issued = await pki.issueClient(identityUri('enroll', rowId), ttlSeconds * 1000, controlUrl());
|
|
1086
1077
|
const expiresAt = issued.notAfter.toISOString();
|
|
1087
1078
|
db.run('DELETE FROM enrollment_credentials WHERE expires_at < ?', new Date(Date.now() - 86_400_000).toISOString());
|
|
1088
1079
|
// Minting again for a name revokes its previous unconsumed key.
|
|
@@ -1123,8 +1114,6 @@ export async function createApp(options = {}) {
|
|
|
1123
1114
|
keyHash: signed.keyHash,
|
|
1124
1115
|
notAfter: signed.notAfter,
|
|
1125
1116
|
address: body.address,
|
|
1126
|
-
publicAddress: body.publicAddress,
|
|
1127
|
-
wgPublicKey: body.wgPublicKey,
|
|
1128
1117
|
labels: fresh.labels_json ? JSON.parse(fresh.labels_json) : [],
|
|
1129
1118
|
});
|
|
1130
1119
|
db.run('UPDATE enrollment_credentials SET node_id=?,node_key_hash=? WHERE id=?', fresh.reserved_node_id, signed.keyHash, fresh.id);
|
|
@@ -1173,13 +1162,13 @@ export async function createApp(options = {}) {
|
|
|
1173
1162
|
metadata: { node: node.name },
|
|
1174
1163
|
});
|
|
1175
1164
|
reconcileSockets();
|
|
1176
|
-
//
|
|
1165
|
+
// Other nodes drop the node from service endpoints and ingress configuration on their next work fetch.
|
|
1177
1166
|
endpointSnapshot = JSON.stringify(endpointsFor(currentDesiredStates()));
|
|
1178
1167
|
wakeAll();
|
|
1179
1168
|
res.json({ ok: true, node: node.name });
|
|
1180
1169
|
});
|
|
1181
1170
|
api('get', '/api/v1/nodes', null, async (req, res) => {
|
|
1182
|
-
const rows = db.all('SELECT id,name,address,
|
|
1171
|
+
const rows = db.all('SELECT id,name,address,ingress,ingress_status_json,status,labels_json,last_seen_at,disabled_at,cert_not_after,created_at,updated_at FROM nodes ORDER BY name');
|
|
1183
1172
|
res.json({
|
|
1184
1173
|
nodes: rows.map((row) => {
|
|
1185
1174
|
const { labels_json, ...rest } = row;
|
|
@@ -1197,17 +1186,7 @@ export async function createApp(options = {}) {
|
|
|
1197
1186
|
wakeHub.wake(nodeId, projectId ?? undefined);
|
|
1198
1187
|
return commandId;
|
|
1199
1188
|
}
|
|
1200
|
-
//
|
|
1201
|
-
// generates a new keypair, reports the public key and peers pick it up from their next work response.
|
|
1202
|
-
api('post', '/api/v1/nodes/:name/rotate-wg-key', null, async (req, res) => {
|
|
1203
|
-
if (!MESH)
|
|
1204
|
-
return res.status(409).json({ error: 'the WireGuard mesh is not enabled (TUG_MESH=true)' });
|
|
1205
|
-
const node = findNode(String(req.params.name));
|
|
1206
|
-
if (!node)
|
|
1207
|
-
return res.status(404).json({ error: 'node not found' });
|
|
1208
|
-
res.json({ commandId: queueCommand(node.id, null, 'wg.rotate-key'), nodeId: node.id });
|
|
1209
|
-
});
|
|
1210
|
-
// Removes a retired node: its identity, WireGuard peer, statuses and queued commands. Refused while a current deployment still places
|
|
1189
|
+
// Removes a retired node: its identity, statuses and queued commands. Refused while a current deployment still places
|
|
1211
1190
|
// something on it (redeploy without it first) or while its agent is still reporting, unless force=true.
|
|
1212
1191
|
api('delete', '/api/v1/nodes/:name', null, async (req, res) => {
|
|
1213
1192
|
const node = db.get('SELECT id,name,last_seen_at FROM nodes WHERE name=? OR id=?', String(req.params.name), String(req.params.name));
|
|
@@ -1232,7 +1211,6 @@ export async function createApp(options = {}) {
|
|
|
1232
1211
|
});
|
|
1233
1212
|
db.run('DELETE FROM commands WHERE node_id=?', node.id);
|
|
1234
1213
|
db.run('DELETE FROM node_statuses WHERE node_id=?', node.id);
|
|
1235
|
-
db.run('DELETE FROM wg_peers WHERE node_id=?', node.id);
|
|
1236
1214
|
db.run('DELETE FROM nodes WHERE id=?', node.id);
|
|
1237
1215
|
db.run('UPDATE enrollment_credentials SET revoked_at=COALESCE(revoked_at,?) WHERE name=? AND node_id IS NULL', now(), node.name);
|
|
1238
1216
|
reconcileSockets();
|
|
@@ -1244,33 +1222,37 @@ export async function createApp(options = {}) {
|
|
|
1244
1222
|
wakeAll();
|
|
1245
1223
|
res.json({ ok: true, node: node.name });
|
|
1246
1224
|
});
|
|
1225
|
+
api('post', '/api/v1/nodes/:name/address', null, async (req, res) => {
|
|
1226
|
+
const node = findNode(String(req.params.name));
|
|
1227
|
+
if (!node)
|
|
1228
|
+
return res.status(404).json({ error: 'node not found' });
|
|
1229
|
+
const { address } = req.body;
|
|
1230
|
+
if (!isNodeHost(address))
|
|
1231
|
+
return res
|
|
1232
|
+
.status(400)
|
|
1233
|
+
.json({ error: 'address must be an IP address or hostname without a port' });
|
|
1234
|
+
db.run('UPDATE nodes SET address=?,updated_at=? WHERE id=?', address, now(), node.id);
|
|
1235
|
+
audit(req.actor, {
|
|
1236
|
+
type: 'node.address',
|
|
1237
|
+
message: `node ${node.name} address changed`,
|
|
1238
|
+
metadata: { node: node.name, address },
|
|
1239
|
+
});
|
|
1240
|
+
endpointSnapshot = JSON.stringify(endpointsFor(currentDesiredStates()));
|
|
1241
|
+
wakeAll();
|
|
1242
|
+
res.json({ ok: true, node: node.name, address });
|
|
1243
|
+
});
|
|
1247
1244
|
api('post', '/api/v1/nodes/:name/ingress', null, async (req, res) => {
|
|
1248
1245
|
const node = findNode(String(req.params.name));
|
|
1249
1246
|
if (!node)
|
|
1250
1247
|
return res.status(404).json({ error: 'node not found' });
|
|
1251
1248
|
const enabled = req.body.enabled !== false;
|
|
1252
|
-
db.run('UPDATE nodes SET ingress=?,updated_at=? WHERE id=?', enabled ? 1 : 0, now(), node.id);
|
|
1249
|
+
db.run('UPDATE nodes SET ingress_status_json=CASE WHEN ingress<>? THEN NULL ELSE ingress_status_json END,ingress=?,updated_at=? WHERE id=?', enabled ? 1 : 0, enabled ? 1 : 0, now(), node.id);
|
|
1253
1250
|
wakeHub.wake(node.id);
|
|
1254
1251
|
res.json({ ok: true, node: node.name, ingress: enabled });
|
|
1255
1252
|
});
|
|
1256
|
-
post('/agent/v1/wireguard/key', authAgent, async (req, res) => {
|
|
1257
|
-
if (!MESH)
|
|
1258
|
-
return res.status(409).json({ error: 'the WireGuard mesh is not enabled' });
|
|
1259
|
-
const { publicKey } = req.body;
|
|
1260
|
-
if (!isWgKey(publicKey))
|
|
1261
|
-
return res.status(400).json({ error: 'publicKey must be a base64 WireGuard public key' });
|
|
1262
|
-
try {
|
|
1263
|
-
syncWgPeer(req.node.id, publicKey);
|
|
1264
|
-
wakeAll();
|
|
1265
|
-
res.json({ ok: true });
|
|
1266
|
-
}
|
|
1267
|
-
catch (e) {
|
|
1268
|
-
sendError(res, isUniqueViolation(e) ? httpError(409, 'public key already registered to another node') : e);
|
|
1269
|
-
}
|
|
1270
|
-
});
|
|
1271
1253
|
api('post', '/api/v1/credentials', null, async (req, res) => {
|
|
1272
1254
|
const request = parseCredentialRequest(req.body);
|
|
1273
|
-
const created = await createCredential(db, pki,
|
|
1255
|
+
const created = await createCredential(db, pki, controlUrl(), request, req.actor.name);
|
|
1274
1256
|
audit(req.actor, {
|
|
1275
1257
|
type: 'credential.create',
|
|
1276
1258
|
message: `credential ${created.name} (${created.role}${created.projects ? `, projects: ${created.projects.join(', ')}` : ''}) created by ${req.actor.name}`,
|
|
@@ -2219,15 +2201,6 @@ export async function createApp(options = {}) {
|
|
|
2219
2201
|
await check('docker', async () => ({
|
|
2220
2202
|
output: (await runCommand('docker', ['version', '--format', '{{.Server.Version}}'])).out.trim(),
|
|
2221
2203
|
}));
|
|
2222
|
-
if (MESH) {
|
|
2223
|
-
await check('mesh', async () => {
|
|
2224
|
-
const nodes = db.all('SELECT n.name, n.overlay_ip, EXISTS(SELECT 1 FROM wg_peers w WHERE w.node_id=n.id) AS has_key FROM nodes n');
|
|
2225
|
-
const missing = nodes.filter((n) => !n.has_key).map((n) => n.name);
|
|
2226
|
-
if (missing.length)
|
|
2227
|
-
throw new Error(`nodes without a reported WireGuard key (is TUG_MESH=true on their agents?): ${missing.join(', ')}`);
|
|
2228
|
-
return { cidr: MESH_CIDR, port: WG_PORT, nodes: nodes.length };
|
|
2229
|
-
});
|
|
2230
|
-
}
|
|
2231
2204
|
const ingressNodes = db.all('SELECT name,ingress_status_json FROM nodes WHERE ingress=1');
|
|
2232
2205
|
if (ingressNodes.length) {
|
|
2233
2206
|
await check('ingress', async () => {
|
|
@@ -2261,10 +2234,9 @@ export async function createApp(options = {}) {
|
|
|
2261
2234
|
}
|
|
2262
2235
|
function endpointsFor(states) {
|
|
2263
2236
|
const staleBefore = new Date(Date.now() - Number(process.env.TUG_STALE_SECONDS || 60) * 1000).toISOString();
|
|
2264
|
-
const nodes = db.all('SELECT
|
|
2237
|
+
const nodes = db.all('SELECT * FROM nodes WHERE disabled_at IS NULL');
|
|
2265
2238
|
const views = nodes.map((n) => ({
|
|
2266
2239
|
name: n.name,
|
|
2267
|
-
overlayIp: MESH && n.has_key ? n.overlay_ip : null,
|
|
2268
2240
|
address: n.address,
|
|
2269
2241
|
lastSeenAt: n.last_seen_at,
|
|
2270
2242
|
}));
|
|
@@ -2277,24 +2249,6 @@ export async function createApp(options = {}) {
|
|
|
2277
2249
|
}));
|
|
2278
2250
|
return resolveServiceEndpoints(states, views, statuses, staleBefore);
|
|
2279
2251
|
}
|
|
2280
|
-
function networkFor(node) {
|
|
2281
|
-
if (!MESH || !node.overlay_ip)
|
|
2282
|
-
return undefined;
|
|
2283
|
-
const peers = db.all('SELECT w.*, n.name FROM wg_peers w JOIN nodes n ON n.id=w.node_id WHERE w.node_id != ? AND n.disabled_at IS NULL ORDER BY n.name', node.id);
|
|
2284
|
-
return {
|
|
2285
|
-
interface: WG_INTERFACE,
|
|
2286
|
-
overlayIp: node.overlay_ip,
|
|
2287
|
-
cidr: MESH_CIDR,
|
|
2288
|
-
listenPort: WG_PORT,
|
|
2289
|
-
peers: peers.map((p) => ({
|
|
2290
|
-
node: p.name,
|
|
2291
|
-
publicKey: p.public_key,
|
|
2292
|
-
endpoint: p.endpoint ?? undefined,
|
|
2293
|
-
allowedIps: p.allowed_ips.split(','),
|
|
2294
|
-
persistentKeepalive: WG_KEEPALIVE,
|
|
2295
|
-
})),
|
|
2296
|
-
};
|
|
2297
|
-
}
|
|
2298
2252
|
/** Names of all nodes and of the ingress nodes, for port binding and upstream decisions. */
|
|
2299
2253
|
function nodeTopology() {
|
|
2300
2254
|
const rows = db.all('SELECT name,ingress FROM nodes WHERE disabled_at IS NULL ORDER BY name');
|
|
@@ -2305,7 +2259,6 @@ export async function createApp(options = {}) {
|
|
|
2305
2259
|
}
|
|
2306
2260
|
/** Where this node's published ports listen: the address other nodes and the ingress reach it on, or loopback. */
|
|
2307
2261
|
function portBindingFor(node, states) {
|
|
2308
|
-
const hasKey = MESH && node.overlay_ip && db.get('SELECT 1 FROM wg_peers WHERE node_id=?', node.id);
|
|
2309
2262
|
const { nodes, ingressNodes } = nodeTopology();
|
|
2310
2263
|
const loopback = {};
|
|
2311
2264
|
for (const state of states) {
|
|
@@ -2313,7 +2266,7 @@ export async function createApp(options = {}) {
|
|
|
2313
2266
|
if (services.length)
|
|
2314
2267
|
loopback[state.project] = services;
|
|
2315
2268
|
}
|
|
2316
|
-
return { address:
|
|
2269
|
+
return { address: node.address || null, loopback };
|
|
2317
2270
|
}
|
|
2318
2271
|
function ingressFor(node, states) {
|
|
2319
2272
|
const secrets = new Map();
|
|
@@ -2367,9 +2320,6 @@ export async function createApp(options = {}) {
|
|
|
2367
2320
|
commands,
|
|
2368
2321
|
retainedImages,
|
|
2369
2322
|
};
|
|
2370
|
-
const network = networkFor(node);
|
|
2371
|
-
if (network)
|
|
2372
|
-
work.network = network;
|
|
2373
2323
|
work.ports = portBindingFor(node, desiredStates);
|
|
2374
2324
|
const holds = restores.holds();
|
|
2375
2325
|
if (Object.keys(holds).length)
|
|
@@ -2408,7 +2358,6 @@ export async function createApp(options = {}) {
|
|
|
2408
2358
|
}),
|
|
2409
2359
|
};
|
|
2410
2360
|
db.run(`INSERT INTO node_statuses(id,node_id,project_id,observed_version,status_json,health,updated_at) VALUES(?,?,?,?,?,?,?) ON CONFLICT(node_id,project_id) DO UPDATE SET observed_version=excluded.observed_version,status_json=excluded.status_json,health=excluded.health,updated_at=excluded.updated_at`, id('ns'), req.node.id, st.project, st.observedVersion, JSON.stringify(sanitized), st.health, ts);
|
|
2411
|
-
updateDeploymentConvergence(st.project);
|
|
2412
2361
|
}
|
|
2413
2362
|
// Commands are resolved under the calling node first: only a running command of this node can be
|
|
2414
2363
|
// heartbeated or completed, so foreign and already-finished command ids change nothing.
|
|
@@ -2423,10 +2372,14 @@ export async function createApp(options = {}) {
|
|
|
2423
2372
|
db.run("UPDATE commands SET state=?,result_json=?,updated_at=? WHERE id=? AND node_id=? AND state='running'", cr.state, JSON.stringify(cr.result), ts, cr.commandId, req.node.id);
|
|
2424
2373
|
}
|
|
2425
2374
|
if (report.ingress)
|
|
2426
|
-
db.run('UPDATE nodes SET ingress_status_json=? WHERE id=?', JSON.stringify({ ...report.ingress, reportedAt: ts }), req.node.id);
|
|
2375
|
+
db.run('UPDATE nodes SET ingress_status_json=? WHERE id=? AND ingress=1 AND disabled_at IS NULL', JSON.stringify({ ...report.ingress, reportedAt: ts }), req.node.id);
|
|
2427
2376
|
db.run('UPDATE nodes SET last_seen_at=?,status=? WHERE id=?', ts, 'online', req.node.id);
|
|
2428
2377
|
if (report.sentAt)
|
|
2429
2378
|
db.run('UPDATE nodes SET clock_skew_ms=?,clock_observed_at=? WHERE id=?', Date.parse(report.sentAt) - Date.parse(ts), ts, req.node.id);
|
|
2379
|
+
// Ingress-only nodes can acknowledge routing without reporting any project; recheck
|
|
2380
|
+
// every active rollout after all project and ingress observations have been stored.
|
|
2381
|
+
for (const deployment of db.all("SELECT DISTINCT project_id FROM deployments WHERE state='rolling_out'"))
|
|
2382
|
+
updateDeploymentConvergence(deployment.project_id);
|
|
2430
2383
|
try {
|
|
2431
2384
|
restores.advance();
|
|
2432
2385
|
}
|
|
@@ -2598,18 +2551,18 @@ async function ensureServerCredential(pki, dir, names, force = false) {
|
|
|
2598
2551
|
export async function listen(app, port = PORT) {
|
|
2599
2552
|
const pki = app.locals.pki;
|
|
2600
2553
|
const dataDir = app.locals.dataDir;
|
|
2601
|
-
const
|
|
2602
|
-
if (!
|
|
2603
|
-
throw new Error('
|
|
2604
|
-
const url = new URL(
|
|
2554
|
+
const advertised = app.locals.controlUrl;
|
|
2555
|
+
if (!advertised)
|
|
2556
|
+
throw new Error('TUG_CONTROL_URL is required: the credential-free https origin clients connect to');
|
|
2557
|
+
const url = new URL(advertised);
|
|
2605
2558
|
if (url.protocol !== 'https:' ||
|
|
2606
2559
|
url.username ||
|
|
2607
2560
|
url.password ||
|
|
2608
2561
|
url.search ||
|
|
2609
2562
|
url.hash ||
|
|
2610
2563
|
url.pathname !== '/')
|
|
2611
|
-
throw new Error('
|
|
2612
|
-
const names = serverNames(
|
|
2564
|
+
throw new Error('TUG_CONTROL_URL must be a credential-free https origin such as https://control.example.com:8443');
|
|
2565
|
+
const names = serverNames(advertised, (process.env.TUG_EXTRA_SANS || '').split(','));
|
|
2613
2566
|
const credential = await ensureServerCredential(pki, dataDir, names);
|
|
2614
2567
|
const server = https.createServer(serverTlsOptions({ cert: credential.cert, key: credential.key, ca: pki.caPem }), app);
|
|
2615
2568
|
server.requestTimeout = 5 * 60_000;
|
package/dist/credentials.js
CHANGED
|
@@ -39,7 +39,7 @@ export const ROUTE_ROLES = {
|
|
|
39
39
|
'POST /api/v1/nodes/enrollment-credential': 'admin',
|
|
40
40
|
'POST /api/v1/nodes/:name/identity': 'admin',
|
|
41
41
|
'POST /api/v1/nodes/:name/disable': 'admin',
|
|
42
|
-
'POST /api/v1/nodes/:name/
|
|
42
|
+
'POST /api/v1/nodes/:name/address': 'admin',
|
|
43
43
|
'POST /api/v1/nodes/:name/ingress': 'admin',
|
|
44
44
|
'DELETE /api/v1/nodes/:name': 'admin',
|
|
45
45
|
'POST /api/v1/backups/run': 'deploy',
|
package/dist/db.d.ts
CHANGED
|
@@ -13,8 +13,6 @@ export interface NodeRow {
|
|
|
13
13
|
id: string;
|
|
14
14
|
name: string;
|
|
15
15
|
address: string | null;
|
|
16
|
-
public_address: string | null;
|
|
17
|
-
overlay_ip: string | null;
|
|
18
16
|
ingress: number;
|
|
19
17
|
ingress_status_json: string | null;
|
|
20
18
|
status: string;
|
|
@@ -31,14 +29,6 @@ export interface NodeRow {
|
|
|
31
29
|
created_at: string;
|
|
32
30
|
updated_at: string;
|
|
33
31
|
}
|
|
34
|
-
export interface WgPeerRow {
|
|
35
|
-
node_id: string;
|
|
36
|
-
public_key: string;
|
|
37
|
-
overlay_ip: string;
|
|
38
|
-
endpoint: string | null;
|
|
39
|
-
allowed_ips: string;
|
|
40
|
-
updated_at: string;
|
|
41
|
-
}
|
|
42
32
|
export interface EnrollmentCredentialRow {
|
|
43
33
|
id: string;
|
|
44
34
|
key_hash: string;
|
package/dist/db.js
CHANGED
|
@@ -54,8 +54,7 @@ function initializeControl(db) {
|
|
|
54
54
|
db.exec(`
|
|
55
55
|
CREATE TABLE IF NOT EXISTS meta(key TEXT PRIMARY KEY, value TEXT NOT NULL);
|
|
56
56
|
CREATE TABLE IF NOT EXISTS projects(id TEXT PRIMARY KEY, name TEXT UNIQUE NOT NULL, current_deployment_id TEXT, created_at TEXT, updated_at TEXT);
|
|
57
|
-
CREATE TABLE IF NOT EXISTS nodes(id TEXT PRIMARY KEY, name TEXT UNIQUE NOT NULL, address TEXT,
|
|
58
|
-
CREATE TABLE IF NOT EXISTS wg_peers(node_id TEXT PRIMARY KEY, public_key TEXT NOT NULL UNIQUE, overlay_ip TEXT NOT NULL, endpoint TEXT, allowed_ips TEXT NOT NULL, updated_at TEXT);
|
|
57
|
+
CREATE TABLE IF NOT EXISTS nodes(id TEXT PRIMARY KEY, name TEXT UNIQUE NOT NULL, address TEXT, ingress INTEGER NOT NULL DEFAULT 0, ingress_status_json TEXT, status TEXT, labels_json TEXT, key_hash TEXT, disabled_at TEXT, cert_not_after TEXT, last_renewed_at TEXT, last_seen_at TEXT, clock_skew_ms INTEGER, clock_observed_at TEXT, created_at TEXT, updated_at TEXT);
|
|
59
58
|
CREATE TABLE IF NOT EXISTS enrollment_credentials(id TEXT PRIMARY KEY, key_hash TEXT UNIQUE NOT NULL, name TEXT NOT NULL, labels_json TEXT, expires_at TEXT NOT NULL, revoked_at TEXT, node_id TEXT, reserved_node_id TEXT NOT NULL, node_key_hash TEXT, created_at TEXT);
|
|
60
59
|
CREATE TABLE IF NOT EXISTS deployments(id TEXT PRIMARY KEY, project_id TEXT, version TEXT, state TEXT, created_at TEXT, updated_at TEXT, error_message TEXT, actor TEXT);
|
|
61
60
|
CREATE TABLE IF NOT EXISTS desired_states(id TEXT PRIMARY KEY, project_id TEXT, deployment_id TEXT, version TEXT, state_json TEXT, created_at TEXT);
|
package/dist/ingress.d.ts
CHANGED
|
@@ -4,8 +4,7 @@ export declare const CADDY_CONTAINER = "tug-caddy";
|
|
|
4
4
|
export declare const DEFAULT_CADDY_IMAGE = "caddy:2-alpine";
|
|
5
5
|
export interface NodeView {
|
|
6
6
|
name: string;
|
|
7
|
-
/**
|
|
8
|
-
overlayIp: string | null;
|
|
7
|
+
/** Operator-provided address other nodes use to reach this node's published ports. */
|
|
9
8
|
address: string | null;
|
|
10
9
|
lastSeenAt: string | null;
|
|
11
10
|
}
|
package/dist/ingress.js
CHANGED
|
@@ -26,7 +26,7 @@ export function resolveServiceEndpoints(states, nodes, statuses, staleBefore) {
|
|
|
26
26
|
const endpoints = [];
|
|
27
27
|
for (const nodeName of spec.placement?.nodes || []) {
|
|
28
28
|
const node = byName.get(nodeName);
|
|
29
|
-
const address = node
|
|
29
|
+
const address = node?.address;
|
|
30
30
|
if (!node || !address || !node.lastSeenAt || node.lastSeenAt < staleBefore)
|
|
31
31
|
continue;
|
|
32
32
|
const serviceState = reported.get(`${nodeName}\0${state.project}`)?.serviceStates?.[service];
|
package/dist/main.js
CHANGED
|
@@ -9,7 +9,7 @@ async function readStdin() {
|
|
|
9
9
|
return Buffer.concat(chunks).toString('utf8');
|
|
10
10
|
}
|
|
11
11
|
const usage = `Usage: tug-runtime <control|agent> [command]
|
|
12
|
-
control run the control plane (needs
|
|
12
|
+
control run the control plane (needs TUG_CONTROL_URL)
|
|
13
13
|
control issue-credential issue an operator credential on the control-plane host (--name, --role, --replace)
|
|
14
14
|
agent run the node agent
|
|
15
15
|
agent identity print the CSR for this node's pending key
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { isIP } from 'node:net';
|
|
2
|
+
const HOST_LABEL = /^[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?$/i;
|
|
3
|
+
/** An IP literal or DNS hostname with no port, brackets, whitespace or other syntax that could reach generated configuration. */
|
|
4
|
+
export const isNodeHost = (value) => typeof value === 'string' &&
|
|
5
|
+
value.length <= 253 &&
|
|
6
|
+
(isIP(value) !== 0 || value.split('.').every((label) => HOST_LABEL.test(label)));
|
package/dist/pki.d.ts
CHANGED
|
@@ -59,7 +59,7 @@ export interface SubjectNames {
|
|
|
59
59
|
ip: string[];
|
|
60
60
|
}
|
|
61
61
|
/** The DNS names and IP addresses a server certificate covers, from the public URL plus optional extras. */
|
|
62
|
-
export declare function serverNames(
|
|
62
|
+
export declare function serverNames(controlUrl: string, extras?: string[]): SubjectNames;
|
|
63
63
|
export declare const namesKey: (names: SubjectNames) => string;
|
|
64
64
|
export declare class Pki {
|
|
65
65
|
readonly caPem: string;
|
package/dist/pki.js
CHANGED
|
@@ -149,9 +149,9 @@ export async function verifyCsr(csrPem) {
|
|
|
149
149
|
}
|
|
150
150
|
const serial = () => `0${crypto.randomBytes(15).toString('hex')}`;
|
|
151
151
|
/** The DNS names and IP addresses a server certificate covers, from the public URL plus optional extras. */
|
|
152
|
-
export function serverNames(
|
|
152
|
+
export function serverNames(controlUrl, extras = []) {
|
|
153
153
|
const names = { dns: [], ip: [] };
|
|
154
|
-
const host = new URL(
|
|
154
|
+
const host = new URL(controlUrl).hostname.replace(/^\[|\]$/g, '');
|
|
155
155
|
for (const entry of [host, ...extras.map((e) => e.trim()).filter(Boolean)]) {
|
|
156
156
|
const bucket = net.isIP(entry) ? names.ip : names.dns;
|
|
157
157
|
if (!bucket.includes(entry))
|
package/dist/protocol.d.ts
CHANGED
|
@@ -165,7 +165,7 @@ export declare const ACTIVE_DEPLOYMENT_STATES: readonly ["uploading", "building"
|
|
|
165
165
|
/** Deployment states a CLI wait loop stops on. */
|
|
166
166
|
export declare const TERMINAL_DEPLOYMENT_STATES: readonly ["succeeded", "failed", "build_failed", "cancelled"];
|
|
167
167
|
export type DeploymentState = (typeof ACTIVE_DEPLOYMENT_STATES)[number] | (typeof TERMINAL_DEPLOYMENT_STATES)[number];
|
|
168
|
-
export type CommandType = 'diagnostics.run' | 'service.restart' | 'logs.fetch' | 'volume.delete' | '
|
|
168
|
+
export type CommandType = 'diagnostics.run' | 'service.restart' | 'logs.fetch' | 'volume.delete' | 'backup.check' | 'backup.run' | 'backup.restore' | 'backup.list';
|
|
169
169
|
/** Commands whose helper containers need the backup destination's credentials. */
|
|
170
170
|
export declare const BACKUP_COMMAND_TYPES: readonly CommandType[];
|
|
171
171
|
/** `GET /agent/v1/backup/credentials`: environment for the restic helper of one running backup command. */
|
|
@@ -273,8 +273,6 @@ export type CommandResultPayload = {
|
|
|
273
273
|
project: string;
|
|
274
274
|
volume: string;
|
|
275
275
|
deleted: true;
|
|
276
|
-
} | {
|
|
277
|
-
publicKey: string;
|
|
278
276
|
} | BackupJobResult | {
|
|
279
277
|
error: string;
|
|
280
278
|
};
|
|
@@ -423,8 +421,6 @@ export interface WorkResponse {
|
|
|
423
421
|
desiredStates: DesiredState[];
|
|
424
422
|
commands: Command[];
|
|
425
423
|
retainedImages?: string[];
|
|
426
|
-
/** WireGuard mesh config for this node; present only when the mesh is enabled on control. */
|
|
427
|
-
network?: NetworkConfig;
|
|
428
424
|
/** Healthy cross-node endpoints per project and service; present only when service DNS is enabled. */
|
|
429
425
|
serviceEndpoints?: ServiceEndpoints;
|
|
430
426
|
/** Edge proxy config; present only for nodes marked as ingress nodes. */
|
|
@@ -439,33 +435,11 @@ export interface WorkResponse {
|
|
|
439
435
|
* explicit host IP (including `0.0.0.0`) are never changed.
|
|
440
436
|
*/
|
|
441
437
|
export interface PortBinding {
|
|
442
|
-
/** The node's
|
|
438
|
+
/** The node's `address` (an IP or a hostname the agent resolves); null binds to `127.0.0.1`. */
|
|
443
439
|
address: string | null;
|
|
444
440
|
/** Per project, services whose ingress port only the managed Caddy on this node dials; that port binds to `127.0.0.1`. */
|
|
445
441
|
loopback: Record<string, string[]>;
|
|
446
442
|
}
|
|
447
|
-
export interface WgPeer {
|
|
448
|
-
node: string;
|
|
449
|
-
publicKey: string;
|
|
450
|
-
/** `host:port` the peer can be dialed on; absent for nodes behind NAT (they dial out instead). */
|
|
451
|
-
endpoint?: string;
|
|
452
|
-
allowedIps: string[];
|
|
453
|
-
persistentKeepalive: number;
|
|
454
|
-
}
|
|
455
|
-
/** The `network` block of `GET /agent/v1/work`: everything an agent needs to reconcile `wg0`. */
|
|
456
|
-
export interface NetworkConfig {
|
|
457
|
-
interface: string;
|
|
458
|
-
/** This node's overlay address, e.g. `10.210.0.2`. */
|
|
459
|
-
overlayIp: string;
|
|
460
|
-
/** Mesh CIDR, e.g. `10.210.0.0/16`; `overlayIp` is assigned to `wg0` with this prefix length. */
|
|
461
|
-
cidr: string;
|
|
462
|
-
listenPort: number;
|
|
463
|
-
peers: WgPeer[];
|
|
464
|
-
}
|
|
465
|
-
/** Body of `POST /agent/v1/wireguard/key` (also accepted as `wgPublicKey` on register and enroll). */
|
|
466
|
-
export interface WgKeyRequest {
|
|
467
|
-
publicKey: string;
|
|
468
|
-
}
|
|
469
443
|
export interface ServiceEndpoint {
|
|
470
444
|
node: string;
|
|
471
445
|
address: string;
|
|
@@ -552,8 +526,6 @@ export interface NodeInfo {
|
|
|
552
526
|
id: string;
|
|
553
527
|
name: string;
|
|
554
528
|
address: string | null;
|
|
555
|
-
public_address: string | null;
|
|
556
|
-
overlay_ip?: string | null;
|
|
557
529
|
labels?: string[];
|
|
558
530
|
status: string;
|
|
559
531
|
created_at: string;
|
|
@@ -565,7 +537,6 @@ export interface IdentityRequest {
|
|
|
565
537
|
/** `create` makes a new node (idempotent for the identical key); `replace` installs a new key on an existing node. */
|
|
566
538
|
mode: 'create' | 'replace';
|
|
567
539
|
address?: string;
|
|
568
|
-
publicAddress?: string;
|
|
569
540
|
labels?: string[];
|
|
570
541
|
}
|
|
571
542
|
/** Response of `POST /api/v1/nodes/:name/identity` and `POST /agent/v1/enroll`. */
|
|
@@ -581,9 +552,6 @@ export interface IdentityResponse {
|
|
|
581
552
|
export interface EnrollRequest {
|
|
582
553
|
csr: string;
|
|
583
554
|
address?: string;
|
|
584
|
-
publicAddress?: string;
|
|
585
|
-
/** Base64 WireGuard public key; the private key never leaves the node. */
|
|
586
|
-
wgPublicKey?: string;
|
|
587
555
|
}
|
|
588
556
|
/** Response of `POST /agent/v1/enroll`. */
|
|
589
557
|
export type EnrollResponse = IdentityResponse;
|