@or3/cloud 0.1.54 → 0.1.56
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 +90 -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,86 @@ function closeAfter(server, code) {
|
|
|
607
608
|
setTimeout(() => server.close(() => process.exit(code)), 250).unref();
|
|
608
609
|
}
|
|
609
610
|
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
'
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
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.');
|
|
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
|
+
const environment = await deploymentEnv();
|
|
631
|
+
if (envValue(environment, 'OR3_DASHBOARD_UPDATES_ENABLED') !== 'true') return;
|
|
632
|
+
const serviceFilters = [
|
|
633
|
+
'--filter', `label=com.docker.compose.project=${project}`,
|
|
634
|
+
'--filter', 'label=com.docker.compose.service=or3-operator',
|
|
635
|
+
];
|
|
636
|
+
let lastOutput = '';
|
|
637
|
+
for (let attempt = 0; attempt < 6; attempt += 1) {
|
|
638
|
+
// Old protocol-v1 operators may start their own Compose replacement
|
|
639
|
+
// after writing the terminal job. Removing every container for this
|
|
640
|
+
// one stateless service also kills that in-container Compose child and
|
|
641
|
+
// clears any half-created successor without touching the application.
|
|
642
|
+
for (let sweep = 0; sweep < 2; sweep += 1) {
|
|
643
|
+
const listed = await runProcess('docker', ['ps', '-aq', ...serviceFilters], {
|
|
644
|
+
cwd: deploymentDirectory,
|
|
645
|
+
env: process.env,
|
|
646
|
+
timeoutMs: 30_000,
|
|
647
|
+
});
|
|
648
|
+
lastOutput = listed.output;
|
|
649
|
+
const containerIds = listed.code === 0
|
|
650
|
+
? listed.output.split(/\s+/).filter((value) => /^[0-9a-f]{12,64}$/i.test(value))
|
|
651
|
+
: [];
|
|
652
|
+
if (containerIds.length > 0) {
|
|
653
|
+
const removed = await runProcess('docker', ['rm', '--force', ...containerIds], {
|
|
654
|
+
cwd: deploymentDirectory,
|
|
655
|
+
env: process.env,
|
|
656
|
+
timeoutMs: 30_000,
|
|
657
|
+
});
|
|
658
|
+
lastOutput = `${lastOutput}\n${removed.output}`.trim();
|
|
659
|
+
}
|
|
660
|
+
await delay(750);
|
|
661
|
+
}
|
|
662
|
+
const compose = [
|
|
663
|
+
'compose',
|
|
664
|
+
'--project-name', project,
|
|
665
|
+
'--project-directory', deploymentDirectory,
|
|
666
|
+
'--env-file', join(deploymentDirectory, '.env'),
|
|
667
|
+
'-f', join(deploymentDirectory, 'compose.yaml'),
|
|
668
|
+
'-f', join(deploymentDirectory, 'compose.operator.yaml'),
|
|
669
|
+
];
|
|
670
|
+
const restarted = await runProcess('docker', [...compose, 'up', '-d', '--no-deps', 'or3-operator'], {
|
|
671
|
+
cwd: deploymentDirectory,
|
|
672
|
+
env: process.env,
|
|
673
|
+
timeoutMs: 60_000,
|
|
674
|
+
});
|
|
675
|
+
const running = restarted.code === 0
|
|
676
|
+
? await runProcess('docker', [...compose, 'ps', '--status', 'running', '-q', 'or3-operator'], {
|
|
677
|
+
cwd: deploymentDirectory,
|
|
678
|
+
env: process.env,
|
|
679
|
+
timeoutMs: 30_000,
|
|
680
|
+
})
|
|
681
|
+
: { code: restarted.code, output: '' };
|
|
682
|
+
lastOutput = `${lastOutput}\n${restarted.output}\n${running.output}`.trim();
|
|
683
|
+
if (restarted.code === 0 && running.code === 0 && /^[0-9a-f]{12,64}$/i.test(running.output.trim())) return;
|
|
684
|
+
await delay(2_000);
|
|
685
|
+
}
|
|
686
|
+
throw new Error(`The dashboard operator handoff could not start its successor: ${lastOutput}`);
|
|
687
|
+
}
|
|
688
|
+
await delay(250);
|
|
638
689
|
}
|
|
690
|
+
throw new Error('The dashboard operator handoff did not observe a durable terminal job.');
|
|
639
691
|
}
|
|
640
692
|
|
|
641
693
|
async function finish(server, job, error) {
|
|
@@ -644,21 +696,17 @@ async function finish(server, job, error) {
|
|
|
644
696
|
if (error) job.error = error instanceof Error ? error.message : 'The update did not complete. OR3 restored the previous verified deployment when possible.';
|
|
645
697
|
await writeJob(job);
|
|
646
698
|
await audit(error ? 'update_failed' : 'update_succeeded', { jobId: job.id, targetVersion: job.targetVersion });
|
|
647
|
-
//
|
|
648
|
-
//
|
|
649
|
-
//
|
|
699
|
+
// The target CLI schedules a separate helper container before returning.
|
|
700
|
+
// That helper can survive replacing this container after the terminal job
|
|
701
|
+
// and lifecycle journal are durable. A process inside this container cannot.
|
|
650
702
|
let enabled = true;
|
|
651
703
|
try {
|
|
652
704
|
const environment = await deploymentEnv();
|
|
653
705
|
enabled = envValue(environment, 'OR3_DASHBOARD_UPDATES_ENABLED') === 'true';
|
|
654
|
-
if (enabled) recreateOperatorAfterCommit(environment);
|
|
655
706
|
} catch {
|
|
656
|
-
// Keep the
|
|
707
|
+
// Keep serving the durable job when state cannot be read.
|
|
657
708
|
}
|
|
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);
|
|
709
|
+
if (!enabled) closeAfter(server, 0);
|
|
662
710
|
}
|
|
663
711
|
|
|
664
712
|
async function reconcileInterruptedJob() {
|
|
@@ -870,5 +918,9 @@ async function main() {
|
|
|
870
918
|
}
|
|
871
919
|
|
|
872
920
|
if (process.argv[1] && resolve(process.argv[1]) === resolve(fileURLToPath(import.meta.url))) {
|
|
873
|
-
|
|
921
|
+
if (process.argv[2] === '--complete-handoff') {
|
|
922
|
+
await completeOperatorHandoff(process.argv[3], process.argv[4]);
|
|
923
|
+
} else {
|
|
924
|
+
await main();
|
|
925
|
+
}
|
|
874
926
|
}
|
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.56";
|
|
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.56",
|
|
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:9d594d40e303149f52afca1b9a1df361a427b3ab347fcf794ea30f2e1f0ac9fc",
|
|
22
|
+
"operatorImageDigest": "sha256:4aa56bef475b2c1fd5f79f72e5c93433f9a3f0aaf0432cc7b823f6537407a125",
|
|
23
|
+
"sourceRevision": "7a6749f0f6d33390098ab030d38bafefb841d8c7"
|
|
24
24
|
},
|
|
25
25
|
"files": [
|
|
26
26
|
"dist",
|