@knowzai/cli 0.3.0 → 0.5.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 +68 -0
- package/assets/portable/releases.json +42 -0
- package/assets/selfhosted/.env.example +22 -1
- package/assets/selfhosted/docker-compose.mssql.yml +113 -0
- package/assets/selfhosted/docker-compose.yml +29 -15
- package/assets/selfhosted/lmstudio-azure-bridge.mjs +73 -0
- package/assets/selfhosted/releases.json +129 -6
- package/bin/run.js +2 -0
- package/dist/.tsbuildinfo +1 -1
- package/dist/base-command.d.ts +2 -0
- package/dist/base-command.d.ts.map +1 -1
- package/dist/base-command.js +11 -1
- package/dist/base-command.js.map +1 -1
- package/dist/commands/ask.d.ts.map +1 -1
- package/dist/commands/ask.js +2 -0
- package/dist/commands/ask.js.map +1 -1
- package/dist/commands/backup.d.ts +1 -0
- package/dist/commands/backup.d.ts.map +1 -1
- package/dist/commands/backup.js +29 -10
- package/dist/commands/backup.js.map +1 -1
- package/dist/commands/chat.d.ts +2 -1
- package/dist/commands/chat.d.ts.map +1 -1
- package/dist/commands/chat.js +10 -4
- package/dist/commands/chat.js.map +1 -1
- package/dist/commands/down.d.ts +14 -27
- package/dist/commands/down.d.ts.map +1 -1
- package/dist/commands/down.js +6 -31
- package/dist/commands/down.js.map +1 -1
- package/dist/commands/knowledge/amend.d.ts +2 -2
- package/dist/commands/knowledge/amend.d.ts.map +1 -1
- package/dist/commands/knowledge/amend.js +5 -0
- package/dist/commands/knowledge/amend.js.map +1 -1
- package/dist/commands/login.d.ts +4 -0
- package/dist/commands/login.d.ts.map +1 -1
- package/dist/commands/login.js +21 -2
- package/dist/commands/login.js.map +1 -1
- package/dist/commands/menu.js +1 -1
- package/dist/commands/menu.js.map +1 -1
- package/dist/commands/platform/destroy.d.ts +15 -5
- package/dist/commands/platform/destroy.d.ts.map +1 -1
- package/dist/commands/platform/destroy.js +6 -33
- package/dist/commands/platform/destroy.js.map +1 -1
- package/dist/commands/platform/doctor.d.ts +18 -3
- package/dist/commands/platform/doctor.d.ts.map +1 -1
- package/dist/commands/platform/doctor.js +4 -10
- package/dist/commands/platform/doctor.js.map +1 -1
- package/dist/commands/platform/down.d.ts +15 -15
- package/dist/commands/platform/down.d.ts.map +1 -1
- package/dist/commands/platform/down.js +4 -8
- package/dist/commands/platform/down.js.map +1 -1
- package/dist/commands/platform/logs.d.ts +15 -14
- package/dist/commands/platform/logs.d.ts.map +1 -1
- package/dist/commands/platform/logs.js +5 -16
- package/dist/commands/platform/logs.js.map +1 -1
- package/dist/commands/platform/reset.d.ts +16 -13
- package/dist/commands/platform/reset.d.ts.map +1 -1
- package/dist/commands/platform/reset.js +7 -36
- package/dist/commands/platform/reset.js.map +1 -1
- package/dist/commands/platform/setup.d.ts +1 -0
- package/dist/commands/platform/setup.d.ts.map +1 -1
- package/dist/commands/platform/setup.js +1 -1
- package/dist/commands/platform/setup.js.map +1 -1
- package/dist/commands/platform/status.d.ts +14 -16
- package/dist/commands/platform/status.d.ts.map +1 -1
- package/dist/commands/platform/status.js +4 -29
- package/dist/commands/platform/status.js.map +1 -1
- package/dist/commands/restore.d.ts +10 -1
- package/dist/commands/restore.d.ts.map +1 -1
- package/dist/commands/restore.js +56 -13
- package/dist/commands/restore.js.map +1 -1
- package/dist/commands/runtime/destroy.d.ts +29 -0
- package/dist/commands/runtime/destroy.d.ts.map +1 -0
- package/dist/commands/runtime/destroy.js +13 -0
- package/dist/commands/runtime/destroy.js.map +1 -0
- package/dist/commands/runtime/doctor.d.ts +27 -0
- package/dist/commands/runtime/doctor.d.ts.map +1 -0
- package/dist/commands/runtime/doctor.js +11 -0
- package/dist/commands/runtime/doctor.js.map +1 -0
- package/dist/commands/runtime/logs.d.ts +28 -0
- package/dist/commands/runtime/logs.d.ts.map +1 -0
- package/dist/commands/runtime/logs.js +12 -0
- package/dist/commands/runtime/logs.js.map +1 -0
- package/dist/commands/runtime/status.d.ts +27 -0
- package/dist/commands/runtime/status.d.ts.map +1 -0
- package/dist/commands/runtime/status.js +11 -0
- package/dist/commands/runtime/status.js.map +1 -0
- package/dist/commands/runtime/upgrade.d.ts +28 -0
- package/dist/commands/runtime/upgrade.d.ts.map +1 -0
- package/dist/commands/runtime/upgrade.js +12 -0
- package/dist/commands/runtime/upgrade.js.map +1 -0
- package/dist/commands/setup.d.ts +1 -0
- package/dist/commands/setup.d.ts.map +1 -1
- package/dist/commands/setup.js +1 -1
- package/dist/commands/transfer.js +1 -1
- package/dist/commands/transfer.js.map +1 -1
- package/dist/commands/up.d.ts +3 -11
- package/dist/commands/up.d.ts.map +1 -1
- package/dist/commands/up.js +48 -36
- package/dist/commands/up.js.map +1 -1
- package/dist/commands/whoami.js +1 -1
- package/dist/commands/whoami.js.map +1 -1
- package/dist/hooks/init/launch-menu.d.ts.map +1 -1
- package/dist/hooks/init/launch-menu.js +2 -0
- package/dist/hooks/init/launch-menu.js.map +1 -1
- package/dist/lib/anonymous-registry.d.ts +3 -0
- package/dist/lib/anonymous-registry.d.ts.map +1 -0
- package/dist/lib/anonymous-registry.js +25 -0
- package/dist/lib/anonymous-registry.js.map +1 -0
- package/dist/lib/async-process.d.ts +15 -0
- package/dist/lib/async-process.d.ts.map +1 -0
- package/dist/lib/async-process.js +145 -0
- package/dist/lib/async-process.js.map +1 -0
- package/dist/lib/backup-process.d.ts +4 -0
- package/dist/lib/backup-process.d.ts.map +1 -0
- package/dist/lib/backup-process.js +20 -0
- package/dist/lib/backup-process.js.map +1 -0
- package/dist/lib/brand.d.ts +25 -0
- package/dist/lib/brand.d.ts.map +1 -0
- package/dist/lib/brand.js +105 -0
- package/dist/lib/brand.js.map +1 -0
- package/dist/lib/chat.d.ts +1 -0
- package/dist/lib/chat.d.ts.map +1 -1
- package/dist/lib/chat.js +12 -3
- package/dist/lib/chat.js.map +1 -1
- package/dist/lib/compose-status.d.ts +14 -0
- package/dist/lib/compose-status.d.ts.map +1 -0
- package/dist/lib/compose-status.js +31 -0
- package/dist/lib/compose-status.js.map +1 -0
- package/dist/lib/container-engine.js +2 -2
- package/dist/lib/container-engine.js.map +1 -1
- package/dist/lib/local-engine-context.d.ts +16 -0
- package/dist/lib/local-engine-context.d.ts.map +1 -0
- package/dist/lib/local-engine-context.js +123 -0
- package/dist/lib/local-engine-context.js.map +1 -0
- package/dist/lib/login-guided.d.ts.map +1 -1
- package/dist/lib/login-guided.js +2 -1
- package/dist/lib/login-guided.js.map +1 -1
- package/dist/lib/login.d.ts +3 -0
- package/dist/lib/login.d.ts.map +1 -1
- package/dist/lib/login.js +25 -5
- package/dist/lib/login.js.map +1 -1
- package/dist/lib/oci-registry-probe.d.ts +43 -0
- package/dist/lib/oci-registry-probe.d.ts.map +1 -0
- package/dist/lib/oci-registry-probe.js +110 -0
- package/dist/lib/oci-registry-probe.js.map +1 -0
- package/dist/lib/peer-command.d.ts.map +1 -1
- package/dist/lib/peer-command.js +3 -1
- package/dist/lib/peer-command.js.map +1 -1
- package/dist/lib/platform-acquisition.d.ts +27 -0
- package/dist/lib/platform-acquisition.d.ts.map +1 -0
- package/dist/lib/platform-acquisition.js +21 -0
- package/dist/lib/platform-acquisition.js.map +1 -0
- package/dist/lib/platform-backup.d.ts +1 -0
- package/dist/lib/platform-backup.d.ts.map +1 -1
- package/dist/lib/platform-backup.js +375 -157
- package/dist/lib/platform-backup.js.map +1 -1
- package/dist/lib/platform-command-shared.d.ts +2 -0
- package/dist/lib/platform-command-shared.d.ts.map +1 -1
- package/dist/lib/platform-command-shared.js +13 -5
- package/dist/lib/platform-command-shared.js.map +1 -1
- package/dist/lib/platform-edition.d.ts +3 -6
- package/dist/lib/platform-edition.d.ts.map +1 -1
- package/dist/lib/platform-edition.js +9 -17
- package/dist/lib/platform-edition.js.map +1 -1
- package/dist/lib/platform-frontdoor-runtime.d.ts.map +1 -1
- package/dist/lib/platform-frontdoor-runtime.js +44 -19
- package/dist/lib/platform-frontdoor-runtime.js.map +1 -1
- package/dist/lib/platform-frontdoor.d.ts +32 -5
- package/dist/lib/platform-frontdoor.d.ts.map +1 -1
- package/dist/lib/platform-frontdoor.js +251 -171
- package/dist/lib/platform-frontdoor.js.map +1 -1
- package/dist/lib/platform-image-admission.d.ts +16 -0
- package/dist/lib/platform-image-admission.d.ts.map +1 -0
- package/dist/lib/platform-image-admission.js +73 -0
- package/dist/lib/platform-image-admission.js.map +1 -0
- package/dist/lib/platform-image-source.d.ts.map +1 -1
- package/dist/lib/platform-image-source.js +2 -0
- package/dist/lib/platform-image-source.js.map +1 -1
- package/dist/lib/platform-lifecycle.d.ts +4 -0
- package/dist/lib/platform-lifecycle.d.ts.map +1 -1
- package/dist/lib/platform-lifecycle.js +42 -8
- package/dist/lib/platform-lifecycle.js.map +1 -1
- package/dist/lib/platform-onboarding.d.ts +10 -0
- package/dist/lib/platform-onboarding.d.ts.map +1 -1
- package/dist/lib/platform-onboarding.js +21 -0
- package/dist/lib/platform-onboarding.js.map +1 -1
- package/dist/lib/platform-preflight.d.ts +6 -3
- package/dist/lib/platform-preflight.d.ts.map +1 -1
- package/dist/lib/platform-preflight.js +52 -19
- package/dist/lib/platform-preflight.js.map +1 -1
- package/dist/lib/platform-release-manifest.d.ts +6 -1
- package/dist/lib/platform-release-manifest.d.ts.map +1 -1
- package/dist/lib/platform-release-manifest.js +10 -0
- package/dist/lib/platform-release-manifest.js.map +1 -1
- package/dist/lib/platform-runtime.d.ts +5 -2
- package/dist/lib/platform-runtime.d.ts.map +1 -1
- package/dist/lib/platform-runtime.js +7 -40
- package/dist/lib/platform-runtime.js.map +1 -1
- package/dist/lib/portable-release-digests.d.ts +5 -0
- package/dist/lib/portable-release-digests.d.ts.map +1 -0
- package/dist/lib/portable-release-digests.js +53 -0
- package/dist/lib/portable-release-digests.js.map +1 -0
- package/dist/lib/release-pin-matrix.d.ts +29 -0
- package/dist/lib/release-pin-matrix.d.ts.map +1 -0
- package/dist/lib/release-pin-matrix.js +67 -0
- package/dist/lib/release-pin-matrix.js.map +1 -0
- package/dist/lib/runtime-backup-archive.d.ts +20 -0
- package/dist/lib/runtime-backup-archive.d.ts.map +1 -0
- package/dist/lib/runtime-backup-archive.js +269 -0
- package/dist/lib/runtime-backup-archive.js.map +1 -0
- package/dist/lib/runtime-command.d.ts +37 -0
- package/dist/lib/runtime-command.d.ts.map +1 -0
- package/dist/lib/runtime-command.js +50 -0
- package/dist/lib/runtime-command.js.map +1 -0
- package/dist/lib/runtime-discovery.d.ts +18 -0
- package/dist/lib/runtime-discovery.d.ts.map +1 -0
- package/dist/lib/runtime-discovery.js +56 -0
- package/dist/lib/runtime-discovery.js.map +1 -0
- package/dist/lib/runtime-lock.d.ts +3 -0
- package/dist/lib/runtime-lock.d.ts.map +1 -0
- package/dist/lib/runtime-lock.js +50 -0
- package/dist/lib/runtime-lock.js.map +1 -0
- package/dist/lib/runtime-operations.d.ts +98 -0
- package/dist/lib/runtime-operations.d.ts.map +1 -0
- package/dist/lib/runtime-operations.js +182 -0
- package/dist/lib/runtime-operations.js.map +1 -0
- package/dist/lib/runtime-provider.d.ts +17 -0
- package/dist/lib/runtime-provider.d.ts.map +1 -0
- package/dist/lib/runtime-provider.js +52 -0
- package/dist/lib/runtime-provider.js.map +1 -0
- package/dist/lib/runtime-status.d.ts +24 -0
- package/dist/lib/runtime-status.d.ts.map +1 -0
- package/dist/lib/runtime-status.js +128 -0
- package/dist/lib/runtime-status.js.map +1 -0
- package/dist/lib/selfhosted-backup.d.ts +37 -0
- package/dist/lib/selfhosted-backup.d.ts.map +1 -0
- package/dist/lib/selfhosted-backup.js +446 -0
- package/dist/lib/selfhosted-backup.js.map +1 -0
- package/dist/lib/selfhosted-keyring.d.ts +19 -0
- package/dist/lib/selfhosted-keyring.d.ts.map +1 -0
- package/dist/lib/selfhosted-keyring.js +193 -0
- package/dist/lib/selfhosted-keyring.js.map +1 -0
- package/dist/lib/selfhosted-lifecycle.d.ts +66 -10
- package/dist/lib/selfhosted-lifecycle.d.ts.map +1 -1
- package/dist/lib/selfhosted-lifecycle.js +344 -39
- package/dist/lib/selfhosted-lifecycle.js.map +1 -1
- package/dist/lib/selfhosted-login.d.ts +34 -0
- package/dist/lib/selfhosted-login.d.ts.map +1 -0
- package/dist/lib/selfhosted-login.js +116 -0
- package/dist/lib/selfhosted-login.js.map +1 -0
- package/dist/lib/selfhosted-readiness.d.ts +3 -0
- package/dist/lib/selfhosted-readiness.d.ts.map +1 -0
- package/dist/lib/selfhosted-readiness.js +24 -0
- package/dist/lib/selfhosted-readiness.js.map +1 -0
- package/dist/lib/setup-capabilities.d.ts +15 -0
- package/dist/lib/setup-capabilities.d.ts.map +1 -0
- package/dist/lib/setup-capabilities.js +21 -0
- package/dist/lib/setup-capabilities.js.map +1 -0
- package/dist/lib/setup-console.d.ts +11 -0
- package/dist/lib/setup-console.d.ts.map +1 -1
- package/dist/lib/setup-console.js +136 -74
- package/dist/lib/setup-console.js.map +1 -1
- package/dist/lib/setup-page.d.ts +12 -0
- package/dist/lib/setup-page.d.ts.map +1 -1
- package/dist/lib/setup-page.js +56 -27
- package/dist/lib/setup-page.js.map +1 -1
- package/dist/lib/setup-plan.d.ts +1 -0
- package/dist/lib/setup-plan.d.ts.map +1 -1
- package/dist/lib/setup-plan.js +5 -1
- package/dist/lib/setup-plan.js.map +1 -1
- package/dist/lib/setup-server.d.ts +11 -2
- package/dist/lib/setup-server.d.ts.map +1 -1
- package/dist/lib/setup-server.js +150 -26
- package/dist/lib/setup-server.js.map +1 -1
- package/dist/lib/ui.d.ts +3 -1
- package/dist/lib/ui.d.ts.map +1 -1
- package/dist/lib/ui.js +39 -7
- package/dist/lib/ui.js.map +1 -1
- package/dist/lib/up-result.d.ts +3 -0
- package/dist/lib/up-result.d.ts.map +1 -1
- package/dist/lib/up-result.js.map +1 -1
- package/dist/menu/run.js +1 -1
- package/dist/menu/run.js.map +1 -1
- package/dist/menu/should-launch.d.ts +0 -14
- package/dist/menu/should-launch.d.ts.map +1 -1
- package/dist/menu/should-launch.js +5 -1
- package/dist/menu/should-launch.js.map +1 -1
- package/oclif.manifest.json +5052 -2825
- package/package.json +12 -9
|
@@ -1,10 +1,12 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { assertLocalEngineContext, readSavedLocalEngine } from './local-engine-context.js';
|
|
2
|
+
import { acquireRuntimeLock } from './runtime-lock.js';
|
|
3
|
+
import { closeSync, copyFileSync, cpSync, createReadStream, createWriteStream, existsSync, fstatSync, openSync, readFileSync, readSync, rmSync, statSync, writeFileSync, } from 'node:fs';
|
|
2
4
|
import { createCipheriv, createDecipheriv, createHash, randomBytes, scryptSync } from 'node:crypto';
|
|
3
5
|
import { dirname, isAbsolute, join, resolve } from 'node:path';
|
|
4
6
|
import { tmpdir } from 'node:os';
|
|
5
7
|
import { fileURLToPath } from 'node:url';
|
|
6
8
|
import { pipeline } from 'node:stream/promises';
|
|
7
|
-
import { ExitCode, KnowzError } from '@knowzai/core';
|
|
9
|
+
import { ExitCode, KnowzError, createPrivateTempDirectory, ensurePrivateDirectory, protectPrivatePath, publishPrivateFile, withPrivateOutput } from '@knowzai/core';
|
|
8
10
|
import { redactPlatformSecrets, parseEnv, writePrivateFile, } from './platform-runtime.js';
|
|
9
11
|
export const BACKUP_SECURITY_WARNING = 'SECURITY WARNING: this backup contains the kzin_ installation credential, all tenant PII, legacy plaintext API-key columns, and every stack secret (.env, JWT, MinIO root password, and SuperAdmin passphrase). Treat it like a production credentials file; use --encrypt and store it securely.';
|
|
10
12
|
export const BACKUP_SCRYPT = { N: 1 << 15, r: 8, p: 1, maxmem: 96 * 1024 * 1024 };
|
|
@@ -32,9 +34,12 @@ function composeInvocation(paths, projectName, engine, action) {
|
|
|
32
34
|
command: front.command,
|
|
33
35
|
args: [
|
|
34
36
|
...front.baseArgs,
|
|
35
|
-
'--project-name',
|
|
36
|
-
|
|
37
|
-
'--
|
|
37
|
+
'--project-name',
|
|
38
|
+
projectName,
|
|
39
|
+
'--file',
|
|
40
|
+
paths.composeFile,
|
|
41
|
+
'--env-file',
|
|
42
|
+
paths.envFile,
|
|
38
43
|
...action,
|
|
39
44
|
],
|
|
40
45
|
options: {
|
|
@@ -69,42 +74,82 @@ export function planBackup(paths, projectName, env, opts) {
|
|
|
69
74
|
const steps = [];
|
|
70
75
|
if (!opts.postgresRunning) {
|
|
71
76
|
steps.push({
|
|
72
|
-
kind: 'compose',
|
|
73
|
-
|
|
77
|
+
kind: 'compose',
|
|
78
|
+
description: 'start postgres',
|
|
79
|
+
invocation: composeInvocation(paths, projectName, engine, [
|
|
80
|
+
'up',
|
|
81
|
+
'--detach',
|
|
82
|
+
'--wait',
|
|
83
|
+
'postgres',
|
|
84
|
+
]),
|
|
74
85
|
});
|
|
75
86
|
}
|
|
76
87
|
for (const database of ['KnowzMaster', 'KnowzKnowledge']) {
|
|
77
88
|
steps.push({
|
|
78
|
-
kind: 'compose',
|
|
89
|
+
kind: 'compose',
|
|
90
|
+
description: `dump ${database}`,
|
|
79
91
|
invocation: composeInvocation(paths, projectName, engine, [
|
|
80
|
-
'exec',
|
|
92
|
+
'exec',
|
|
93
|
+
'-T',
|
|
94
|
+
'postgres',
|
|
95
|
+
'pg_dump',
|
|
96
|
+
'-U',
|
|
97
|
+
pgUser,
|
|
98
|
+
'-Fc',
|
|
99
|
+
'-f',
|
|
100
|
+
`/tmp/${database}.dump`,
|
|
101
|
+
database,
|
|
81
102
|
]),
|
|
82
103
|
});
|
|
83
104
|
steps.push({
|
|
84
|
-
kind: 'compose',
|
|
105
|
+
kind: 'compose',
|
|
106
|
+
description: `copy ${database} dump`,
|
|
85
107
|
invocation: composeInvocation(paths, projectName, engine, [
|
|
86
|
-
'cp',
|
|
108
|
+
'cp',
|
|
109
|
+
`postgres:/tmp/${database}.dump`,
|
|
110
|
+
join(staging, 'pg', `${database}.dump`),
|
|
87
111
|
]),
|
|
88
112
|
});
|
|
89
113
|
steps.push({
|
|
90
|
-
kind: 'compose',
|
|
91
|
-
|
|
114
|
+
kind: 'compose',
|
|
115
|
+
description: `remove staged ${database} dump`,
|
|
116
|
+
invocation: composeInvocation(paths, projectName, engine, [
|
|
117
|
+
'exec',
|
|
118
|
+
'-T',
|
|
119
|
+
'postgres',
|
|
120
|
+
'rm',
|
|
121
|
+
'-f',
|
|
122
|
+
`/tmp/${database}.dump`,
|
|
123
|
+
]),
|
|
92
124
|
});
|
|
93
125
|
}
|
|
94
126
|
for (const volume of ['miniodata', 'objectsdata', 'identity']) {
|
|
95
127
|
const physical = `${projectName}_knowz-portable-${volume}`;
|
|
96
128
|
steps.push({
|
|
97
|
-
kind: 'engine-run',
|
|
129
|
+
kind: 'engine-run',
|
|
130
|
+
description: `snapshot ${volume}`,
|
|
98
131
|
invocation: engineInvocation(engine, paths.runtimeDir, [
|
|
99
|
-
'run',
|
|
100
|
-
'
|
|
132
|
+
'run',
|
|
133
|
+
'--rm',
|
|
134
|
+
'-v',
|
|
135
|
+
`${physical}:/src:ro`,
|
|
136
|
+
'-v',
|
|
137
|
+
`${staging}:/out`,
|
|
138
|
+
'alpine',
|
|
139
|
+
'tar',
|
|
140
|
+
'-czf',
|
|
141
|
+
`/out/volumes/${volume}.tar.gz`,
|
|
142
|
+
'-C',
|
|
143
|
+
'/src',
|
|
144
|
+
'.',
|
|
101
145
|
]),
|
|
102
146
|
});
|
|
103
147
|
}
|
|
104
148
|
steps.push({ kind: 'fs', description: 'write manifest' });
|
|
105
149
|
if (!opts.postgresRunning) {
|
|
106
150
|
steps.push({
|
|
107
|
-
kind: 'compose',
|
|
151
|
+
kind: 'compose',
|
|
152
|
+
description: 'stop postgres',
|
|
108
153
|
invocation: composeInvocation(paths, projectName, engine, ['stop', 'postgres']),
|
|
109
154
|
});
|
|
110
155
|
}
|
|
@@ -118,41 +163,105 @@ export function planRestore(paths, projectName, manifest, opts) {
|
|
|
118
163
|
steps.push({ kind: 'fs', description: 'restore runtime configuration and portable assets' });
|
|
119
164
|
for (const volume of manifest.volumes) {
|
|
120
165
|
const physical = `${projectName}_knowz-portable-${volume}`;
|
|
121
|
-
steps.push({
|
|
122
|
-
|
|
166
|
+
steps.push({
|
|
167
|
+
kind: 'engine-run',
|
|
168
|
+
description: `wipe ${physical}`,
|
|
169
|
+
invocation: engineInvocation(engine, paths.runtimeDir, ['volume', 'rm', '--force', physical]),
|
|
170
|
+
});
|
|
171
|
+
steps.push({
|
|
172
|
+
kind: 'engine-run',
|
|
173
|
+
description: `create ${physical}`,
|
|
174
|
+
invocation: engineInvocation(engine, paths.runtimeDir, ['volume', 'create', physical]),
|
|
175
|
+
});
|
|
123
176
|
if (volume !== 'identity' || opts.mode === 'replace-installation') {
|
|
124
177
|
steps.push({
|
|
125
|
-
kind: 'engine-run',
|
|
178
|
+
kind: 'engine-run',
|
|
179
|
+
description: `extract volumes/${volume}.tar.gz`,
|
|
126
180
|
invocation: engineInvocation(engine, paths.runtimeDir, [
|
|
127
|
-
'run',
|
|
128
|
-
'
|
|
181
|
+
'run',
|
|
182
|
+
'--rm',
|
|
183
|
+
'-v',
|
|
184
|
+
`${physical}:/dst`,
|
|
185
|
+
'-v',
|
|
186
|
+
`${staging}:/in:ro`,
|
|
187
|
+
'alpine',
|
|
188
|
+
'tar',
|
|
189
|
+
'-xzf',
|
|
190
|
+
`/in/volumes/${volume}.tar.gz`,
|
|
191
|
+
'-C',
|
|
192
|
+
'/dst',
|
|
129
193
|
]),
|
|
130
194
|
});
|
|
131
195
|
}
|
|
132
196
|
}
|
|
133
197
|
const pgVolume = `${projectName}_knowz-portable-pgdata`;
|
|
134
|
-
steps.push({
|
|
135
|
-
|
|
136
|
-
|
|
198
|
+
steps.push({
|
|
199
|
+
kind: 'engine-run',
|
|
200
|
+
description: `wipe ${pgVolume}`,
|
|
201
|
+
invocation: engineInvocation(engine, paths.runtimeDir, ['volume', 'rm', '--force', pgVolume]),
|
|
202
|
+
});
|
|
203
|
+
steps.push({
|
|
204
|
+
kind: 'engine-run',
|
|
205
|
+
description: `create ${pgVolume}`,
|
|
206
|
+
invocation: engineInvocation(engine, paths.runtimeDir, ['volume', 'create', pgVolume]),
|
|
207
|
+
});
|
|
208
|
+
steps.push({
|
|
209
|
+
kind: 'compose',
|
|
210
|
+
description: 'start postgres',
|
|
211
|
+
invocation: composeInvocation(paths, projectName, engine, [
|
|
212
|
+
'up',
|
|
213
|
+
'--detach',
|
|
214
|
+
'--wait',
|
|
215
|
+
'postgres',
|
|
216
|
+
]),
|
|
217
|
+
});
|
|
137
218
|
const pgUser = opts.pgUser || 'knowz';
|
|
138
219
|
for (const database of manifest.pgDatabases) {
|
|
139
220
|
steps.push({
|
|
140
|
-
kind: 'compose',
|
|
141
|
-
|
|
221
|
+
kind: 'compose',
|
|
222
|
+
description: `copy ${database} dump`,
|
|
223
|
+
invocation: composeInvocation(paths, projectName, engine, [
|
|
224
|
+
'cp',
|
|
225
|
+
join(staging, 'pg', `${database}.dump`),
|
|
226
|
+
`postgres:/tmp/${database}.dump`,
|
|
227
|
+
]),
|
|
142
228
|
});
|
|
143
229
|
steps.push({
|
|
144
|
-
kind: 'compose',
|
|
230
|
+
kind: 'compose',
|
|
231
|
+
description: `restore ${database}`,
|
|
145
232
|
invocation: composeInvocation(paths, projectName, engine, [
|
|
146
|
-
'exec',
|
|
147
|
-
'-
|
|
233
|
+
'exec',
|
|
234
|
+
'-T',
|
|
235
|
+
'postgres',
|
|
236
|
+
'pg_restore',
|
|
237
|
+
'--clean',
|
|
238
|
+
'--if-exists',
|
|
239
|
+
'--no-owner',
|
|
240
|
+
'-U',
|
|
241
|
+
pgUser,
|
|
242
|
+
'-d',
|
|
243
|
+
database,
|
|
244
|
+
`/tmp/${database}.dump`,
|
|
148
245
|
]),
|
|
149
246
|
});
|
|
150
247
|
steps.push({
|
|
151
|
-
kind: 'compose',
|
|
152
|
-
|
|
248
|
+
kind: 'compose',
|
|
249
|
+
description: `remove staged ${database} dump`,
|
|
250
|
+
invocation: composeInvocation(paths, projectName, engine, [
|
|
251
|
+
'exec',
|
|
252
|
+
'-T',
|
|
253
|
+
'postgres',
|
|
254
|
+
'rm',
|
|
255
|
+
'-f',
|
|
256
|
+
`/tmp/${database}.dump`,
|
|
257
|
+
]),
|
|
153
258
|
});
|
|
154
259
|
}
|
|
155
|
-
steps.push({
|
|
260
|
+
steps.push({
|
|
261
|
+
kind: 'compose',
|
|
262
|
+
description: 'stop postgres',
|
|
263
|
+
invocation: composeInvocation(paths, projectName, engine, ['stop', 'postgres']),
|
|
264
|
+
});
|
|
156
265
|
return steps;
|
|
157
266
|
}
|
|
158
267
|
function fileSha256(path) {
|
|
@@ -206,13 +315,16 @@ export function parseAndValidateManifest(json) {
|
|
|
206
315
|
if (!isRecord(value) || value.schemaVersion !== 1) {
|
|
207
316
|
throw new KnowzError('Unsupported backup manifest schemaVersion; expected 1.', ExitCode.Usage);
|
|
208
317
|
}
|
|
209
|
-
if (value.includesIdentity !== true ||
|
|
318
|
+
if (value.includesIdentity !== true ||
|
|
319
|
+
!Array.isArray(value.pgDatabases) ||
|
|
320
|
+
!Array.isArray(value.volumes) ||
|
|
321
|
+
!isRecord(value.checksums)) {
|
|
210
322
|
throw new KnowzError('Backup manifest is incomplete or invalid.', ExitCode.Usage);
|
|
211
323
|
}
|
|
212
324
|
const databases = value.pgDatabases.map(String);
|
|
213
325
|
const volumes = value.volumes.map(String);
|
|
214
|
-
if (!['KnowzMaster', 'KnowzKnowledge'].every(database => databases.includes(database)) ||
|
|
215
|
-
!['miniodata', 'objectsdata', 'identity'].every(volume => volumes.includes(volume))) {
|
|
326
|
+
if (!['KnowzMaster', 'KnowzKnowledge'].every((database) => databases.includes(database)) ||
|
|
327
|
+
!['miniodata', 'objectsdata', 'identity'].every((volume) => volumes.includes(volume))) {
|
|
216
328
|
throw new KnowzError('Backup manifest does not contain every required portable data store.', ExitCode.Usage);
|
|
217
329
|
}
|
|
218
330
|
const checksums = Object.fromEntries(Object.entries(value.checksums).filter((entry) => typeof entry[1] === 'string'));
|
|
@@ -233,28 +345,18 @@ export function parseAndValidateManifest(json) {
|
|
|
233
345
|
export function validateBackupChecksums(stagingDir, manifest) {
|
|
234
346
|
for (const artifact of BACKUP_ARTIFACTS) {
|
|
235
347
|
const expected = manifest.checksums[artifact];
|
|
236
|
-
if (!expected ||
|
|
348
|
+
if (!expected ||
|
|
349
|
+
!existsSync(join(stagingDir, artifact)) ||
|
|
350
|
+
fileSha256(join(stagingDir, artifact)) !== expected) {
|
|
237
351
|
throw new KnowzError(`Backup checksum validation failed for ${artifact}.`, ExitCode.Usage);
|
|
238
352
|
}
|
|
239
353
|
}
|
|
240
354
|
}
|
|
241
355
|
export function createPrivateStaging(parent) {
|
|
242
|
-
|
|
243
|
-
chmodSync(staging, 0o700);
|
|
244
|
-
return staging;
|
|
356
|
+
return createPrivateTempDirectory(parent, '.backup-');
|
|
245
357
|
}
|
|
246
358
|
export function finalizePrivateArchive(source, destination) {
|
|
247
|
-
|
|
248
|
-
try {
|
|
249
|
-
renameSync(source, destination);
|
|
250
|
-
}
|
|
251
|
-
catch (error) {
|
|
252
|
-
if (error.code !== 'EXDEV')
|
|
253
|
-
throw error;
|
|
254
|
-
copyFileSync(source, destination);
|
|
255
|
-
rmSync(source, { force: true });
|
|
256
|
-
}
|
|
257
|
-
chmodSync(destination, 0o600);
|
|
359
|
+
publishPrivateFile(source, destination);
|
|
258
360
|
}
|
|
259
361
|
function encryptionHeader(path) {
|
|
260
362
|
const fd = openSync(path, 'r');
|
|
@@ -272,13 +374,22 @@ function encryptionHeader(path) {
|
|
|
272
374
|
catch {
|
|
273
375
|
throw new KnowzError('Encrypted backup header is invalid.', ExitCode.Usage);
|
|
274
376
|
}
|
|
275
|
-
if (!isRecord(parsed) ||
|
|
276
|
-
|
|
377
|
+
if (!isRecord(parsed) ||
|
|
378
|
+
parsed.v !== 1 ||
|
|
379
|
+
parsed.alg !== 'aes-256-gcm' ||
|
|
380
|
+
parsed.kdf !== 'scrypt' ||
|
|
381
|
+
typeof parsed.salt !== 'string' ||
|
|
382
|
+
typeof parsed.nonce !== 'string') {
|
|
277
383
|
throw new KnowzError('Encrypted backup header is unsupported.', ExitCode.Usage);
|
|
278
384
|
}
|
|
279
385
|
const tag = Buffer.alloc(16);
|
|
280
386
|
readSync(fd, tag, 0, 16, size - 16);
|
|
281
|
-
return {
|
|
387
|
+
return {
|
|
388
|
+
header: parsed,
|
|
389
|
+
ciphertextOffset: newline + 1,
|
|
390
|
+
size,
|
|
391
|
+
tag,
|
|
392
|
+
};
|
|
282
393
|
}
|
|
283
394
|
finally {
|
|
284
395
|
closeSync(fd);
|
|
@@ -289,19 +400,23 @@ export async function encryptStream(input, output, passphrase) {
|
|
|
289
400
|
const nonce = randomBytes(12);
|
|
290
401
|
const key = scryptSync(passphrase, salt, 32, BACKUP_SCRYPT);
|
|
291
402
|
const header = {
|
|
292
|
-
v: 1,
|
|
403
|
+
v: 1,
|
|
404
|
+
alg: 'aes-256-gcm',
|
|
405
|
+
kdf: 'scrypt',
|
|
406
|
+
salt: salt.toString('base64'),
|
|
407
|
+
nonce: nonce.toString('base64'),
|
|
293
408
|
};
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
}
|
|
409
|
+
await withPrivateOutput(output, async (temporary) => {
|
|
410
|
+
writeFileSync(temporary, `${JSON.stringify(header)}\n`);
|
|
411
|
+
const cipher = createCipheriv('aes-256-gcm', key, nonce);
|
|
412
|
+
try {
|
|
413
|
+
await pipeline(createReadStream(input), cipher, createWriteStream(temporary, { flags: 'a' }));
|
|
414
|
+
writeFileSync(temporary, cipher.getAuthTag(), { flag: 'a' });
|
|
415
|
+
}
|
|
416
|
+
catch (cause) {
|
|
417
|
+
throw new KnowzError('Could not encrypt the backup archive.', ExitCode.Generic, { cause });
|
|
418
|
+
}
|
|
419
|
+
});
|
|
305
420
|
}
|
|
306
421
|
export async function decryptStream(input, output, passphrase) {
|
|
307
422
|
const envelope = encryptionHeader(input);
|
|
@@ -310,14 +425,14 @@ export async function decryptStream(input, output, passphrase) {
|
|
|
310
425
|
const key = scryptSync(passphrase, salt, 32, BACKUP_SCRYPT);
|
|
311
426
|
const decipher = createDecipheriv('aes-256-gcm', key, nonce);
|
|
312
427
|
decipher.setAuthTag(envelope.tag);
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
}
|
|
428
|
+
await withPrivateOutput(output, async (temporary) => {
|
|
429
|
+
try {
|
|
430
|
+
await pipeline(createReadStream(input, { start: envelope.ciphertextOffset, end: envelope.size - 17 }), decipher, createWriteStream(temporary));
|
|
431
|
+
}
|
|
432
|
+
catch (cause) {
|
|
433
|
+
throw new KnowzError('Wrong passphrase or tampered archive.', ExitCode.Auth, { cause });
|
|
434
|
+
}
|
|
435
|
+
});
|
|
321
436
|
}
|
|
322
437
|
export async function runRestorePreflight(paths, projectName, opts) {
|
|
323
438
|
if (!opts.yes && !opts.isTty) {
|
|
@@ -327,7 +442,11 @@ export async function runRestorePreflight(paths, projectName, opts) {
|
|
|
327
442
|
const invocation = existsSync(paths.composeFile) && existsSync(paths.envFile)
|
|
328
443
|
? composeInvocation(paths, projectName, engine, ['ps', '--services', '--status', 'running'])
|
|
329
444
|
: engineInvocation(engine, process.cwd(), [
|
|
330
|
-
'ps',
|
|
445
|
+
'ps',
|
|
446
|
+
'--filter',
|
|
447
|
+
`label=com.docker.compose.project=${projectName}`,
|
|
448
|
+
'--format',
|
|
449
|
+
'{{.Names}}',
|
|
331
450
|
]);
|
|
332
451
|
const result = await Promise.resolve(opts.runner(invocation.command, invocation.args, invocation.options));
|
|
333
452
|
if (result.status !== 0) {
|
|
@@ -370,7 +489,8 @@ function ensureBackupRuntime(paths) {
|
|
|
370
489
|
const env = parseEnv(readFileSync(paths.envFile, 'utf8'));
|
|
371
490
|
const pgHost = env.PG_HOST?.trim();
|
|
372
491
|
const s3Endpoint = env.S3_ENDPOINT?.trim();
|
|
373
|
-
if ((pgHost && pgHost !== 'postgres') ||
|
|
492
|
+
if ((pgHost && pgHost !== 'postgres') ||
|
|
493
|
+
(s3Endpoint && !/^https?:\/\/minio(?::|\/|$)/i.test(s3Endpoint))) {
|
|
374
494
|
throw new KnowzError('Portable backup v1 does not support external database or object-storage backends.', ExitCode.Usage);
|
|
375
495
|
}
|
|
376
496
|
return env;
|
|
@@ -386,7 +506,10 @@ function resolveArchiveDestination(out, projectName, encrypted, now) {
|
|
|
386
506
|
destination = resolve(process.cwd(), filename);
|
|
387
507
|
else {
|
|
388
508
|
const candidate = resolve(out);
|
|
389
|
-
destination =
|
|
509
|
+
destination =
|
|
510
|
+
existsSync(candidate) && statSync(candidate).isDirectory()
|
|
511
|
+
? join(candidate, filename)
|
|
512
|
+
: candidate;
|
|
390
513
|
if (encrypted && !destination.endsWith('.enc'))
|
|
391
514
|
destination += '.enc';
|
|
392
515
|
}
|
|
@@ -398,52 +521,81 @@ function resolveArchiveDestination(out, projectName, encrypted, now) {
|
|
|
398
521
|
}
|
|
399
522
|
export async function runPortableBackup(input) {
|
|
400
523
|
const env = ensureBackupRuntime(input.paths);
|
|
401
|
-
const engine = requireEngine(input.engine)
|
|
524
|
+
const engine = await assertLocalEngineContext(requireEngine(input.engine), {
|
|
525
|
+
runner: input.runner,
|
|
526
|
+
savedEngine: readSavedLocalEngine(input.paths.stateFile),
|
|
527
|
+
});
|
|
402
528
|
if (input.encrypt && !input.passphrase)
|
|
403
529
|
throw new KnowzError('A backup passphrase is required for --encrypt.', ExitCode.Auth);
|
|
404
530
|
const now = input.now?.() ?? new Date();
|
|
405
531
|
const destination = resolveArchiveDestination(input.out, input.projectName, input.encrypt, now);
|
|
406
532
|
const secrets = knownSecretValues(env);
|
|
407
|
-
const
|
|
408
|
-
const plainArchive = join(input.paths.runtimeDir, `.backup-${process.pid}-${Date.now()}.tar.gz`);
|
|
409
|
-
let startedPostgres = false;
|
|
410
|
-
let stopInvocation;
|
|
533
|
+
const unlock = acquireRuntimeLock(input.paths.runtimeDir);
|
|
411
534
|
try {
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
await
|
|
430
|
-
|
|
535
|
+
const staging = createPrivateStaging(input.paths.runtimeDir);
|
|
536
|
+
const plainArchive = join(input.paths.runtimeDir, `.backup-${process.pid}-${Date.now()}.tar.gz`);
|
|
537
|
+
let startedPostgres = false;
|
|
538
|
+
let stopInvocation;
|
|
539
|
+
try {
|
|
540
|
+
ensurePrivateDirectory(join(staging, 'pg'));
|
|
541
|
+
ensurePrivateDirectory(join(staging, 'volumes'));
|
|
542
|
+
ensurePrivateDirectory(join(staging, 'runtime'));
|
|
543
|
+
writePrivateFile(join(staging, 'runtime/.env'), readFileSync(input.paths.envFile, 'utf8'));
|
|
544
|
+
writePrivateFile(join(staging, 'runtime/state.json'), readFileSync(input.paths.stateFile, 'utf8'));
|
|
545
|
+
const ps = composeInvocation(input.paths, input.projectName, engine, [
|
|
546
|
+
'ps',
|
|
547
|
+
'--services',
|
|
548
|
+
'--status',
|
|
549
|
+
'running',
|
|
550
|
+
'postgres',
|
|
551
|
+
]);
|
|
552
|
+
const status = await runInvocation(ps, 'inspect postgres', input.runner, secrets);
|
|
553
|
+
const postgresRunning = (status.stdout ?? '')
|
|
554
|
+
.split(/\r?\n/)
|
|
555
|
+
.some((service) => service.trim() === 'postgres');
|
|
556
|
+
const plan = planBackup(input.paths, input.projectName, env, {
|
|
557
|
+
postgresRunning,
|
|
558
|
+
engine,
|
|
559
|
+
stagingDir: staging,
|
|
560
|
+
});
|
|
561
|
+
stopInvocation = plan.find((step) => step.description === 'stop postgres')?.invocation;
|
|
562
|
+
startedPostgres = !postgresRunning;
|
|
563
|
+
await executeBackupSteps(plan.filter((step) => step.description !== 'stop postgres'), input.runner, secrets);
|
|
564
|
+
const manifest = buildManifest(input.projectName, env, input.cliVersion, input.encrypt, staging);
|
|
565
|
+
writeFileSync(join(staging, 'manifest.json'), `${JSON.stringify(manifest, null, 2)}\n`, {
|
|
566
|
+
mode: 0o600,
|
|
567
|
+
});
|
|
568
|
+
await runInvocation({
|
|
569
|
+
command: 'tar',
|
|
570
|
+
args: ['-czf', plainArchive, '-C', staging, '.'],
|
|
571
|
+
options: { cwd: input.paths.runtimeDir, shell: false },
|
|
572
|
+
}, 'create backup archive', input.runner, secrets);
|
|
573
|
+
protectPrivatePath(plainArchive);
|
|
574
|
+
if (input.encrypt) {
|
|
575
|
+
await encryptStream(plainArchive, destination, input.passphrase);
|
|
576
|
+
rmSync(plainArchive, { force: true });
|
|
577
|
+
}
|
|
578
|
+
else {
|
|
579
|
+
finalizePrivateArchive(plainArchive, destination);
|
|
580
|
+
}
|
|
581
|
+
input.emit?.(BACKUP_SECURITY_WARNING);
|
|
582
|
+
return { archive: destination, encrypted: input.encrypt, warning: BACKUP_SECURITY_WARNING };
|
|
431
583
|
}
|
|
432
|
-
|
|
433
|
-
|
|
584
|
+
finally {
|
|
585
|
+
if (startedPostgres && stopInvocation) {
|
|
586
|
+
try {
|
|
587
|
+
await runInvocation(stopInvocation, 'stop postgres', input.runner, secrets);
|
|
588
|
+
}
|
|
589
|
+
catch {
|
|
590
|
+
/* Preserve the primary result/error. */
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
rmSync(staging, { recursive: true, force: true });
|
|
594
|
+
rmSync(plainArchive, { force: true });
|
|
434
595
|
}
|
|
435
|
-
input.emit?.(BACKUP_SECURITY_WARNING);
|
|
436
|
-
return { archive: destination, encrypted: input.encrypt, warning: BACKUP_SECURITY_WARNING };
|
|
437
596
|
}
|
|
438
597
|
finally {
|
|
439
|
-
|
|
440
|
-
try {
|
|
441
|
-
await runInvocation(stopInvocation, 'stop postgres', input.runner, secrets);
|
|
442
|
-
}
|
|
443
|
-
catch { /* Preserve the primary result/error. */ }
|
|
444
|
-
}
|
|
445
|
-
rmSync(staging, { recursive: true, force: true });
|
|
446
|
-
rmSync(plainArchive, { force: true });
|
|
598
|
+
unlock();
|
|
447
599
|
}
|
|
448
600
|
}
|
|
449
601
|
function validateTarListing(listing) {
|
|
@@ -479,12 +631,31 @@ function installPortableAssets(paths) {
|
|
|
479
631
|
if (![compose, contract, postgres].every(existsSync)) {
|
|
480
632
|
throw new KnowzError('Packaged portable runtime assets are incomplete.', ExitCode.Generic);
|
|
481
633
|
}
|
|
482
|
-
|
|
634
|
+
ensurePrivateDirectory(paths.runtimeDir);
|
|
483
635
|
copyFileSync(compose, paths.composeFile);
|
|
484
636
|
copyFileSync(contract, join(paths.runtimeDir, 'runtime-contract.json'));
|
|
485
637
|
rmSync(paths.postgresInitDir, { recursive: true, force: true });
|
|
486
638
|
cpSync(postgres, paths.postgresInitDir, { recursive: true });
|
|
487
639
|
}
|
|
640
|
+
export function rewriteRestoredRuntimeState(json, name, engine) {
|
|
641
|
+
let state;
|
|
642
|
+
try {
|
|
643
|
+
state = JSON.parse(json);
|
|
644
|
+
if (!state || Array.isArray(state) || typeof state !== 'object')
|
|
645
|
+
throw new Error();
|
|
646
|
+
}
|
|
647
|
+
catch {
|
|
648
|
+
throw new KnowzError('Archive runtime state is invalid JSON.', ExitCode.Usage);
|
|
649
|
+
}
|
|
650
|
+
return (JSON.stringify({
|
|
651
|
+
...state,
|
|
652
|
+
name,
|
|
653
|
+
engine,
|
|
654
|
+
healthPassed: false,
|
|
655
|
+
restoredAt: new Date().toISOString(),
|
|
656
|
+
updatedAt: new Date().toISOString(),
|
|
657
|
+
}, null, 2) + '\n');
|
|
658
|
+
}
|
|
488
659
|
export async function runPortableRestore(input) {
|
|
489
660
|
if (!input.yes && !input.isTty) {
|
|
490
661
|
throw new KnowzError('Refusing to restore without --yes (no TTY).', ExitCode.Usage);
|
|
@@ -494,20 +665,34 @@ export async function runPortableRestore(input) {
|
|
|
494
665
|
throw new KnowzError(`Backup archive not found: ${archive}`, ExitCode.Usage);
|
|
495
666
|
if (archive.endsWith('.enc') && !input.passphrase)
|
|
496
667
|
throw new KnowzError('A passphrase is required to restore this encrypted backup.', ExitCode.Auth);
|
|
497
|
-
const engine = requireEngine(input.engine)
|
|
498
|
-
|
|
499
|
-
|
|
668
|
+
const engine = await assertLocalEngineContext(requireEngine(input.engine), {
|
|
669
|
+
runner: input.runner,
|
|
670
|
+
savedEngine: readSavedLocalEngine(input.paths.stateFile),
|
|
671
|
+
});
|
|
672
|
+
const stagingParent = createPrivateTempDirectory(tmpdir(), 'knowz-restore-');
|
|
500
673
|
const extracted = join(stagingParent, 'extracted');
|
|
501
|
-
|
|
674
|
+
ensurePrivateDirectory(extracted);
|
|
502
675
|
const plainArchive = archive.endsWith('.enc') ? join(stagingParent, 'backup.tar.gz') : archive;
|
|
503
676
|
try {
|
|
504
677
|
if (archive.endsWith('.enc'))
|
|
505
678
|
await decryptStream(archive, plainArchive, input.passphrase);
|
|
506
|
-
const list = await runInvocation({
|
|
679
|
+
const list = await runInvocation({
|
|
680
|
+
command: 'tar',
|
|
681
|
+
args: ['-tzf', plainArchive],
|
|
682
|
+
options: { cwd: stagingParent, shell: false },
|
|
683
|
+
}, 'inspect backup archive', input.runner, []);
|
|
507
684
|
validateTarListing(list.stdout ?? '');
|
|
508
|
-
const verboseList = await runInvocation({
|
|
685
|
+
const verboseList = await runInvocation({
|
|
686
|
+
command: 'tar',
|
|
687
|
+
args: ['-tvzf', plainArchive],
|
|
688
|
+
options: { cwd: stagingParent, shell: false },
|
|
689
|
+
}, 'inspect backup archive entry types', input.runner, []);
|
|
509
690
|
validateTarEntryTypes(verboseList.stdout ?? '');
|
|
510
|
-
await runInvocation({
|
|
691
|
+
await runInvocation({
|
|
692
|
+
command: 'tar',
|
|
693
|
+
args: ['-xzf', plainArchive, '--no-same-owner', '--no-same-permissions', '-C', extracted],
|
|
694
|
+
options: { cwd: stagingParent, shell: false },
|
|
695
|
+
}, 'extract backup archive', input.runner, []);
|
|
511
696
|
const manifestPath = join(extracted, 'manifest.json');
|
|
512
697
|
if (!existsSync(manifestPath))
|
|
513
698
|
throw new KnowzError('Backup archive has no manifest.json.', ExitCode.Usage);
|
|
@@ -522,7 +707,10 @@ export async function runPortableRestore(input) {
|
|
|
522
707
|
input.emit?.(identityWarning);
|
|
523
708
|
input.emit?.('Data Protection keys are deliberately excluded from portable backups. Re-enter federation, Twilio, phone-pool, and tenant-resource credentials after restore.');
|
|
524
709
|
await runRestorePreflight(input.paths, input.projectName, {
|
|
525
|
-
yes: input.yes,
|
|
710
|
+
yes: input.yes,
|
|
711
|
+
isTty: input.isTty,
|
|
712
|
+
engine,
|
|
713
|
+
runner: input.runner,
|
|
526
714
|
});
|
|
527
715
|
if (!input.yes) {
|
|
528
716
|
const confirmed = await input.confirmName?.();
|
|
@@ -530,47 +718,77 @@ export async function runPortableRestore(input) {
|
|
|
530
718
|
throw new KnowzError('Restore confirmation did not match the platform name.', ExitCode.Usage);
|
|
531
719
|
}
|
|
532
720
|
const runtimeExisted = existsSync(input.paths.runtimeDir);
|
|
533
|
-
|
|
534
|
-
installPortableAssets(input.paths);
|
|
535
|
-
else if (!existsSync(input.paths.composeFile))
|
|
536
|
-
installPortableAssets(input.paths);
|
|
537
|
-
if (!existsSync(input.paths.envFile))
|
|
538
|
-
writePrivateFile(input.paths.envFile, readFileSync(join(extracted, 'runtime/.env'), 'utf8'));
|
|
539
|
-
else
|
|
540
|
-
input.emit?.('Existing runtime .env preserved; the archive copy was skipped.');
|
|
541
|
-
if (!existsSync(input.paths.stateFile))
|
|
542
|
-
writePrivateFile(input.paths.stateFile, readFileSync(join(extracted, 'runtime/state.json'), 'utf8'));
|
|
543
|
-
const env = parseEnv(readFileSync(input.paths.envFile, 'utf8'));
|
|
544
|
-
const secrets = knownSecretValues(env);
|
|
545
|
-
const plan = planRestore(input.paths, input.projectName, manifest, {
|
|
546
|
-
mode: input.mode, runtimeDirExists: runtimeExisted, engine, stagingDir: extracted, pgUser: env.PG_USER || 'knowz',
|
|
547
|
-
});
|
|
548
|
-
const stop = plan.find(step => step.description === 'stop postgres')?.invocation;
|
|
549
|
-
let postgresStarted = false;
|
|
721
|
+
const unlock = acquireRuntimeLock(input.paths.runtimeDir);
|
|
550
722
|
try {
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
723
|
+
await runRestorePreflight(input.paths, input.projectName, {
|
|
724
|
+
yes: input.yes,
|
|
725
|
+
isTty: input.isTty,
|
|
726
|
+
engine,
|
|
727
|
+
runner: input.runner,
|
|
728
|
+
});
|
|
729
|
+
if (!runtimeExisted && (existsSync(input.paths.stateFile) || existsSync(input.paths.envFile)))
|
|
730
|
+
throw new KnowzError('Restore target changed during validation.', ExitCode.Usage);
|
|
731
|
+
if (!runtimeExisted)
|
|
732
|
+
installPortableAssets(input.paths);
|
|
733
|
+
else if (!existsSync(input.paths.composeFile))
|
|
734
|
+
installPortableAssets(input.paths);
|
|
735
|
+
if (!existsSync(input.paths.envFile))
|
|
736
|
+
writePrivateFile(input.paths.envFile, readFileSync(join(extracted, 'runtime/.env'), 'utf8'));
|
|
737
|
+
else
|
|
738
|
+
input.emit?.('Existing runtime .env preserved; the archive copy was skipped.');
|
|
739
|
+
if (!existsSync(input.paths.stateFile))
|
|
740
|
+
writePrivateFile(input.paths.stateFile, rewriteRestoredRuntimeState(readFileSync(join(extracted, 'runtime/state.json'), 'utf8'), input.projectName, engine));
|
|
741
|
+
const env = parseEnv(readFileSync(input.paths.envFile, 'utf8'));
|
|
742
|
+
const secrets = knownSecretValues(env);
|
|
743
|
+
const plan = planRestore(input.paths, input.projectName, manifest, {
|
|
744
|
+
mode: input.mode,
|
|
745
|
+
runtimeDirExists: runtimeExisted,
|
|
746
|
+
engine,
|
|
747
|
+
stagingDir: extracted,
|
|
748
|
+
pgUser: env.PG_USER || 'knowz',
|
|
749
|
+
});
|
|
750
|
+
const stop = plan.find((step) => step.description === 'stop postgres')?.invocation;
|
|
751
|
+
let postgresStarted = false;
|
|
752
|
+
try {
|
|
753
|
+
for (const step of plan) {
|
|
754
|
+
if (!step.invocation || step.description === 'stop postgres')
|
|
755
|
+
continue;
|
|
756
|
+
await runInvocation(step.invocation, step.description, input.runner, secrets);
|
|
757
|
+
if (step.description === 'start postgres')
|
|
758
|
+
postgresStarted = true;
|
|
759
|
+
}
|
|
557
760
|
}
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
761
|
+
finally {
|
|
762
|
+
if (postgresStarted && stop) {
|
|
763
|
+
try {
|
|
764
|
+
await runInvocation(stop, 'stop postgres', input.runner, secrets);
|
|
765
|
+
}
|
|
766
|
+
catch {
|
|
767
|
+
/* Preserve primary failure. */
|
|
768
|
+
}
|
|
563
769
|
}
|
|
564
|
-
catch { /* Preserve primary failure. */ }
|
|
565
770
|
}
|
|
771
|
+
if (input.mode === 'clone') {
|
|
772
|
+
const warning = 'Clone restore created an empty identity volume: this is a new seat. Run `knowz activate`, then `knowz up`.';
|
|
773
|
+
input.emit?.(warning);
|
|
774
|
+
return {
|
|
775
|
+
platformName: input.projectName,
|
|
776
|
+
mode: input.mode,
|
|
777
|
+
next: 'knowz activate && knowz up',
|
|
778
|
+
warning,
|
|
779
|
+
};
|
|
780
|
+
}
|
|
781
|
+
input.emit?.('Restore complete. Start the stack with `knowz up`.');
|
|
782
|
+
return {
|
|
783
|
+
platformName: input.projectName,
|
|
784
|
+
mode: input.mode,
|
|
785
|
+
next: 'knowz up',
|
|
786
|
+
warning: identityWarning,
|
|
787
|
+
};
|
|
566
788
|
}
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
input.emit?.(warning);
|
|
570
|
-
return { platformName: input.projectName, mode: input.mode, next: 'knowz activate && knowz up', warning };
|
|
789
|
+
finally {
|
|
790
|
+
unlock();
|
|
571
791
|
}
|
|
572
|
-
input.emit?.('Restore complete. Start the stack with `knowz up`.');
|
|
573
|
-
return { platformName: input.projectName, mode: input.mode, next: 'knowz up', warning: identityWarning };
|
|
574
792
|
}
|
|
575
793
|
finally {
|
|
576
794
|
rmSync(stagingParent, { recursive: true, force: true });
|