@wpelevator/tug 0.8.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 +21 -15
- 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/build.d.ts +6 -0
- package/dist/build.js +27 -7
- package/dist/cli-build.d.ts +2 -0
- package/dist/cli-build.js +22 -2
- 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 +60 -66
- package/dist/compose.d.ts +2 -0
- package/dist/compose.js +9 -0
- 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 +109 -141
- 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/live.d.ts +2 -0
- package/dist/live.js +9 -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 +14 -3
- package/dist/pki.js +24 -7
- 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 -70
- package/dist/mesh.js +0 -202
package/dist/compose.js
CHANGED
|
@@ -289,6 +289,15 @@ export function ingressHostsOf(compose) {
|
|
|
289
289
|
}
|
|
290
290
|
return hosts;
|
|
291
291
|
}
|
|
292
|
+
/** True when two lowercase ingress hosts could route the same request: equal, or one is a `*.` wildcard covering the other. */
|
|
293
|
+
export function ingressHostsOverlap(a, b) {
|
|
294
|
+
if (a === b)
|
|
295
|
+
return true;
|
|
296
|
+
const suffix = (host) => (host.startsWith('*.') ? host.slice(1) : undefined);
|
|
297
|
+
const wa = suffix(a);
|
|
298
|
+
const wb = suffix(b);
|
|
299
|
+
return (wa !== undefined && b.endsWith(wa)) || (wb !== undefined && a.endsWith(wb));
|
|
300
|
+
}
|
|
292
301
|
export function validateCompose(compose, nodes = []) {
|
|
293
302
|
if (!compose?.services || typeof compose.services !== 'object')
|
|
294
303
|
throw new Error('compose file must define services');
|
package/dist/container-spec.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { CreateContainerBody } from './docker-api.ts';
|
|
2
|
+
import type { VolumeInspect } from './docker-api.ts';
|
|
2
3
|
import type { DesiredState, ServiceEndpoint } from './protocol.ts';
|
|
3
4
|
import type { PortBind } from './compose.ts';
|
|
4
5
|
export declare const LABEL_PROJECT = "tug.project";
|
|
@@ -7,10 +8,14 @@ export declare const LABEL_JOB = "tug.job";
|
|
|
7
8
|
export declare const LABEL_SERVICE = "tug.service";
|
|
8
9
|
export declare const LABEL_REVISION = "tug.revision";
|
|
9
10
|
export declare const LABEL_HASH = "tug.config-hash";
|
|
11
|
+
/** Never treat a Docker name alone as proof of ownership; validate its labels before reuse. */
|
|
12
|
+
export declare function assertVolumeOwnership(volume: VolumeInspect, project: string, name: string): void;
|
|
10
13
|
/** The project network every container of the project joins; service names are its aliases. */
|
|
11
14
|
export declare const networkName: (project: string) => string;
|
|
12
15
|
/** Docker volume name for a Compose volume; `external` volumes keep their own name. */
|
|
13
16
|
export declare const volumeName: (project: string, volume: string) => string;
|
|
17
|
+
/** Scratch volumes occupy a separate namespace from project-managed volumes. */
|
|
18
|
+
export declare const scratchVolumeName: (project: string, restore: string) => string;
|
|
14
19
|
export declare const containerName: (project: string, service: string, hash: string) => string;
|
|
15
20
|
export interface VolumeSpec {
|
|
16
21
|
/** Docker volume name. */
|
package/dist/container-spec.js
CHANGED
|
@@ -9,10 +9,17 @@ export const LABEL_JOB = 'tug.job';
|
|
|
9
9
|
export const LABEL_SERVICE = 'tug.service';
|
|
10
10
|
export const LABEL_REVISION = 'tug.revision';
|
|
11
11
|
export const LABEL_HASH = 'tug.config-hash';
|
|
12
|
+
/** Never treat a Docker name alone as proof of ownership; validate its labels before reuse. */
|
|
13
|
+
export function assertVolumeOwnership(volume, project, name) {
|
|
14
|
+
if (volume.Labels?.[LABEL_PROJECT] !== project || volume.Labels?.['tug.volume'] !== name)
|
|
15
|
+
throw new Error(`volume ${volume.Name} is not owned by project ${project} as volume ${name}`);
|
|
16
|
+
}
|
|
12
17
|
/** The project network every container of the project joins; service names are its aliases. */
|
|
13
18
|
export const networkName = (project) => `tug_${project}`;
|
|
14
19
|
/** Docker volume name for a Compose volume; `external` volumes keep their own name. */
|
|
15
|
-
export const volumeName = (project, volume) => `
|
|
20
|
+
export const volumeName = (project, volume) => `tug_v${project.length}_${project}_${volume}`;
|
|
21
|
+
/** Scratch volumes occupy a separate namespace from project-managed volumes. */
|
|
22
|
+
export const scratchVolumeName = (project, restore) => `tug_r${project.length}_${project}_${restore}`;
|
|
16
23
|
export const containerName = (project, service, hash) => `tug-${project}-${service}-${hash.slice(0, 8)}`;
|
|
17
24
|
const SECOND_NS = 1_000_000_000;
|
|
18
25
|
const UNITS = {
|
|
@@ -113,10 +120,12 @@ function volumeSpecs(desired, node, used) {
|
|
|
113
120
|
for (const key of [...used].sort()) {
|
|
114
121
|
const def = desired.compose.volumes?.[key] || {};
|
|
115
122
|
const xe = def['x-tug'] || {};
|
|
123
|
+
const customLabels = { ...(def.labels || {}) };
|
|
124
|
+
delete customLabels['tug.restore'];
|
|
116
125
|
const labels = {
|
|
126
|
+
...customLabels,
|
|
117
127
|
[LABEL_PROJECT]: desired.project,
|
|
118
128
|
'tug.volume': key,
|
|
119
|
-
...(def.labels || {}),
|
|
120
129
|
};
|
|
121
130
|
if (def.external) {
|
|
122
131
|
out.push({
|
|
@@ -2,6 +2,8 @@ export interface IssueArgs {
|
|
|
2
2
|
name: string;
|
|
3
3
|
role: string;
|
|
4
4
|
replace: boolean;
|
|
5
|
+
/** The control URL to write into the credential; defaults to `TUG_CONTROL_URL`. */
|
|
6
|
+
controlUrl?: string;
|
|
5
7
|
}
|
|
6
8
|
export declare function parseIssueArgs(argv: string[]): IssueArgs;
|
|
7
9
|
/**
|
|
@@ -25,15 +25,15 @@ export function parseIssueArgs(argv) {
|
|
|
25
25
|
*/
|
|
26
26
|
export async function issueHostCredential(args) {
|
|
27
27
|
const dataDir = process.env.TUG_DATA_DIR || './data';
|
|
28
|
-
const
|
|
29
|
-
if (!
|
|
30
|
-
throw new Error('
|
|
28
|
+
const controlUrl = args.controlUrl ?? process.env.TUG_CONTROL_URL;
|
|
29
|
+
if (!controlUrl)
|
|
30
|
+
throw new Error('TUG_CONTROL_URL is required');
|
|
31
31
|
const db = await openDb(path.join(dataDir, 'tug.db'));
|
|
32
32
|
try {
|
|
33
33
|
const fresh = !db.get('SELECT 1 FROM operator_credentials') && !db.get('SELECT 1 FROM nodes');
|
|
34
34
|
const pki = await Pki.load(path.join(dataDir, 'pki'), fresh);
|
|
35
35
|
const request = parseCredentialRequest({ name: args.name, role: args.role });
|
|
36
|
-
const created = await createCredential(db, pki, new URL(
|
|
36
|
+
const created = await createCredential(db, pki, new URL(controlUrl).origin, request, 'host', {
|
|
37
37
|
replace: args.replace,
|
|
38
38
|
});
|
|
39
39
|
if (args.replace)
|
package/dist/control.d.ts
CHANGED
|
@@ -27,7 +27,7 @@ export declare function createApp(options?: {
|
|
|
27
27
|
openDatabase?: typeof openDb;
|
|
28
28
|
/** Minimum time between event prunes triggered by agent reports; default one hour. */ eventPruneIntervalMs?: number;
|
|
29
29
|
/** Override the live open/agent startup timeout in milliseconds. */ liveStartTimeoutMs?: number;
|
|
30
|
-
/** Credential-free HTTPS origin clients
|
|
30
|
+
/** Credential-free HTTPS origin clients connect to by default; defaults to `TUG_CONTROL_URL`. */ controlUrl?: string;
|
|
31
31
|
/**
|
|
32
32
|
* Replaces certificate-based identity resolution. Only for in-process tests that skip TLS; production never sets it.
|
|
33
33
|
* It returns the principal URI and key hash a connection would have presented.
|
package/dist/control.js
CHANGED
|
@@ -16,15 +16,15 @@ import * as tar from 'tar';
|
|
|
16
16
|
import { openDb, openSnapshotDb } from "./db.js";
|
|
17
17
|
import { isUniqueViolation } from "./db-driver.js";
|
|
18
18
|
import { encrypt, decrypt, loadOrCreateMasterKey } from "./secrets.js";
|
|
19
|
-
import { buildDesiredState, ingressHostsOf, loopbackIngressServices, serviceSecretNames, validateCompose, } from "./compose.js";
|
|
19
|
+
import { buildDesiredState, ingressHostsOf, ingressHostsOverlap, loopbackIngressServices, serviceSecretNames, validateCompose, } from "./compose.js";
|
|
20
20
|
import { buildImage, unpackContext, run as runCommand } from "./build.js";
|
|
21
21
|
import { sentinelNames } from "./normalize.js";
|
|
22
22
|
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. */
|
|
@@ -708,11 +737,10 @@ export async function createApp(options = {}) {
|
|
|
708
737
|
for (const other of currentDesiredStates()) {
|
|
709
738
|
if (other.project === project)
|
|
710
739
|
continue;
|
|
711
|
-
for (const [host, service] of ingressHostsOf(other.compose))
|
|
712
|
-
const mine
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
}
|
|
740
|
+
for (const [host, service] of ingressHostsOf(other.compose))
|
|
741
|
+
for (const [wantedHost, mine] of wanted)
|
|
742
|
+
if (ingressHostsOverlap(wantedHost, host))
|
|
743
|
+
throw httpError(409, `${mine}: ingress.host ${wantedHost} ${wantedHost === host ? 'is' : `overlaps ${host},`} already routed by project ${other.project} (${service})`);
|
|
716
744
|
}
|
|
717
745
|
}
|
|
718
746
|
function createRollback(project, opts) {
|
|
@@ -938,41 +966,7 @@ export async function createApp(options = {}) {
|
|
|
938
966
|
/** A node whose agent has been seen before but not within TUG_STALE_SECONDS; never-seen (just registered) nodes stay schedulable. */
|
|
939
967
|
const isStale = (row) => !!row.last_seen_at &&
|
|
940
968
|
Date.parse(row.last_seen_at) < Date.now() - Number(process.env.TUG_STALE_SECONDS || 60) * 1000;
|
|
941
|
-
const NODE_INFO_SQL = 'SELECT id,name,address,
|
|
942
|
-
/** Allocates the node's stable overlay IP on first registration (retrying if a concurrent register took it). */
|
|
943
|
-
function ensureOverlayIp(nodeId) {
|
|
944
|
-
if (!MESH)
|
|
945
|
-
return;
|
|
946
|
-
for (let attempt = 0; attempt < 5; attempt++) {
|
|
947
|
-
const row = db.get('SELECT overlay_ip FROM nodes WHERE id=?', nodeId);
|
|
948
|
-
if (row?.overlay_ip)
|
|
949
|
-
return;
|
|
950
|
-
const used = db
|
|
951
|
-
.all('SELECT overlay_ip FROM nodes WHERE overlay_ip IS NOT NULL')
|
|
952
|
-
.map((r) => r.overlay_ip);
|
|
953
|
-
try {
|
|
954
|
-
db.run('UPDATE nodes SET overlay_ip=? WHERE id=? AND overlay_ip IS NULL', allocateOverlayIp(MESH_CIDR, used), nodeId);
|
|
955
|
-
return;
|
|
956
|
-
}
|
|
957
|
-
catch (e) {
|
|
958
|
-
if (!isUniqueViolation(e))
|
|
959
|
-
throw e;
|
|
960
|
-
}
|
|
961
|
-
}
|
|
962
|
-
throw new Error('could not allocate an overlay address');
|
|
963
|
-
}
|
|
964
|
-
/** Stores (or refreshes) the node's WireGuard peer entry; the private key never reaches control. */
|
|
965
|
-
function syncWgPeer(nodeId, publicKey) {
|
|
966
|
-
if (!MESH)
|
|
967
|
-
return;
|
|
968
|
-
const node = db.get('SELECT * FROM nodes WHERE id=?', nodeId);
|
|
969
|
-
const existing = db.get('SELECT * FROM wg_peers WHERE node_id=?', nodeId);
|
|
970
|
-
const key = publicKey ?? existing?.public_key;
|
|
971
|
-
if (!node?.overlay_ip || !key)
|
|
972
|
-
return;
|
|
973
|
-
db.run(`INSERT INTO wg_peers(node_id,public_key,overlay_ip,endpoint,allowed_ips,updated_at) VALUES(?,?,?,?,?,?)
|
|
974
|
-
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());
|
|
975
|
-
}
|
|
969
|
+
const NODE_INFO_SQL = 'SELECT id,name,address,labels_json,status,created_at,updated_at FROM nodes WHERE id=?';
|
|
976
970
|
const NODE_RENEW_MIN_INTERVAL_MS = () => Number(process.env.TUG_RENEW_MIN_INTERVAL_MS ?? 60 * 60 * 1000);
|
|
977
971
|
const nodeInfo = (nodeId) => {
|
|
978
972
|
const row = db.get(NODE_INFO_SQL, nodeId);
|
|
@@ -982,8 +976,6 @@ export async function createApp(options = {}) {
|
|
|
982
976
|
id: row.id,
|
|
983
977
|
name: row.name,
|
|
984
978
|
address: row.address,
|
|
985
|
-
public_address: row.public_address,
|
|
986
|
-
overlay_ip: row.overlay_ip,
|
|
987
979
|
labels: row.labels_json ? JSON.parse(row.labels_json) : [],
|
|
988
980
|
status: row.status,
|
|
989
981
|
created_at: row.created_at,
|
|
@@ -995,20 +987,18 @@ export async function createApp(options = {}) {
|
|
|
995
987
|
function insertNode(input) {
|
|
996
988
|
if (!NODE_NAME.test(input.name))
|
|
997
989
|
throw httpError(400, 'node name must be 1-63 letters, numbers, dots, underscores or hyphens');
|
|
998
|
-
if (input.
|
|
999
|
-
throw httpError(400, '
|
|
990
|
+
if (input.address && !isNodeHost(input.address))
|
|
991
|
+
throw httpError(400, 'address must be an IP address or hostname without a port');
|
|
1000
992
|
const ts = now();
|
|
1001
|
-
db.run(`INSERT INTO nodes(id,name,address,
|
|
1002
|
-
VALUES(
|
|
1003
|
-
ensureOverlayIp(input.id);
|
|
1004
|
-
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);
|
|
1005
995
|
wakeAll();
|
|
1006
996
|
}
|
|
1007
997
|
const identityResponse = (nodeId, cert) => ({
|
|
1008
998
|
node: nodeInfo(nodeId),
|
|
1009
999
|
certificate: cert,
|
|
1010
1000
|
ca: pki.caPem,
|
|
1011
|
-
url:
|
|
1001
|
+
url: controlUrl(),
|
|
1012
1002
|
});
|
|
1013
1003
|
// The node generates its key and CSR; an admin posts the CSR here. The private key is never transmitted.
|
|
1014
1004
|
api('post', '/api/v1/nodes/:name/identity', null, async (req, res) => {
|
|
@@ -1044,7 +1034,6 @@ export async function createApp(options = {}) {
|
|
|
1044
1034
|
keyHash: signed.keyHash,
|
|
1045
1035
|
notAfter: signed.notAfter,
|
|
1046
1036
|
address: body.address,
|
|
1047
|
-
publicAddress: body.publicAddress,
|
|
1048
1037
|
labels: body.labels,
|
|
1049
1038
|
});
|
|
1050
1039
|
}
|
|
@@ -1052,13 +1041,18 @@ export async function createApp(options = {}) {
|
|
|
1052
1041
|
else {
|
|
1053
1042
|
if (!existing)
|
|
1054
1043
|
return res.status(404).json({ error: 'node not found' });
|
|
1055
|
-
|
|
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);
|
|
1056
1048
|
audit(req.actor, {
|
|
1057
1049
|
type: 'node.replaced',
|
|
1058
1050
|
message: `node ${name} key replaced by ${req.actor.name}`,
|
|
1059
1051
|
metadata: { node: name },
|
|
1060
1052
|
});
|
|
1061
1053
|
reconcileSockets();
|
|
1054
|
+
endpointSnapshot = JSON.stringify(endpointsFor(currentDesiredStates()));
|
|
1055
|
+
wakeAll();
|
|
1062
1056
|
}
|
|
1063
1057
|
res.json(identityResponse(nodeId, signed.cert));
|
|
1064
1058
|
}
|
|
@@ -1079,7 +1073,7 @@ export async function createApp(options = {}) {
|
|
|
1079
1073
|
return res.status(400).json({ error: 'labels must be a list of strings' });
|
|
1080
1074
|
const ttlSeconds = Math.max(1, Number(req.body.ttlSeconds ?? ENROLL_DEFAULT_TTL_MS / 1000));
|
|
1081
1075
|
const rowId = id('enr');
|
|
1082
|
-
const issued = await pki.issueClient(identityUri('enroll', rowId), ttlSeconds * 1000,
|
|
1076
|
+
const issued = await pki.issueClient(identityUri('enroll', rowId), ttlSeconds * 1000, controlUrl());
|
|
1083
1077
|
const expiresAt = issued.notAfter.toISOString();
|
|
1084
1078
|
db.run('DELETE FROM enrollment_credentials WHERE expires_at < ?', new Date(Date.now() - 86_400_000).toISOString());
|
|
1085
1079
|
// Minting again for a name revokes its previous unconsumed key.
|
|
@@ -1120,8 +1114,6 @@ export async function createApp(options = {}) {
|
|
|
1120
1114
|
keyHash: signed.keyHash,
|
|
1121
1115
|
notAfter: signed.notAfter,
|
|
1122
1116
|
address: body.address,
|
|
1123
|
-
publicAddress: body.publicAddress,
|
|
1124
|
-
wgPublicKey: body.wgPublicKey,
|
|
1125
1117
|
labels: fresh.labels_json ? JSON.parse(fresh.labels_json) : [],
|
|
1126
1118
|
});
|
|
1127
1119
|
db.run('UPDATE enrollment_credentials SET node_id=?,node_key_hash=? WHERE id=?', fresh.reserved_node_id, signed.keyHash, fresh.id);
|
|
@@ -1170,13 +1162,13 @@ export async function createApp(options = {}) {
|
|
|
1170
1162
|
metadata: { node: node.name },
|
|
1171
1163
|
});
|
|
1172
1164
|
reconcileSockets();
|
|
1173
|
-
//
|
|
1165
|
+
// Other nodes drop the node from service endpoints and ingress configuration on their next work fetch.
|
|
1174
1166
|
endpointSnapshot = JSON.stringify(endpointsFor(currentDesiredStates()));
|
|
1175
1167
|
wakeAll();
|
|
1176
1168
|
res.json({ ok: true, node: node.name });
|
|
1177
1169
|
});
|
|
1178
1170
|
api('get', '/api/v1/nodes', null, async (req, res) => {
|
|
1179
|
-
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');
|
|
1180
1172
|
res.json({
|
|
1181
1173
|
nodes: rows.map((row) => {
|
|
1182
1174
|
const { labels_json, ...rest } = row;
|
|
@@ -1194,17 +1186,7 @@ export async function createApp(options = {}) {
|
|
|
1194
1186
|
wakeHub.wake(nodeId, projectId ?? undefined);
|
|
1195
1187
|
return commandId;
|
|
1196
1188
|
}
|
|
1197
|
-
//
|
|
1198
|
-
// generates a new keypair, reports the public key and peers pick it up from their next work response.
|
|
1199
|
-
api('post', '/api/v1/nodes/:name/rotate-wg-key', null, async (req, res) => {
|
|
1200
|
-
if (!MESH)
|
|
1201
|
-
return res.status(409).json({ error: 'the WireGuard mesh is not enabled (TUG_MESH=true)' });
|
|
1202
|
-
const node = findNode(String(req.params.name));
|
|
1203
|
-
if (!node)
|
|
1204
|
-
return res.status(404).json({ error: 'node not found' });
|
|
1205
|
-
res.json({ commandId: queueCommand(node.id, null, 'wg.rotate-key'), nodeId: node.id });
|
|
1206
|
-
});
|
|
1207
|
-
// 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
|
|
1208
1190
|
// something on it (redeploy without it first) or while its agent is still reporting, unless force=true.
|
|
1209
1191
|
api('delete', '/api/v1/nodes/:name', null, async (req, res) => {
|
|
1210
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));
|
|
@@ -1229,7 +1211,6 @@ export async function createApp(options = {}) {
|
|
|
1229
1211
|
});
|
|
1230
1212
|
db.run('DELETE FROM commands WHERE node_id=?', node.id);
|
|
1231
1213
|
db.run('DELETE FROM node_statuses WHERE node_id=?', node.id);
|
|
1232
|
-
db.run('DELETE FROM wg_peers WHERE node_id=?', node.id);
|
|
1233
1214
|
db.run('DELETE FROM nodes WHERE id=?', node.id);
|
|
1234
1215
|
db.run('UPDATE enrollment_credentials SET revoked_at=COALESCE(revoked_at,?) WHERE name=? AND node_id IS NULL', now(), node.name);
|
|
1235
1216
|
reconcileSockets();
|
|
@@ -1241,33 +1222,37 @@ export async function createApp(options = {}) {
|
|
|
1241
1222
|
wakeAll();
|
|
1242
1223
|
res.json({ ok: true, node: node.name });
|
|
1243
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
|
+
});
|
|
1244
1244
|
api('post', '/api/v1/nodes/:name/ingress', null, async (req, res) => {
|
|
1245
1245
|
const node = findNode(String(req.params.name));
|
|
1246
1246
|
if (!node)
|
|
1247
1247
|
return res.status(404).json({ error: 'node not found' });
|
|
1248
1248
|
const enabled = req.body.enabled !== false;
|
|
1249
|
-
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);
|
|
1250
1250
|
wakeHub.wake(node.id);
|
|
1251
1251
|
res.json({ ok: true, node: node.name, ingress: enabled });
|
|
1252
1252
|
});
|
|
1253
|
-
post('/agent/v1/wireguard/key', authAgent, async (req, res) => {
|
|
1254
|
-
if (!MESH)
|
|
1255
|
-
return res.status(409).json({ error: 'the WireGuard mesh is not enabled' });
|
|
1256
|
-
const { publicKey } = req.body;
|
|
1257
|
-
if (!isWgKey(publicKey))
|
|
1258
|
-
return res.status(400).json({ error: 'publicKey must be a base64 WireGuard public key' });
|
|
1259
|
-
try {
|
|
1260
|
-
syncWgPeer(req.node.id, publicKey);
|
|
1261
|
-
wakeAll();
|
|
1262
|
-
res.json({ ok: true });
|
|
1263
|
-
}
|
|
1264
|
-
catch (e) {
|
|
1265
|
-
sendError(res, isUniqueViolation(e) ? httpError(409, 'public key already registered to another node') : e);
|
|
1266
|
-
}
|
|
1267
|
-
});
|
|
1268
1253
|
api('post', '/api/v1/credentials', null, async (req, res) => {
|
|
1269
1254
|
const request = parseCredentialRequest(req.body);
|
|
1270
|
-
const created = await createCredential(db, pki,
|
|
1255
|
+
const created = await createCredential(db, pki, controlUrl(), request, req.actor.name);
|
|
1271
1256
|
audit(req.actor, {
|
|
1272
1257
|
type: 'credential.create',
|
|
1273
1258
|
message: `credential ${created.name} (${created.role}${created.projects ? `, projects: ${created.projects.join(', ')}` : ''}) created by ${req.actor.name}`,
|
|
@@ -1624,6 +1609,10 @@ export async function createApp(options = {}) {
|
|
|
1624
1609
|
}
|
|
1625
1610
|
}
|
|
1626
1611
|
const nodes = nodeRows.map((row) => ({ name: row.name }));
|
|
1612
|
+
for (const [name, svc] of Object.entries(compose.services || {}))
|
|
1613
|
+
for (const node of svc['x-tug']?.placement?.nodes || [])
|
|
1614
|
+
if (!nodes.some((n) => n.name === node))
|
|
1615
|
+
throw httpError(400, `${name}: placement node ${node} is not an enabled node`);
|
|
1627
1616
|
try {
|
|
1628
1617
|
validateCompose(compose, nodes.map((n) => n.name));
|
|
1629
1618
|
}
|
|
@@ -2212,15 +2201,6 @@ export async function createApp(options = {}) {
|
|
|
2212
2201
|
await check('docker', async () => ({
|
|
2213
2202
|
output: (await runCommand('docker', ['version', '--format', '{{.Server.Version}}'])).out.trim(),
|
|
2214
2203
|
}));
|
|
2215
|
-
if (MESH) {
|
|
2216
|
-
await check('mesh', async () => {
|
|
2217
|
-
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');
|
|
2218
|
-
const missing = nodes.filter((n) => !n.has_key).map((n) => n.name);
|
|
2219
|
-
if (missing.length)
|
|
2220
|
-
throw new Error(`nodes without a reported WireGuard key (is TUG_MESH=true on their agents?): ${missing.join(', ')}`);
|
|
2221
|
-
return { cidr: MESH_CIDR, port: WG_PORT, nodes: nodes.length };
|
|
2222
|
-
});
|
|
2223
|
-
}
|
|
2224
2204
|
const ingressNodes = db.all('SELECT name,ingress_status_json FROM nodes WHERE ingress=1');
|
|
2225
2205
|
if (ingressNodes.length) {
|
|
2226
2206
|
await check('ingress', async () => {
|
|
@@ -2254,10 +2234,9 @@ export async function createApp(options = {}) {
|
|
|
2254
2234
|
}
|
|
2255
2235
|
function endpointsFor(states) {
|
|
2256
2236
|
const staleBefore = new Date(Date.now() - Number(process.env.TUG_STALE_SECONDS || 60) * 1000).toISOString();
|
|
2257
|
-
const nodes = db.all('SELECT
|
|
2237
|
+
const nodes = db.all('SELECT * FROM nodes WHERE disabled_at IS NULL');
|
|
2258
2238
|
const views = nodes.map((n) => ({
|
|
2259
2239
|
name: n.name,
|
|
2260
|
-
overlayIp: MESH && n.has_key ? n.overlay_ip : null,
|
|
2261
2240
|
address: n.address,
|
|
2262
2241
|
lastSeenAt: n.last_seen_at,
|
|
2263
2242
|
}));
|
|
@@ -2270,24 +2249,6 @@ export async function createApp(options = {}) {
|
|
|
2270
2249
|
}));
|
|
2271
2250
|
return resolveServiceEndpoints(states, views, statuses, staleBefore);
|
|
2272
2251
|
}
|
|
2273
|
-
function networkFor(node) {
|
|
2274
|
-
if (!MESH || !node.overlay_ip)
|
|
2275
|
-
return undefined;
|
|
2276
|
-
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);
|
|
2277
|
-
return {
|
|
2278
|
-
interface: WG_INTERFACE,
|
|
2279
|
-
overlayIp: node.overlay_ip,
|
|
2280
|
-
cidr: MESH_CIDR,
|
|
2281
|
-
listenPort: WG_PORT,
|
|
2282
|
-
peers: peers.map((p) => ({
|
|
2283
|
-
node: p.name,
|
|
2284
|
-
publicKey: p.public_key,
|
|
2285
|
-
endpoint: p.endpoint ?? undefined,
|
|
2286
|
-
allowedIps: p.allowed_ips.split(','),
|
|
2287
|
-
persistentKeepalive: WG_KEEPALIVE,
|
|
2288
|
-
})),
|
|
2289
|
-
};
|
|
2290
|
-
}
|
|
2291
2252
|
/** Names of all nodes and of the ingress nodes, for port binding and upstream decisions. */
|
|
2292
2253
|
function nodeTopology() {
|
|
2293
2254
|
const rows = db.all('SELECT name,ingress FROM nodes WHERE disabled_at IS NULL ORDER BY name');
|
|
@@ -2298,7 +2259,6 @@ export async function createApp(options = {}) {
|
|
|
2298
2259
|
}
|
|
2299
2260
|
/** Where this node's published ports listen: the address other nodes and the ingress reach it on, or loopback. */
|
|
2300
2261
|
function portBindingFor(node, states) {
|
|
2301
|
-
const hasKey = MESH && node.overlay_ip && db.get('SELECT 1 FROM wg_peers WHERE node_id=?', node.id);
|
|
2302
2262
|
const { nodes, ingressNodes } = nodeTopology();
|
|
2303
2263
|
const loopback = {};
|
|
2304
2264
|
for (const state of states) {
|
|
@@ -2306,7 +2266,7 @@ export async function createApp(options = {}) {
|
|
|
2306
2266
|
if (services.length)
|
|
2307
2267
|
loopback[state.project] = services;
|
|
2308
2268
|
}
|
|
2309
|
-
return { address:
|
|
2269
|
+
return { address: node.address || null, loopback };
|
|
2310
2270
|
}
|
|
2311
2271
|
function ingressFor(node, states) {
|
|
2312
2272
|
const secrets = new Map();
|
|
@@ -2360,9 +2320,6 @@ export async function createApp(options = {}) {
|
|
|
2360
2320
|
commands,
|
|
2361
2321
|
retainedImages,
|
|
2362
2322
|
};
|
|
2363
|
-
const network = networkFor(node);
|
|
2364
|
-
if (network)
|
|
2365
|
-
work.network = network;
|
|
2366
2323
|
work.ports = portBindingFor(node, desiredStates);
|
|
2367
2324
|
const holds = restores.holds();
|
|
2368
2325
|
if (Object.keys(holds).length)
|
|
@@ -2401,7 +2358,6 @@ export async function createApp(options = {}) {
|
|
|
2401
2358
|
}),
|
|
2402
2359
|
};
|
|
2403
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);
|
|
2404
|
-
updateDeploymentConvergence(st.project);
|
|
2405
2361
|
}
|
|
2406
2362
|
// Commands are resolved under the calling node first: only a running command of this node can be
|
|
2407
2363
|
// heartbeated or completed, so foreign and already-finished command ids change nothing.
|
|
@@ -2416,10 +2372,14 @@ export async function createApp(options = {}) {
|
|
|
2416
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);
|
|
2417
2373
|
}
|
|
2418
2374
|
if (report.ingress)
|
|
2419
|
-
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);
|
|
2420
2376
|
db.run('UPDATE nodes SET last_seen_at=?,status=? WHERE id=?', ts, 'online', req.node.id);
|
|
2421
2377
|
if (report.sentAt)
|
|
2422
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);
|
|
2423
2383
|
try {
|
|
2424
2384
|
restores.advance();
|
|
2425
2385
|
}
|
|
@@ -2460,6 +2420,14 @@ export async function createApp(options = {}) {
|
|
|
2460
2420
|
return res.status(409).json({ error: `command is ${command.state}, not running` });
|
|
2461
2421
|
if (!command.project_id || !backupDestination())
|
|
2462
2422
|
return res.status(409).json({ error: 'no backup destination is configured' });
|
|
2423
|
+
// A command that outlived the project's placement on this node (a newer version moved it) gets nothing.
|
|
2424
|
+
const desiredRow = currentDesired(command.project_id);
|
|
2425
|
+
const desiredNow = desiredRow
|
|
2426
|
+
? JSON.parse(desiredRow.state_json)
|
|
2427
|
+
: undefined;
|
|
2428
|
+
const placedHere = Object.values(desiredNow?.services || {}).some((svc) => svc.placement?.nodes?.includes(req.node.name));
|
|
2429
|
+
if (!placedHere)
|
|
2430
|
+
return res.status(403).json({ error: 'project is no longer placed on this node' });
|
|
2463
2431
|
const response = {
|
|
2464
2432
|
env: {
|
|
2465
2433
|
...backupEnv(),
|
|
@@ -2583,18 +2551,18 @@ async function ensureServerCredential(pki, dir, names, force = false) {
|
|
|
2583
2551
|
export async function listen(app, port = PORT) {
|
|
2584
2552
|
const pki = app.locals.pki;
|
|
2585
2553
|
const dataDir = app.locals.dataDir;
|
|
2586
|
-
const
|
|
2587
|
-
if (!
|
|
2588
|
-
throw new Error('
|
|
2589
|
-
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);
|
|
2590
2558
|
if (url.protocol !== 'https:' ||
|
|
2591
2559
|
url.username ||
|
|
2592
2560
|
url.password ||
|
|
2593
2561
|
url.search ||
|
|
2594
2562
|
url.hash ||
|
|
2595
2563
|
url.pathname !== '/')
|
|
2596
|
-
throw new Error('
|
|
2597
|
-
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(','));
|
|
2598
2566
|
const credential = await ensureServerCredential(pki, dataDir, names);
|
|
2599
2567
|
const server = https.createServer(serverTlsOptions({ cert: credential.cert, key: credential.key, ca: pki.caPem }), app);
|
|
2600
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);
|