openmates 0.25.0-alpha.1 → 0.25.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.
|
@@ -33831,15 +33831,37 @@ async function readRuntimeIncidentState(installDir, role) {
|
|
|
33831
33831
|
throw error;
|
|
33832
33832
|
}
|
|
33833
33833
|
}
|
|
33834
|
-
async function
|
|
33834
|
+
async function runtimeIncidentStateOwner(installDir, effectiveUid = process.geteuid?.() ?? process.getuid?.()) {
|
|
33835
|
+
if (effectiveUid !== 0) return null;
|
|
33836
|
+
const rootDir = path.join(installDir, ".openmates");
|
|
33837
|
+
const stateDir2 = path.join(rootDir, "runtime-health");
|
|
33838
|
+
const [rootInfo, stateInfo] = await Promise.all([fs.lstat(rootDir), fs.lstat(stateDir2)]);
|
|
33839
|
+
for (const info of [rootInfo, stateInfo]) {
|
|
33840
|
+
if (!info.isDirectory() || info.isSymbolicLink() || (info.mode & 511) !== 448) {
|
|
33841
|
+
throw new Error("Runtime health state requires private installer-owned directories.");
|
|
33842
|
+
}
|
|
33843
|
+
}
|
|
33844
|
+
if (rootInfo.uid !== stateInfo.uid || rootInfo.gid !== stateInfo.gid) {
|
|
33845
|
+
throw new Error("Runtime health state directories have conflicting owners.");
|
|
33846
|
+
}
|
|
33847
|
+
return { uid: stateInfo.uid, gid: stateInfo.gid };
|
|
33848
|
+
}
|
|
33849
|
+
async function writeRuntimeIncidentState(installDir, role, state, effectiveUid = process.geteuid?.() ?? process.getuid?.()) {
|
|
33835
33850
|
const stateDir2 = path.join(installDir, ".openmates", "runtime-health");
|
|
33836
33851
|
const statePath = path.join(stateDir2, `${role}.json`);
|
|
33837
|
-
const temporaryPath = `${statePath}.${
|
|
33852
|
+
const temporaryPath = `${statePath}.${randomUUID13()}.tmp`;
|
|
33838
33853
|
await fs.mkdir(stateDir2, { recursive: true, mode: 448 });
|
|
33839
|
-
await
|
|
33840
|
-
|
|
33841
|
-
|
|
33842
|
-
|
|
33854
|
+
const owner = await runtimeIncidentStateOwner(installDir, effectiveUid);
|
|
33855
|
+
try {
|
|
33856
|
+
await fs.writeFile(temporaryPath, `${JSON.stringify(state, null, 2)}
|
|
33857
|
+
`, { flag: "wx", mode: 384 });
|
|
33858
|
+
if (owner && (owner.uid !== 0 || owner.gid !== 0)) await fs.chown(temporaryPath, owner.uid, owner.gid);
|
|
33859
|
+
await fs.rename(temporaryPath, statePath);
|
|
33860
|
+
await fs.chmod(statePath, 384);
|
|
33861
|
+
} catch (error) {
|
|
33862
|
+
await fs.rm(temporaryPath, { force: true }).catch(() => void 0);
|
|
33863
|
+
throw error;
|
|
33864
|
+
}
|
|
33843
33865
|
}
|
|
33844
33866
|
function isPrivateAddress(address) {
|
|
33845
33867
|
const normalized = address.toLowerCase();
|
|
@@ -34736,6 +34758,56 @@ function lifecycleServiceSelection(role, flags, config) {
|
|
|
34736
34758
|
}
|
|
34737
34759
|
return { services: [], requested: false };
|
|
34738
34760
|
}
|
|
34761
|
+
function validateReuseCompletedSetupScope(input) {
|
|
34762
|
+
if (input.role !== "core" || input.installMode !== "image" || typeof input.servicesFlag !== "string" || input.excludeFlag !== void 0) {
|
|
34763
|
+
throw new Error("--reuse-completed-setup requires a core image-mode update with explicit --services and no --exclude.");
|
|
34764
|
+
}
|
|
34765
|
+
if (!input.selectedServices.length || input.selectedServices.some((service) => service !== "api" && service !== "prometheus" && !service.endsWith("-worker"))) {
|
|
34766
|
+
throw new Error("--reuse-completed-setup allows only api, worker, and prometheus services; exclude setup and infrastructure services.");
|
|
34767
|
+
}
|
|
34768
|
+
if (!/^sha-[a-f0-9]{40}$/i.test(input.targetTag)) {
|
|
34769
|
+
throw new Error("--reuse-completed-setup requires an immutable --image-tag sha-<40 hex characters>.");
|
|
34770
|
+
}
|
|
34771
|
+
}
|
|
34772
|
+
function validateReuseCompletedSetup(input) {
|
|
34773
|
+
validateReuseCompletedSetupScope(input);
|
|
34774
|
+
const expectedImage = `${input.registry.replace(/\/$/, "")}/openmates-cms-setup:${input.targetTag}`;
|
|
34775
|
+
if (input.setup.image !== expectedImage || input.setup.status !== "exited" || input.setup.exitCode !== 0) {
|
|
34776
|
+
throw new Error(`--reuse-completed-setup requires cms-setup exited successfully with target image ${expectedImage}.`);
|
|
34777
|
+
}
|
|
34778
|
+
for (const service of ["cms", "cms-database", "cache", "vault"]) {
|
|
34779
|
+
const state = input.dependencies[service];
|
|
34780
|
+
if (!state || state.status !== "running" || state.health !== void 0 && state.health !== "healthy") {
|
|
34781
|
+
throw new Error(`--reuse-completed-setup requires running, healthy ${service}.`);
|
|
34782
|
+
}
|
|
34783
|
+
}
|
|
34784
|
+
}
|
|
34785
|
+
function imageUpdateUpArgs(baseArgs, selectedServices, filterRequested, reuseCompletedSetup) {
|
|
34786
|
+
if (reuseCompletedSetup && (!filterRequested || selectedServices.length === 0)) {
|
|
34787
|
+
throw new Error("--reuse-completed-setup requires explicitly selected services.");
|
|
34788
|
+
}
|
|
34789
|
+
return appendSelectedServices([...baseArgs, "up", "-d", ...reuseCompletedSetup ? ["--no-deps"] : []], selectedServices, filterRequested);
|
|
34790
|
+
}
|
|
34791
|
+
function inspectReuseCompletedSetupContainer(installPath, withOverrides, service, includeStopped) {
|
|
34792
|
+
const compose = composeArgs(installPath, withOverrides, "image", "core", false);
|
|
34793
|
+
const ps = spawnSync("docker", [...compose, "ps", ...includeStopped ? ["-a"] : [], "-q", service], { cwd: installPath, encoding: "utf8", timeout: 5e3 });
|
|
34794
|
+
const ids = ps.status === 0 ? ps.stdout.trim().split(/\s+/).filter(Boolean) : [];
|
|
34795
|
+
if (ids.length !== 1) throw new Error(`--reuse-completed-setup could not identify one ${service} container.`);
|
|
34796
|
+
const inspect = (format) => {
|
|
34797
|
+
const result = spawnSync("docker", ["inspect", "--format", format, ids[0]], { cwd: installPath, encoding: "utf8", timeout: 5e3 });
|
|
34798
|
+
if (result.status !== 0) throw new Error(`--reuse-completed-setup could not inspect ${service}.`);
|
|
34799
|
+
return result.stdout.trim();
|
|
34800
|
+
};
|
|
34801
|
+
const image = JSON.parse(inspect("{{json .Config.Image}}"));
|
|
34802
|
+
const state = JSON.parse(inspect("{{json .State}}"));
|
|
34803
|
+
return { image, status: state.Status, exitCode: state.ExitCode, health: state.Health?.Status };
|
|
34804
|
+
}
|
|
34805
|
+
function verifyReuseCompletedSetup(installPath, withOverrides, input) {
|
|
34806
|
+
validateReuseCompletedSetupScope(input);
|
|
34807
|
+
const setup = inspectReuseCompletedSetupContainer(installPath, withOverrides, "cms-setup", true);
|
|
34808
|
+
const dependencies = Object.fromEntries(["cms", "cms-database", "cache", "vault"].map((service) => [service, inspectReuseCompletedSetupContainer(installPath, withOverrides, service, false)]));
|
|
34809
|
+
validateReuseCompletedSetup({ ...input, setup, dependencies });
|
|
34810
|
+
}
|
|
34739
34811
|
function shouldPullImages() {
|
|
34740
34812
|
return process.env.OPENMATES_SKIP_IMAGE_PULL !== "1";
|
|
34741
34813
|
}
|
|
@@ -34798,10 +34870,10 @@ ${renderFeatureOverrides(overrides)}`;
|
|
|
34798
34870
|
function featureKind(featureId) {
|
|
34799
34871
|
return featureId.split(":", 1)[0] || "unknown";
|
|
34800
34872
|
}
|
|
34801
|
-
function composeArgs(installPath, withOverrides, installMode = getInstallMode(installPath), role = "core") {
|
|
34873
|
+
function composeArgs(installPath, withOverrides, installMode = getInstallMode(installPath), role = "core", materializeCloudNoWebapp = true) {
|
|
34802
34874
|
const config = loadConfigForInstallPath(installPath);
|
|
34803
34875
|
const deploymentMode = getInstallDeploymentMode(installPath, config);
|
|
34804
|
-
ensureOfficialCloudNoWebappComposeFile(installPath, deploymentMode, role);
|
|
34876
|
+
if (materializeCloudNoWebapp) ensureOfficialCloudNoWebappComposeFile(installPath, deploymentMode, role);
|
|
34805
34877
|
const env = readEnvMap(installPath);
|
|
34806
34878
|
const overlayPath = env.OPENMATES_CLOUD_OVERLAY_PATH || config?.openMatesCloudOverlayPath || void 0;
|
|
34807
34879
|
const resolvedOverlayPath = overlayPath ?? defaultOpenMatesCloudOverlayPath(installPath);
|
|
@@ -36710,14 +36782,34 @@ async function serverUpdate(client2, rest, flags) {
|
|
|
36710
36782
|
}
|
|
36711
36783
|
const installPath = resolveServerPath(flags);
|
|
36712
36784
|
const dryRun = flags["dry-run"] === true;
|
|
36713
|
-
if (!dryRun) {
|
|
36714
|
-
ensureGitWorkDirEnv(installPath);
|
|
36715
|
-
ensureRuntimeMetricsDirectory(installPath);
|
|
36716
|
-
}
|
|
36717
36785
|
const config = loadConfigForInstallPath(installPath);
|
|
36718
36786
|
const role = getServerRole(flags, config);
|
|
36719
36787
|
const withOverrides = config?.composeProfile === "full";
|
|
36720
36788
|
const installMode = getInstallMode(installPath, config);
|
|
36789
|
+
const reuseCompletedSetup = flags["reuse-completed-setup"] === true;
|
|
36790
|
+
if (flags["reuse-completed-setup"] !== void 0 && !reuseCompletedSetup) {
|
|
36791
|
+
throw new Error("--reuse-completed-setup does not take a value.");
|
|
36792
|
+
}
|
|
36793
|
+
let guardedTarget;
|
|
36794
|
+
if (reuseCompletedSetup) {
|
|
36795
|
+
const selection = lifecycleServiceSelection(role, flags, config);
|
|
36796
|
+
const requested = resolveTargetImageTag(flags, getImageTagFromEnv(installPath, config), getPackageVersion());
|
|
36797
|
+
guardedTarget = requested.channel === "stable" ? { ...requested, tag: await resolveStableImageTag() } : requested;
|
|
36798
|
+
requireDocker();
|
|
36799
|
+
verifyReuseCompletedSetup(installPath, withOverrides, {
|
|
36800
|
+
role,
|
|
36801
|
+
installMode,
|
|
36802
|
+
servicesFlag: flags.services,
|
|
36803
|
+
excludeFlag: flags.exclude,
|
|
36804
|
+
selectedServices: selection.services,
|
|
36805
|
+
targetTag: guardedTarget.tag,
|
|
36806
|
+
registry: DEFAULT_IMAGE_REGISTRY
|
|
36807
|
+
});
|
|
36808
|
+
}
|
|
36809
|
+
if (!dryRun) {
|
|
36810
|
+
ensureGitWorkDirEnv(installPath);
|
|
36811
|
+
ensureRuntimeMetricsDirectory(installPath);
|
|
36812
|
+
}
|
|
36721
36813
|
const deploymentMode = getInstallDeploymentMode(installPath, config);
|
|
36722
36814
|
const caddyPlan = caddyUpdatePlan(installPath, role, config, flags);
|
|
36723
36815
|
const releaseUpdateLock = dryRun ? () => void 0 : acquireServerUpdateLock(installPath);
|
|
@@ -36742,7 +36834,7 @@ async function serverUpdate(client2, rest, flags) {
|
|
|
36742
36834
|
if (installMode === "image") {
|
|
36743
36835
|
const currentTag = getImageTagFromEnv(installPath, config);
|
|
36744
36836
|
const requestedTarget = resolveTargetImageTag(flags, currentTag, getPackageVersion());
|
|
36745
|
-
const target = requestedTarget.channel === "stable" ? { ...requestedTarget, tag: await resolveStableImageTag() } : requestedTarget;
|
|
36837
|
+
const target = guardedTarget ?? (requestedTarget.channel === "stable" ? { ...requestedTarget, tag: await resolveStableImageTag() } : requestedTarget);
|
|
36746
36838
|
const templateRef = templateRefForImageTag(target.tag, getPackageVersion());
|
|
36747
36839
|
const sourceLinks = targetSourceLinks(target.tag, templateRef);
|
|
36748
36840
|
const safetyPlan = planUpdate({ role, selectedServices, dryRun, skipBackup: flags["skip-backup"] === true, continuous: false, missingRequiredSecrets: missingOrUnverifiedSecrets });
|
|
@@ -36763,6 +36855,7 @@ async function serverUpdate(client2, rest, flags) {
|
|
|
36763
36855
|
secretPreflight,
|
|
36764
36856
|
providerKeyReminders: secretPreflight.emptySecretEnvKeys,
|
|
36765
36857
|
sourceLinks,
|
|
36858
|
+
reuseCompletedSetup,
|
|
36766
36859
|
blocked: safetyPlan.blocked,
|
|
36767
36860
|
blockReason: safetyPlan.blockReason,
|
|
36768
36861
|
dryRun
|
|
@@ -36786,7 +36879,7 @@ async function serverUpdate(client2, rest, flags) {
|
|
|
36786
36879
|
if (secretPreflight.emptySecretEnvKeys.length) {
|
|
36787
36880
|
console.log(` Provider keys: add ${secretPreflight.emptySecretEnvKeys.join(", ")} to activate those providers`);
|
|
36788
36881
|
}
|
|
36789
|
-
console.log(
|
|
36882
|
+
console.log(` Commands: refresh compose, docker compose pull, docker compose up -d${reuseCompletedSetup ? " --no-deps" : ""}, health checks`);
|
|
36790
36883
|
}
|
|
36791
36884
|
return;
|
|
36792
36885
|
}
|
|
@@ -36826,11 +36919,23 @@ async function serverUpdate(client2, rest, flags) {
|
|
|
36826
36919
|
} else {
|
|
36827
36920
|
console.error("Skipping image pull because OPENMATES_SKIP_IMAGE_PULL=1.");
|
|
36828
36921
|
}
|
|
36922
|
+
if (reuseCompletedSetup) {
|
|
36923
|
+
verifyReuseCompletedSetup(installPath, withOverrides, {
|
|
36924
|
+
role,
|
|
36925
|
+
installMode,
|
|
36926
|
+
servicesFlag: flags.services,
|
|
36927
|
+
excludeFlag: flags.exclude,
|
|
36928
|
+
selectedServices,
|
|
36929
|
+
targetTag: target.tag,
|
|
36930
|
+
registry: DEFAULT_IMAGE_REGISTRY
|
|
36931
|
+
});
|
|
36932
|
+
}
|
|
36829
36933
|
writeServerUpdateStatus(installPath, role, { status: "in_progress", targetImageTag: target.tag, sourceLinks, providerKeyReminders: secretPreflight.emptySecretEnvKeys, step: "up" });
|
|
36830
|
-
const upArgs2 =
|
|
36831
|
-
|
|
36934
|
+
const upArgs2 = imageUpdateUpArgs(
|
|
36935
|
+
composeArgs(installPath, withOverrides, installMode, role),
|
|
36832
36936
|
selectedServices,
|
|
36833
|
-
filterRequested
|
|
36937
|
+
filterRequested,
|
|
36938
|
+
reuseCompletedSetup
|
|
36834
36939
|
);
|
|
36835
36940
|
code2 = await runInteractive("docker", upArgs2, installPath);
|
|
36836
36941
|
if (code2 !== 0) throw new Error(`Docker image restart failed with exit code ${code2}.`);
|
|
@@ -38095,6 +38200,7 @@ Command Options:
|
|
|
38095
38200
|
--exclude <csv> Update all role services except selected services
|
|
38096
38201
|
--image-tag <tag> Image mode: update to a specific prebuilt image tag
|
|
38097
38202
|
--channel <name> Image mode: update using stable/main or dev channel tags
|
|
38203
|
+
--reuse-completed-setup Core image mode: reuse verified sha-<40 hex> cms-setup with explicit --services
|
|
38098
38204
|
--continuous Run continuously in foreground, or use with install-service
|
|
38099
38205
|
--interval <min> Foreground continuous update interval (default: 30)
|
|
38100
38206
|
install-service --continuous --channel <name> --window <window>
|
package/dist/cli.js
CHANGED
package/dist/index.js
CHANGED
package/package.json
CHANGED
|
@@ -296,6 +296,7 @@ services:
|
|
|
296
296
|
<<: *openmates-worker-env
|
|
297
297
|
CELERY_QUEUES: app_ai
|
|
298
298
|
CELERY_AUTOSCALE_MAX: ${APP_AI_WORKER_CONCURRENCY:-${CELERY_AUTOSCALE_MAX:-3}}
|
|
299
|
+
CELERY_METRICS_PORT: "9102"
|
|
299
300
|
command: >
|
|
300
301
|
sh -c "chown -R celeryuser:celeryuser /vault-data && gosu celeryuser python -m celery -A backend.core.api.app.tasks.celery_config worker --loglevel=info --queues=app_ai --concurrency=$${CELERY_AUTOSCALE_MAX} --max-tasks-per-child=50 --max-memory-per-child=600000 --prefetch-multiplier=1"
|
|
301
302
|
|
|
@@ -307,6 +308,7 @@ services:
|
|
|
307
308
|
<<: *openmates-worker-env
|
|
308
309
|
CELERY_QUEUES: app_images
|
|
309
310
|
CELERY_AUTOSCALE_MAX: ${APP_IMAGES_WORKER_CONCURRENCY:-${CELERY_AUTOSCALE_MAX:-3}}
|
|
311
|
+
CELERY_METRICS_PORT: "9103"
|
|
310
312
|
command: >
|
|
311
313
|
sh -c "chown -R celeryuser:celeryuser /vault-data && gosu celeryuser python -m celery -A backend.core.api.app.tasks.celery_config worker --loglevel=info --queues=app_images --concurrency=$${CELERY_AUTOSCALE_MAX} --max-tasks-per-child=50 --max-memory-per-child=600000 --prefetch-multiplier=1"
|
|
312
314
|
|
|
@@ -340,6 +342,7 @@ services:
|
|
|
340
342
|
environment:
|
|
341
343
|
<<: *openmates-worker-env
|
|
342
344
|
CELERY_QUEUES: app_pdf
|
|
345
|
+
CELERY_METRICS_PORT: "9104"
|
|
343
346
|
command: >
|
|
344
347
|
sh -c "chown -R celeryuser:celeryuser /vault-data && gosu celeryuser python -m celery -A backend.core.api.app.tasks.celery_config worker --loglevel=info --queues=app_pdf --concurrency=2 --max-tasks-per-child=10 --max-memory-per-child=1000000 --prefetch-multiplier=1"
|
|
345
348
|
|