@or3/cloud 0.1.54 → 0.1.55
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/assets/dashboard-operator.mjs +53 -38
- package/dist/cli.mjs +54 -2
- package/package.json +4 -4
|
@@ -28,6 +28,7 @@ const maxRegistryBytes = 256 * 1024;
|
|
|
28
28
|
const maxAttestationBytes = 1024 * 1024;
|
|
29
29
|
const maxProcessOutputBytes = 64 * 1024;
|
|
30
30
|
const maxUpdateDurationMs = 15 * 60 * 1000;
|
|
31
|
+
const operatorHandoffTimeoutMs = 2 * 60 * 1000;
|
|
31
32
|
let updateClaimed = false;
|
|
32
33
|
const checkAttempts = [];
|
|
33
34
|
const startAttempts = [];
|
|
@@ -607,35 +608,49 @@ function closeAfter(server, code) {
|
|
|
607
608
|
setTimeout(() => server.close(() => process.exit(code)), 250).unref();
|
|
608
609
|
}
|
|
609
610
|
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
* only this sidecar from the newly written digest-qualified overlay. The
|
|
614
|
-
* detached Docker client survives the current container's termination.
|
|
615
|
-
*/
|
|
616
|
-
function recreateOperatorAfterCommit(environment) {
|
|
617
|
-
const project = envValue(environment, 'OR3_COMPOSE_PROJECT');
|
|
618
|
-
if (!project) return;
|
|
619
|
-
try {
|
|
620
|
-
const child = spawn('docker', [
|
|
621
|
-
'compose',
|
|
622
|
-
'--project-name', project,
|
|
623
|
-
'--project-directory', deploymentDirectory,
|
|
624
|
-
'--env-file', join(deploymentDirectory, '.env'),
|
|
625
|
-
'-f', join(deploymentDirectory, 'compose.yaml'),
|
|
626
|
-
'-f', join(deploymentDirectory, 'compose.operator.yaml'),
|
|
627
|
-
'up', '-d', '--no-deps', '--force-recreate', 'or3-operator',
|
|
628
|
-
], {
|
|
629
|
-
cwd: deploymentDirectory,
|
|
630
|
-
env: process.env,
|
|
631
|
-
detached: process.platform !== 'win32',
|
|
632
|
-
stdio: 'ignore',
|
|
633
|
-
});
|
|
634
|
-
child.unref();
|
|
635
|
-
} catch {
|
|
636
|
-
// The durable terminal job and restart policy still leave a host-CLI
|
|
637
|
-
// recovery path if Docker cannot schedule the sidecar replacement.
|
|
611
|
+
async function completeOperatorHandoff(jobId, project) {
|
|
612
|
+
if (!requestIdPattern.test(jobId || '') || !/^[a-z0-9][a-z0-9_-]*$/i.test(project || '')) {
|
|
613
|
+
throw new Error('The dashboard operator handoff arguments are invalid.');
|
|
638
614
|
}
|
|
615
|
+
const deadline = Date.now() + operatorHandoffTimeoutMs;
|
|
616
|
+
while (Date.now() < deadline) {
|
|
617
|
+
const job = await readJob();
|
|
618
|
+
let state;
|
|
619
|
+
try {
|
|
620
|
+
state = JSON.parse(await readFile(statePath, 'utf8'));
|
|
621
|
+
} catch {
|
|
622
|
+
state = undefined;
|
|
623
|
+
}
|
|
624
|
+
if (
|
|
625
|
+
job?.id === jobId
|
|
626
|
+
&& ['succeeded', 'failed', 'needs_attention'].includes(job.phase)
|
|
627
|
+
&& state
|
|
628
|
+
&& !state.incompleteOperation
|
|
629
|
+
) {
|
|
630
|
+
await delay(1_000);
|
|
631
|
+
const environment = await deploymentEnv();
|
|
632
|
+
if (envValue(environment, 'OR3_DASHBOARD_UPDATES_ENABLED') !== 'true') return;
|
|
633
|
+
const restarted = await runProcess('docker', [
|
|
634
|
+
'compose',
|
|
635
|
+
'--project-name', project,
|
|
636
|
+
'--project-directory', deploymentDirectory,
|
|
637
|
+
'--env-file', join(deploymentDirectory, '.env'),
|
|
638
|
+
'-f', join(deploymentDirectory, 'compose.yaml'),
|
|
639
|
+
'-f', join(deploymentDirectory, 'compose.operator.yaml'),
|
|
640
|
+
'up', '-d', '--no-deps', '--force-recreate', 'or3-operator',
|
|
641
|
+
], {
|
|
642
|
+
cwd: deploymentDirectory,
|
|
643
|
+
env: process.env,
|
|
644
|
+
timeoutMs: 60_000,
|
|
645
|
+
});
|
|
646
|
+
if (restarted.code !== 0) {
|
|
647
|
+
throw new Error(`The dashboard operator handoff could not start its successor: ${restarted.output}`);
|
|
648
|
+
}
|
|
649
|
+
return;
|
|
650
|
+
}
|
|
651
|
+
await delay(250);
|
|
652
|
+
}
|
|
653
|
+
throw new Error('The dashboard operator handoff did not observe a durable terminal job.');
|
|
639
654
|
}
|
|
640
655
|
|
|
641
656
|
async function finish(server, job, error) {
|
|
@@ -644,21 +659,17 @@ async function finish(server, job, error) {
|
|
|
644
659
|
if (error) job.error = error instanceof Error ? error.message : 'The update did not complete. OR3 restored the previous verified deployment when possible.';
|
|
645
660
|
await writeJob(job);
|
|
646
661
|
await audit(error ? 'update_failed' : 'update_succeeded', { jobId: job.id, targetVersion: job.targetVersion });
|
|
647
|
-
//
|
|
648
|
-
//
|
|
649
|
-
//
|
|
662
|
+
// The target CLI schedules a separate helper container before returning.
|
|
663
|
+
// That helper can survive replacing this container after the terminal job
|
|
664
|
+
// and lifecycle journal are durable. A process inside this container cannot.
|
|
650
665
|
let enabled = true;
|
|
651
666
|
try {
|
|
652
667
|
const environment = await deploymentEnv();
|
|
653
668
|
enabled = envValue(environment, 'OR3_DASHBOARD_UPDATES_ENABLED') === 'true';
|
|
654
|
-
if (enabled) recreateOperatorAfterCommit(environment);
|
|
655
669
|
} catch {
|
|
656
|
-
// Keep the
|
|
670
|
+
// Keep serving the durable job when state cannot be read.
|
|
657
671
|
}
|
|
658
|
-
|
|
659
|
-
// still enabled, restart the sidecar so Docker recreates it from the just
|
|
660
|
-
// committed digest-qualified overlay and reloads the operator asset.
|
|
661
|
-
closeAfter(server, enabled ? 75 : 0);
|
|
672
|
+
if (!enabled) closeAfter(server, 0);
|
|
662
673
|
}
|
|
663
674
|
|
|
664
675
|
async function reconcileInterruptedJob() {
|
|
@@ -870,5 +881,9 @@ async function main() {
|
|
|
870
881
|
}
|
|
871
882
|
|
|
872
883
|
if (process.argv[1] && resolve(process.argv[1]) === resolve(fileURLToPath(import.meta.url))) {
|
|
873
|
-
|
|
884
|
+
if (process.argv[2] === '--complete-handoff') {
|
|
885
|
+
await completeOperatorHandoff(process.argv[3], process.argv[4]);
|
|
886
|
+
} else {
|
|
887
|
+
await main();
|
|
888
|
+
}
|
|
874
889
|
}
|
package/dist/cli.mjs
CHANGED
|
@@ -30,7 +30,7 @@ import { fileURLToPath } from "node:url";
|
|
|
30
30
|
import { createGunzip } from "node:zlib";
|
|
31
31
|
var execFile = promisify(execFileCallback);
|
|
32
32
|
var PACKAGE_ROOT = resolve(fileURLToPath(new URL("../", import.meta.url)));
|
|
33
|
-
var PACKAGE_VERSION = "0.1.
|
|
33
|
+
var PACKAGE_VERSION = "0.1.55";
|
|
34
34
|
var IMAGE_REPOSITORY = "ghcr.io/saluana/or3-chat";
|
|
35
35
|
var ASSET_ROOT = resolve(fileURLToPath(new URL("../assets/", import.meta.url)));
|
|
36
36
|
var STATE_SCHEMA_VERSION = 1;
|
|
@@ -42,6 +42,8 @@ var BACKUP_RETENTION_KEEP = 5;
|
|
|
42
42
|
var PURGE_REQUIRES_BACKUP_WITHIN_MS = 24 * 60 * 60 * 1000;
|
|
43
43
|
var FREE_SPACE_HEADROOM_BYTES = 64 * 1024 * 1024;
|
|
44
44
|
var BACKUP_ID_PATTERN = /^backup-[0-9A-Za-z-]+$/;
|
|
45
|
+
var DASHBOARD_JOB_ID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
|
46
|
+
var COMPOSE_PROJECT_PATTERN = /^[a-z0-9][a-z0-9_-]*$/i;
|
|
45
47
|
var MANAGED_ASSET_INVENTORY_VERSION = 3;
|
|
46
48
|
var DASHBOARD_LEASE_STALE_MS = 30000;
|
|
47
49
|
var PROVISIONING_CREDENTIAL_KEYS = [
|
|
@@ -1398,6 +1400,54 @@ async function prepareVerifiedDashboardOperator(directory, version) {
|
|
|
1398
1400
|
console.warn("Dashboard updates are unavailable on this Docker setup; the deployment will remain host-CLI managed.");
|
|
1399
1401
|
return;
|
|
1400
1402
|
}
|
|
1403
|
+
function dashboardOperatorHandoffArgs(directory, env, jobId) {
|
|
1404
|
+
const project = env.OR3_COMPOSE_PROJECT;
|
|
1405
|
+
if (!isAbsolute(directory) || !DASHBOARD_JOB_ID_PATTERN.test(jobId) || !COMPOSE_PROJECT_PATTERN.test(project ?? "") || !/^\d+$/.test(env.OR3_OPERATOR_UID ?? "") || !/^\d+$/.test(env.OR3_OPERATOR_GID ?? "") || !/^\d+$/.test(env.OR3_DOCKER_GID ?? "") || !isAbsolute(env.OR3_DOCKER_SOCKET ?? "") || !/@sha256:[0-9a-f]{64}$/i.test(env.OR3_OPERATOR_IMAGE ?? "")) {
|
|
1406
|
+
throw new Error("Refusing to schedule an invalid dashboard operator handoff.");
|
|
1407
|
+
}
|
|
1408
|
+
return [
|
|
1409
|
+
"run",
|
|
1410
|
+
"--detach",
|
|
1411
|
+
"--rm",
|
|
1412
|
+
"--network",
|
|
1413
|
+
"none",
|
|
1414
|
+
"--read-only",
|
|
1415
|
+
"--name",
|
|
1416
|
+
`${project}-operator-handoff-${jobId}`,
|
|
1417
|
+
"--user",
|
|
1418
|
+
`${env.OR3_OPERATOR_UID}:${env.OR3_OPERATOR_GID}`,
|
|
1419
|
+
"--group-add",
|
|
1420
|
+
env.OR3_DOCKER_GID,
|
|
1421
|
+
"--security-opt",
|
|
1422
|
+
"no-new-privileges:true",
|
|
1423
|
+
"--cap-drop",
|
|
1424
|
+
"ALL",
|
|
1425
|
+
"--mount",
|
|
1426
|
+
`type=bind,src=${env.OR3_DOCKER_SOCKET},dst=/var/run/docker.sock`,
|
|
1427
|
+
"--mount",
|
|
1428
|
+
`type=bind,src=${directory},dst=${directory},readonly`,
|
|
1429
|
+
"--workdir",
|
|
1430
|
+
directory,
|
|
1431
|
+
"--tmpfs",
|
|
1432
|
+
"/tmp:rw,noexec,nosuid,size=16m",
|
|
1433
|
+
"--entrypoint",
|
|
1434
|
+
"/usr/local/bin/node",
|
|
1435
|
+
env.OR3_OPERATOR_IMAGE,
|
|
1436
|
+
join(directory, "dashboard-operator.mjs"),
|
|
1437
|
+
"--complete-handoff",
|
|
1438
|
+
jobId,
|
|
1439
|
+
project
|
|
1440
|
+
];
|
|
1441
|
+
}
|
|
1442
|
+
async function scheduleDashboardOperatorHandoff(directory, env) {
|
|
1443
|
+
const jobId = process.env.OR3_DASHBOARD_UPDATE_JOB_ID?.trim();
|
|
1444
|
+
if (!jobId || env.OR3_DASHBOARD_UPDATES_ENABLED !== "true")
|
|
1445
|
+
return;
|
|
1446
|
+
const scheduled = await run("docker", dashboardOperatorHandoffArgs(directory, env, jobId), directory);
|
|
1447
|
+
if (!scheduled.ok || !/^[0-9a-f]{12,64}$/i.test(scheduled.stdout.trim())) {
|
|
1448
|
+
throw new Error(`Could not schedule the dashboard operator handoff. ${scheduled.stderr.trim()}`);
|
|
1449
|
+
}
|
|
1450
|
+
}
|
|
1401
1451
|
var DASHBOARD_OPERATOR_ENV_KEYS = [
|
|
1402
1452
|
"OR3_DASHBOARD_UPDATES_ENABLED",
|
|
1403
1453
|
"OR3_OPERATOR_IMAGE",
|
|
@@ -2926,8 +2976,9 @@ async function updateCommand(directory, flags) {
|
|
|
2926
2976
|
state.deploymentRoot = resolve(loaded.directory);
|
|
2927
2977
|
state.lastSuccessfulOperation = "update";
|
|
2928
2978
|
state.lastError = undefined;
|
|
2929
|
-
await clearPending(loaded.directory, state);
|
|
2930
2979
|
await pruneBackups(loaded.directory, state, BACKUP_RETENTION_KEEP, false);
|
|
2980
|
+
await scheduleDashboardOperatorHandoff(loaded.directory, nextEnv);
|
|
2981
|
+
await clearPending(loaded.directory, state);
|
|
2931
2982
|
console.log(`OR3 updated to ${targetVersion}. Image digest: ${digest}`);
|
|
2932
2983
|
console.log(`Rollback point: ${backup.backupId}. Keep it until login, chat, and file checks pass.`);
|
|
2933
2984
|
} catch (error) {
|
|
@@ -4590,6 +4641,7 @@ export {
|
|
|
4590
4641
|
parseEnv,
|
|
4591
4642
|
managedAssetNames,
|
|
4592
4643
|
isVersion,
|
|
4644
|
+
dashboardOperatorHandoffArgs,
|
|
4593
4645
|
copyAssets,
|
|
4594
4646
|
checkResolvedLoopbackBinding,
|
|
4595
4647
|
buildEnv,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@or3/cloud",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.55",
|
|
4
4
|
"description": "Install and operate the OR3 Cloud container distribution",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "GPL-3.0",
|
|
@@ -18,9 +18,9 @@
|
|
|
18
18
|
"or3Cloud": {
|
|
19
19
|
"dashboardUpdateProtocol": 1,
|
|
20
20
|
"dashboardUpdateMinimumSourceVersion": "0.1.40",
|
|
21
|
-
"imageDigest": "sha256:
|
|
22
|
-
"operatorImageDigest": "sha256:
|
|
23
|
-
"sourceRevision": "
|
|
21
|
+
"imageDigest": "sha256:d92a7a6b8e8a198a59c6506a5385beda6b7c829fc30406bc4653a388b8c5b638",
|
|
22
|
+
"operatorImageDigest": "sha256:e8149df09dc59d3184c325de09d5662b4efe8dccba70dfdde453b2fb5365c335",
|
|
23
|
+
"sourceRevision": "7a73e723edd4ca01ca7a387aad6902e185a8d16c"
|
|
24
24
|
},
|
|
25
25
|
"files": [
|
|
26
26
|
"dist",
|