@omnitron-dev/omnitron 0.3.5 → 0.4.6
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 +296 -6
- package/bin/{pnpmfile-sdk.mjs → pnpmfile-sdk.js} +13 -0
- package/bin/sdk.d.ts +19 -0
- package/bin/sdk.js +932 -0
- package/dist/BUILD.json +2 -2
- package/dist/cli/omnitron.js +14 -6
- package/dist/cli/omnitron.js.map +1 -1
- package/dist/commands/stack.d.ts +2 -5
- package/dist/commands/stack.d.ts.map +1 -1
- package/dist/commands/stack.js +32 -0
- package/dist/commands/stack.js.map +1 -1
- package/dist/commands/status.d.ts.map +1 -1
- package/dist/commands/status.js +1 -0
- package/dist/commands/status.js.map +1 -1
- package/dist/daemon/daemon-client.d.ts +3 -15
- package/dist/daemon/daemon-client.d.ts.map +1 -1
- package/dist/daemon/daemon-client.js +4 -0
- package/dist/daemon/daemon-client.js.map +1 -1
- package/dist/daemon/daemon.d.ts +5 -11
- package/dist/daemon/daemon.d.ts.map +1 -1
- package/dist/daemon/daemon.js +28 -42
- package/dist/daemon/daemon.js.map +1 -1
- package/dist/daemon/daemon.rpc-service.d.ts +3 -15
- package/dist/daemon/daemon.rpc-service.d.ts.map +1 -1
- package/dist/daemon/daemon.rpc-service.js +27 -3
- package/dist/daemon/daemon.rpc-service.js.map +1 -1
- package/dist/daemon/darwin-owner-evidence.js +1 -1
- package/dist/daemon/darwin-owner-evidence.js.map +1 -1
- package/dist/daemon/execution-capabilities.d.ts +1 -1
- package/dist/daemon/execution-capabilities.d.ts.map +1 -1
- package/dist/daemon/execution-capabilities.js +3 -3
- package/dist/daemon/execution-capabilities.js.map +1 -1
- package/dist/daemon/execution-client.d.ts +4 -0
- package/dist/daemon/execution-client.d.ts.map +1 -1
- package/dist/daemon/execution-client.js +28 -6
- package/dist/daemon/execution-client.js.map +1 -1
- package/dist/daemon/execution-host.d.ts +4 -11
- package/dist/daemon/execution-host.d.ts.map +1 -1
- package/dist/daemon/execution-host.js +3 -9
- package/dist/daemon/execution-host.js.map +1 -1
- package/dist/daemon/execution-owner.d.ts.map +1 -1
- package/dist/daemon/execution-owner.js +2 -16
- package/dist/daemon/execution-owner.js.map +1 -1
- package/dist/daemon/execution-shutdown.d.ts +1 -1
- package/dist/daemon/execution-shutdown.d.ts.map +1 -1
- package/dist/daemon/managed-owner-build.d.ts +4 -0
- package/dist/daemon/managed-owner-build.d.ts.map +1 -1
- package/dist/daemon/managed-owner-build.js +14 -2
- package/dist/daemon/managed-owner-build.js.map +1 -1
- package/dist/daemon/managed-runtime.js +1 -1
- package/dist/daemon/managed-runtime.js.map +1 -1
- package/dist/daemon/owner-reconciliation.d.ts.map +1 -1
- package/dist/daemon/owner-reconciliation.js +1 -2
- package/dist/daemon/owner-reconciliation.js.map +1 -1
- package/dist/daemon/system-guardian-cgroup.js +1 -1
- package/dist/daemon/system-guardian-cgroup.js.map +1 -1
- package/dist/infrastructure/existing-containers.d.ts +25 -0
- package/dist/infrastructure/existing-containers.d.ts.map +1 -0
- package/dist/infrastructure/existing-containers.js +183 -0
- package/dist/infrastructure/existing-containers.js.map +1 -0
- package/dist/infrastructure/infrastructure.service.d.ts +14 -1
- package/dist/infrastructure/infrastructure.service.d.ts.map +1 -1
- package/dist/infrastructure/infrastructure.service.js +118 -9
- package/dist/infrastructure/infrastructure.service.js.map +1 -1
- package/dist/infrastructure/stack-infra-manager.d.ts +8 -2
- package/dist/infrastructure/stack-infra-manager.d.ts.map +1 -1
- package/dist/infrastructure/stack-infra-manager.js +46 -9
- package/dist/infrastructure/stack-infra-manager.js.map +1 -1
- package/dist/monitoring/log-journal.d.ts.map +1 -1
- package/dist/monitoring/log-journal.js +1 -2
- package/dist/monitoring/log-journal.js.map +1 -1
- package/dist/operations.d.ts +1 -0
- package/dist/operations.d.ts.map +1 -1
- package/dist/operations.js +1 -0
- package/dist/operations.js.map +1 -1
- package/dist/orchestrator/bootstrap-loader.d.ts +2 -0
- package/dist/orchestrator/bootstrap-loader.d.ts.map +1 -1
- package/dist/orchestrator/bootstrap-loader.js +2 -1
- package/dist/orchestrator/bootstrap-loader.js.map +1 -1
- package/dist/orchestrator/classic-launcher.d.ts.map +1 -1
- package/dist/orchestrator/classic-launcher.js +2 -1
- package/dist/orchestrator/classic-launcher.js.map +1 -1
- package/dist/orchestrator/execution-contract.d.ts +2 -3
- package/dist/orchestrator/execution-contract.d.ts.map +1 -1
- package/dist/orchestrator/file-watcher.d.ts +11 -23
- package/dist/orchestrator/file-watcher.d.ts.map +1 -1
- package/dist/orchestrator/file-watcher.js +57 -42
- package/dist/orchestrator/file-watcher.js.map +1 -1
- package/dist/orchestrator/orchestrator.service.d.ts +10 -5
- package/dist/orchestrator/orchestrator.service.d.ts.map +1 -1
- package/dist/orchestrator/orchestrator.service.js +117 -20
- package/dist/orchestrator/orchestrator.service.js.map +1 -1
- package/dist/project/bootstrap-metadata.d.ts +1 -1
- package/dist/project/bootstrap-metadata.d.ts.map +1 -1
- package/dist/project/bootstrap-metadata.js +3 -2
- package/dist/project/bootstrap-metadata.js.map +1 -1
- package/dist/project/node-stack-stop.d.ts +2 -1
- package/dist/project/node-stack-stop.d.ts.map +1 -1
- package/dist/project/node-stack-stop.js +5 -1
- package/dist/project/node-stack-stop.js.map +1 -1
- package/dist/release/closure-library-path.d.ts +8 -2
- package/dist/release/closure-library-path.d.ts.map +1 -1
- package/dist/release/closure-library-path.js +72 -28
- package/dist/release/closure-library-path.js.map +1 -1
- package/dist/release/local-installation.d.ts +2 -0
- package/dist/release/local-installation.d.ts.map +1 -1
- package/dist/release/local-installation.js +1 -1
- package/dist/release/local-installation.js.map +1 -1
- package/dist/release/portable-archive.d.ts +3 -0
- package/dist/release/portable-archive.d.ts.map +1 -1
- package/dist/release/portable-archive.js +13 -7
- package/dist/release/portable-archive.js.map +1 -1
- package/dist/release/registry-framework.d.ts +25 -0
- package/dist/release/registry-framework.d.ts.map +1 -1
- package/dist/release/registry-framework.js +51 -3
- package/dist/release/registry-framework.js.map +1 -1
- package/dist/release/target-runtime.d.ts +4 -0
- package/dist/release/target-runtime.d.ts.map +1 -1
- package/dist/release/target-runtime.js +9 -0
- package/dist/release/target-runtime.js.map +1 -1
- package/dist/services/host-upgrade.d.ts +2 -0
- package/dist/services/host-upgrade.d.ts.map +1 -1
- package/dist/services/host-upgrade.js +6 -2
- package/dist/services/host-upgrade.js.map +1 -1
- package/dist/services/infrastructure.rpc-service.d.ts +8 -0
- package/dist/services/infrastructure.rpc-service.d.ts.map +1 -1
- package/dist/services/infrastructure.rpc-service.js +90 -10
- package/dist/services/infrastructure.rpc-service.js.map +1 -1
- package/dist/services/kernel-rollout.service.d.ts.map +1 -1
- package/dist/services/kernel-rollout.service.js +1 -2
- package/dist/services/kernel-rollout.service.js.map +1 -1
- package/dist/services/migration-process.d.ts +1 -0
- package/dist/services/migration-process.d.ts.map +1 -1
- package/dist/services/migration-process.js +4 -2
- package/dist/services/migration-process.js.map +1 -1
- package/dist/services/pipeline-definition.d.ts.map +1 -1
- package/dist/services/pipeline-definition.js +32 -6
- package/dist/services/pipeline-definition.js.map +1 -1
- package/dist/services/pipeline.service.d.ts +7 -1
- package/dist/services/pipeline.service.d.ts.map +1 -1
- package/dist/services/pipeline.service.js +106 -13
- package/dist/services/pipeline.service.js.map +1 -1
- package/dist/services/project.rpc-service.d.ts +6 -5
- package/dist/services/project.rpc-service.d.ts.map +1 -1
- package/dist/services/project.rpc-service.js +13 -0
- package/dist/services/project.rpc-service.js.map +1 -1
- package/dist/services/project.service.d.ts +24 -6
- package/dist/services/project.service.d.ts.map +1 -1
- package/dist/services/project.service.js +327 -59
- package/dist/services/project.service.js.map +1 -1
- package/dist/services/remote-deployer.service.d.ts.map +1 -1
- package/dist/services/remote-deployer.service.js +3 -0
- package/dist/services/remote-deployer.service.js.map +1 -1
- package/dist/shared/dto/backend.d.ts +3 -0
- package/dist/shared/dto/backend.d.ts.map +1 -1
- package/dist/shared/dto/backend.js.map +1 -1
- package/dist/shared/dto/pipelines.d.ts +54 -5
- package/dist/shared/dto/pipelines.d.ts.map +1 -1
- package/dist/shared/dto/pipelines.js +0 -7
- package/dist/shared/dto/pipelines.js.map +1 -1
- package/dist/shared/dto/project.d.ts +60 -0
- package/dist/shared/dto/project.d.ts.map +1 -1
- package/dist/shared/dto/services.d.ts +26 -17
- package/dist/shared/dto/services.d.ts.map +1 -1
- package/dist/shared/dto/services.js.map +1 -1
- package/dist/workers/system-worker-manager.d.ts +1 -2
- package/dist/workers/system-worker-manager.d.ts.map +1 -1
- package/dist/workers/system-worker-manager.js +5 -3
- package/dist/workers/system-worker-manager.js.map +1 -1
- package/package.json +17 -16
- package/webapp/dist/BUILD.json +2 -2
- package/webapp/dist/assets/{access-DUjabwNX.js → access-B2gXuoMG.js} +1 -1
- package/webapp/dist/assets/{access-BvKK9dEv.js → access-I0f72P9m.js} +1 -1
- package/webapp/dist/assets/{admin-data-table-BKSDI8sU.js → admin-data-table-CAEL2f-m.js} +1 -1
- package/webapp/dist/assets/{alert-uBG6r9NW.js → alert-BdJ2uaY0.js} +1 -1
- package/webapp/dist/assets/{alerts-CzN6OqX5.js → alerts-DeVkMXWo.js} +1 -1
- package/webapp/dist/assets/{apps-G_Lm6m4F.js → apps-9SuDvyGQ.js} +1 -1
- package/webapp/dist/assets/{audit-CLMpdBzG.js → audit-DT8cY7Ys.js} +1 -1
- package/webapp/dist/assets/backend-status.store-BbBE9cn8.js +4 -0
- package/webapp/dist/assets/{card-DkH32gMY.js → card-DaUEmUI5.js} +1 -1
- package/webapp/dist/assets/{chart-BpGXlCJc.js → chart-BKXP2ter.js} +1 -1
- package/webapp/dist/assets/{client-DFnuv2z3.js → client-CgyTwq9g.js} +1 -1
- package/webapp/dist/assets/{code-editor-DK6Tmmxv.js → code-editor-JVuQ4m-z.js} +1 -1
- package/webapp/dist/assets/{comparison-bars-BtC0dHjF.js → comparison-bars-Ds5Dt1oj.js} +1 -1
- package/webapp/dist/assets/{confirm-dialog-BbGsAz7_.js → confirm-dialog-BayExVbF.js} +1 -1
- package/webapp/dist/assets/{console-keyboard-D3YCA0yU.js → console-keyboard-uSLt92dB.js} +1 -1
- package/webapp/dist/assets/{containers-BsFwprq4.js → containers-DkXTgcrY.js} +1 -1
- package/webapp/dist/assets/{controls-C5W8gHU4.js → controls-CqeGPnaF.js} +1 -1
- package/webapp/dist/assets/{dashboard-BN7rKE5n.js → dashboard-By5yPuOf.js} +1 -1
- package/webapp/dist/assets/{dashboard-builder-BXFxWjXP.js → dashboard-builder-C0hvBwkd.js} +1 -1
- package/webapp/dist/assets/{deploy-progress-CUUwlYtQ.js → deploy-progress-B39cvvCE.js} +1 -1
- package/webapp/dist/assets/{deploy-release-dialog-58lGZDNk.js → deploy-release-dialog-CnzaO_xL.js} +1 -1
- package/webapp/dist/assets/{deployments-DEzjHerg.js → deployments-BXP943hA.js} +1 -1
- package/webapp/dist/assets/{detail-pXPk4O0E.js → detail-B84RgiOb.js} +1 -1
- package/webapp/dist/assets/{detail-Dj3gxaH0.js → detail-CVXDks9-.js} +1 -1
- package/webapp/dist/assets/{detail-CBn9pjJU.js → detail-D9DixtaK.js} +1 -1
- package/webapp/dist/assets/{empty-content-7jOFhG4C.js → empty-content-eyDOuOVp.js} +1 -1
- package/webapp/dist/assets/{error-boundary-CLLI8PWw.js → error-boundary-CQmDr_HS.js} +1 -1
- package/webapp/dist/assets/{event-fields-CiyqrRO8.js → event-fields-C7p2a2HY.js} +1 -1
- package/webapp/dist/assets/{files-DigwXndm.js → files-BsfGT3t6.js} +2 -2
- package/webapp/dist/assets/{icons-OboZgWSt.js → icons-B9Ct8PJ9.js} +1 -1
- package/webapp/dist/assets/index-BEfv96EF.js +1 -0
- package/webapp/dist/assets/{label-B1SF0RTL.js → label-avMhe08g.js} +1 -1
- package/webapp/dist/assets/{log-level-filter-Bcn31D1F.js → log-level-filter-oaUGdbpk.js} +1 -1
- package/webapp/dist/assets/{logs-DrsBbLfb.js → logs-iC1OEDv3.js} +1 -1
- package/webapp/dist/assets/{metric-chart-M-kI-XOt.js → metric-chart-CSa_qv4F.js} +1 -1
- package/webapp/dist/assets/{metrics-D5cDZTEo.js → metrics-CKCmcGKw.js} +1 -1
- package/webapp/dist/assets/{mui-Cd711nOk.js → mui-B3fJdIMc.js} +1 -1
- package/webapp/dist/assets/{mui-CdB_p2lt.js → mui-Bdr6Ks5j.js} +1 -1
- package/webapp/dist/assets/{mui-Db5J-QUd.js → mui-CADnFGCx.js} +1 -1
- package/webapp/dist/assets/{mui-AnEgAVjz.js → mui-CMWGTMXv.js} +1 -1
- package/webapp/dist/assets/{mui-DyT2n1lx.js → mui-CN3dT7bb.js} +1 -1
- package/webapp/dist/assets/{mui-BwsIrivo.js → mui-CPEkFAPo.js} +1 -1
- package/webapp/dist/assets/{mui-dLCwWoMw.js → mui-D3qAWvfZ.js} +1 -1
- package/webapp/dist/assets/{mui-D5m7fDCh.js → mui-D7tBU80e.js} +1 -1
- package/webapp/dist/assets/{mui-59N0OVRa.js → mui-DaJavAoc.js} +1 -1
- package/webapp/dist/assets/{mui-BXSXx9c9.js → mui-SNRRJ_vi.js} +1 -1
- package/webapp/dist/assets/{nodes-CwwZZuu4.js → nodes-DtLyXH0V.js} +1 -1
- package/webapp/dist/assets/{operation-status-Bm1x1wTz.js → operation-status-DHyYOT7G.js} +1 -1
- package/webapp/dist/assets/{page-breadcrumbs-fR7Jmpp4.js → page-breadcrumbs-3R8RZ3nA.js} +1 -1
- package/webapp/dist/assets/pipelines-Do2NAAqK.js +3 -0
- package/webapp/dist/assets/{project.store-DuCgv6Eq.js → project.store-D5IR3fo9.js} +1 -1
- package/webapp/dist/assets/{projects-TBAdpCeX.js → projects-BqlGbzeG.js} +1 -1
- package/webapp/dist/assets/{realtime.store-hfxZD2DU.js → realtime.store-C8D235-B.js} +1 -1
- package/webapp/dist/assets/{release-bits-BJ6i23y0.js → release-bits-IehuBssK.js} +1 -1
- package/webapp/dist/assets/{release-build-panel-BNXkwNE1.js → release-build-panel-C48hP_9w.js} +1 -1
- package/webapp/dist/assets/{release-deployment-state-gN8ZT3aq.js → release-deployment-state-BfmxZP4Y.js} +1 -1
- package/webapp/dist/assets/{releases-CXXKUh-v.js → releases-Cm0RV5Xu.js} +1 -1
- package/webapp/dist/assets/{search-input-C0MHI8vO.js → search-input-SWD1dXxD.js} +1 -1
- package/webapp/dist/assets/settings-DwuEPQGg.js +1 -0
- package/webapp/dist/assets/sign-in-CQ-WeS5A.js +1 -0
- package/webapp/dist/assets/{skeleton-BmWDJWF9.js → skeleton-BnVxAoeX.js} +1 -1
- package/webapp/dist/assets/{snackbar-DSPmjMyi.js → snackbar-CRX6eqRT.js} +1 -1
- package/webapp/dist/assets/{stack-health-status-BIv2C10x.js → stack-health-status-DfErRNb1.js} +1 -1
- package/webapp/dist/assets/{stack-start-notice-D-AH_bzn.js → stack-start-notice-NkYDjR-f.js} +1 -1
- package/webapp/dist/assets/{stacks-WY_ynkON.js → stacks-BOGCfW90.js} +1 -1
- package/webapp/dist/assets/{store-bqUBn2A4.js → store-DtTtbc9H.js} +1 -1
- package/webapp/dist/assets/{system-info-DXMwo6gl.js → system-info-Cw-xywgI.js} +1 -1
- package/webapp/dist/assets/{table-BOWT6Y9d.js → table-D-so_B1p.js} +1 -1
- package/webapp/dist/assets/{table-viewport-Bp4pq-c_.js → table-viewport-DdZ1yAKm.js} +1 -1
- package/webapp/dist/assets/{tabs-k5NPGLnJ.js → tabs-DGqBkXh_.js} +1 -1
- package/webapp/dist/assets/{terminal-QWBau-Us.js → terminal-Bbl0XNeB.js} +1 -1
- package/webapp/dist/assets/{topology-C_Qp1RqW.js → topology-DmrRy7nN.js} +1 -1
- package/webapp/dist/assets/{topology-metadata-dialog-BivuU2y6.js → topology-metadata-dialog-Dpfp4YF1.js} +1 -1
- package/webapp/dist/assets/{traces-By0rdfDC.js → traces-CKHsOabJ.js} +1 -1
- package/webapp/dist/assets/{use-stack-context-8yaHaN8L.js → use-stack-context-BuraYaeY.js} +1 -1
- package/webapp/dist/assets/webapp-D8h_eabt.js +35 -0
- package/webapp/dist/console-version.json +1 -1
- package/webapp/dist/index.html +34 -34
- package/webapp/dist/logo.svg +1 -80
- package/bin/sdk.d.mts +0 -8
- package/bin/sdk.mjs +0 -580
- package/dist/monitoring/process-lifetime.d.ts +0 -11
- package/dist/monitoring/process-lifetime.d.ts.map +0 -1
- package/dist/monitoring/process-lifetime.js +0 -45
- package/dist/monitoring/process-lifetime.js.map +0 -1
- package/dist/shared/host-boot-identity.d.ts +0 -12
- package/dist/shared/host-boot-identity.d.ts.map +0 -1
- package/dist/shared/host-boot-identity.js +0 -89
- package/dist/shared/host-boot-identity.js.map +0 -1
- package/webapp/dist/assets/index-Bo9An3pJ.js +0 -1
- package/webapp/dist/assets/omnitron-logo-koMCbsCv.js +0 -1
- package/webapp/dist/assets/pipelines-BrfTjf6Y.js +0 -3
- package/webapp/dist/assets/settings-B3egRvbI.js +0 -1
- package/webapp/dist/assets/sign-in-9q-Nfy83.js +0 -1
- package/webapp/dist/assets/webapp-BO_WoB_z.js +0 -35
- package/webapp/dist/favicon.png +0 -0
|
@@ -11,6 +11,9 @@ import { readProjectStacks, resolveProjectStacks } from '../config/project-stack
|
|
|
11
11
|
import { readSavedDaemonConfig } from '../config/saved.js';
|
|
12
12
|
import { observeStackHealth } from '../project/stack-health.js';
|
|
13
13
|
import { LocalReleaseSelection } from '../project/local-release-selection.js';
|
|
14
|
+
import { assertResidentApplicationRuntime, assertSameApplicationRuntime } from './host-upgrade.js';
|
|
15
|
+
import { assertReleaseLoaderEnvironment, assertReleaseTarget, releaseOptionsForEnv } from '../release/target-runtime.js';
|
|
16
|
+
import { withClosureLibraryPath } from '../release/closure-library-path.js';
|
|
14
17
|
import { resolveAppPath } from '../config/app-path.js';
|
|
15
18
|
import { infrastructureOwnership, ownsStackInfrastructure } from '../project/infrastructure-ownership.js';
|
|
16
19
|
import { InstalledStackResume, resumeDestinations } from '../project/installed-stack-resume.js';
|
|
@@ -18,7 +21,7 @@ import { isDeepStrictEqual } from 'node:util';
|
|
|
18
21
|
import { diagnoseContainers } from '../project/stack-diagnosis.js';
|
|
19
22
|
import { diagnosisStatus } from '../shared/dto/stack-diagnosis.js';
|
|
20
23
|
import { buildInfraFromRequirements, requirementsFromDefinitions } from '../project/requirements-scanner.js';
|
|
21
|
-
import { randomUUID } from 'node:crypto';
|
|
24
|
+
import { randomUUID, createHash } from 'node:crypto';
|
|
22
25
|
import { NodeStackStop, stackDefinitionFingerprint } from '../project/node-stack-stop.js';
|
|
23
26
|
import { stackStopReview } from '../project/stack-stop-review.js';
|
|
24
27
|
import { inspectStackContainers, removeContainer } from '../infrastructure/container-runtime.js';
|
|
@@ -272,7 +275,18 @@ export class ProjectService extends EventEmitter {
|
|
|
272
275
|
execution: orchestrator,
|
|
273
276
|
definition: (scope, cached) => this.stopDefinition(scope, cached),
|
|
274
277
|
containers: scope => inspectStackContainers(scope),
|
|
275
|
-
disableResume:
|
|
278
|
+
disableResume: (scope, keep) => {
|
|
279
|
+
if (keep)
|
|
280
|
+
this.setInfrastructurePolicy(scope.project, scope.stack, null);
|
|
281
|
+
this.registry.setStackEnabled(scope.project, scope.stack, false);
|
|
282
|
+
},
|
|
283
|
+
holdInfrastructure: async (scope) => {
|
|
284
|
+
await this.drainHostedProvisioning(scope);
|
|
285
|
+
const hosted = this.hostedInfrastructure();
|
|
286
|
+
if (hosted?.representsStack(scope))
|
|
287
|
+
await hosted.holdForPreservation();
|
|
288
|
+
await this.infraManager.getInstance(scope.project, scope.stack)?.holdForPreservation();
|
|
289
|
+
},
|
|
276
290
|
retireInfrastructure: async (scope) => {
|
|
277
291
|
await this.drainHostedProvisioning(scope);
|
|
278
292
|
await this.hostedInfrastructure()?.retireStack(scope);
|
|
@@ -1317,15 +1331,15 @@ export class ProjectService extends EventEmitter {
|
|
|
1317
1331
|
continue;
|
|
1318
1332
|
for (const app of await this.launchEntries(scope.project, name, stack, config))
|
|
1319
1333
|
if (app.bootstrap)
|
|
1320
|
-
bootstraps.set(resolveAppPath(directory, app, app.bootstrap), app.name);
|
|
1334
|
+
bootstraps.set(resolveAppPath(directory, app, app.bootstrap), { app: app.name, ...(app.releaseRoot && { releaseRoot: app.releaseRoot }) });
|
|
1321
1335
|
}
|
|
1322
1336
|
const result = { refreshed: 0, failures: [] };
|
|
1323
|
-
for (const [bootstrap, app] of bootstraps) {
|
|
1337
|
+
for (const [bootstrap, { app, releaseRoot }] of bootstraps) {
|
|
1324
1338
|
try {
|
|
1325
1339
|
const remaining = Math.min(30_000, deadline - Date.now());
|
|
1326
1340
|
if (remaining <= 0)
|
|
1327
1341
|
throw new Error('Declaration refresh deadline exceeded; this application was not read');
|
|
1328
|
-
const { metadata: record } = await readBootstrapMetadata(bootstrap, false, remaining);
|
|
1342
|
+
const { metadata: record } = await readBootstrapMetadata(bootstrap, false, remaining, releaseRoot);
|
|
1329
1343
|
this.bootstrapMetadata.save(record);
|
|
1330
1344
|
this.bootstrapTopology.set(bootstrap, record.definition);
|
|
1331
1345
|
result.refreshed += 1;
|
|
@@ -1385,6 +1399,109 @@ export class ProjectService extends EventEmitter {
|
|
|
1385
1399
|
*/
|
|
1386
1400
|
rereading;
|
|
1387
1401
|
startsInFlight = new Map();
|
|
1402
|
+
infrastructurePolicyKey(project, stack) {
|
|
1403
|
+
const registration = this.registry.get(project);
|
|
1404
|
+
if (!registration?.registeredAt)
|
|
1405
|
+
throw Error('Infrastructure authority requires a project registration');
|
|
1406
|
+
return `lifecycle:infrastructure:${stackExecutionKey({ project, stack })}:${registration.registeredAt}`;
|
|
1407
|
+
}
|
|
1408
|
+
infrastructurePolicy(project, stack) {
|
|
1409
|
+
const value = this.daemonStateStore.kvGetSync(this.infrastructurePolicyKey(project, stack), { strict: true });
|
|
1410
|
+
if (value && (value.mode !== 'preserve' || (value.review !== null && !/^[a-f0-9]{64}$/.test(value.review))))
|
|
1411
|
+
throw Error('Invalid retained infrastructure authority');
|
|
1412
|
+
return value;
|
|
1413
|
+
}
|
|
1414
|
+
setInfrastructurePolicy(project, stack, review, reconcile = false) {
|
|
1415
|
+
this.daemonStateStore.kvUpdateSync(this.infrastructurePolicyKey(project, stack), () => reconcile ? null : { mode: 'preserve', review });
|
|
1416
|
+
}
|
|
1417
|
+
/** Read declarations without importing application code or executing startup hooks. */
|
|
1418
|
+
preservationDefinitions(project, stack, entries) {
|
|
1419
|
+
const root = this.registry.get(project)?.path;
|
|
1420
|
+
if (!root)
|
|
1421
|
+
throw Error('Project is not registered');
|
|
1422
|
+
const definitions = new Map();
|
|
1423
|
+
for (const entry of entries) {
|
|
1424
|
+
if (entry.releaseFrom)
|
|
1425
|
+
throw Error('Preservation requires a prepared release installation');
|
|
1426
|
+
if (!entry.bootstrap)
|
|
1427
|
+
continue;
|
|
1428
|
+
const bootstrap = resolveAppPath(root, entry, entry.bootstrap);
|
|
1429
|
+
const { metadata, declared, state, record } = this.readAppDeclaration(project, stack, entry.name, bootstrap);
|
|
1430
|
+
if (declared.malformed || !metadata || !record || state?.failure || metadataSourceChanged(record))
|
|
1431
|
+
throw Error('Current application declarations must be captured before reviewing infrastructure preservation');
|
|
1432
|
+
definitions.set(entry.name, withDeclaredConfig({ name: entry.name, version: '', processes: [], ...metadata }, declared));
|
|
1433
|
+
}
|
|
1434
|
+
return definitions;
|
|
1435
|
+
}
|
|
1436
|
+
/** Shared lifecycle preflight. No release installation, bootstrap import, provisioning or database effects. */
|
|
1437
|
+
async reviewStackStart(project, stackName, options = {}) {
|
|
1438
|
+
if (options.infrastructure !== undefined && options.infrastructure !== 'preserve')
|
|
1439
|
+
throw Error('Start review requires infrastructure=preserve');
|
|
1440
|
+
if (options.workingTree && options.release)
|
|
1441
|
+
throw Error('Select either a release or the working tree');
|
|
1442
|
+
const report = { protocol: 1, project, stack: stackName, infrastructure: 'preserve', checkedAt: new Date().toISOString(),
|
|
1443
|
+
digest: null, targets: [], deferred: ['Application startup and application health; framework setup and migrations are forbidden in preserve mode'] };
|
|
1444
|
+
try {
|
|
1445
|
+
const config = this.getLoadedConfig(project);
|
|
1446
|
+
if (!config)
|
|
1447
|
+
throw Error('Project configuration is not loaded');
|
|
1448
|
+
const stack = this.resolveStacks(config, project)[stackName];
|
|
1449
|
+
if (!stack)
|
|
1450
|
+
throw Error('Stack is not declared');
|
|
1451
|
+
this.assertStartAuthority({ project, stack: stackName }, stack);
|
|
1452
|
+
const local = stack.type === 'local' && !stack.managedOrigin;
|
|
1453
|
+
const prior = local ? this.localSelection(project, stackName).read().release : null;
|
|
1454
|
+
const id = options.release ?? (options.workingTree ? undefined : prior?.id);
|
|
1455
|
+
report.selection = id ? { release: id } : { workingTree: true };
|
|
1456
|
+
const release = id ? await this.admitRelease(project, stackName, stack, config, id) : null;
|
|
1457
|
+
if (!release && stack.release?.mode === 'required')
|
|
1458
|
+
throw Error('A verified release is required');
|
|
1459
|
+
if (release && prior && prior.id === id && prior.manifestSha256 !== release.manifestSha256)
|
|
1460
|
+
throw Error('Selected release changed');
|
|
1461
|
+
const installation = local && release ? await this.installedRelease(project, stackName, stack, config, release, false) : undefined;
|
|
1462
|
+
const entries = installation?.entries ?? this.resolveStackApps(stack, config);
|
|
1463
|
+
resolveStartupOrder(entries);
|
|
1464
|
+
const definitions = release?.remoteDefinitions ?? this.preservationDefinitions(project, stackName, entries);
|
|
1465
|
+
const environment = await this.stackEnvFor(project, stackName, stack, entries.map(entry => entry.name));
|
|
1466
|
+
const effective = mergeInferredInfrastructure(buildInfraFromRequirements(requirementsFromDefinitions(definitions)), installation?.infrastructure ?? mergeInfrastructure(config, stack));
|
|
1467
|
+
const normalized = normalizeInfraConfig(effective ?? {}, createDefaultRegistry());
|
|
1468
|
+
const requirements = { ...normalized };
|
|
1469
|
+
for (const definition of definitions.values())
|
|
1470
|
+
Object.assign(requirements, definition.omnitronConfig?.infrastructure);
|
|
1471
|
+
if (Object.entries(requirements).some(([name, requirement]) => {
|
|
1472
|
+
const binding = bindService(requirement, stack.serviceOverrides?.[name]);
|
|
1473
|
+
return binding.provisioning === 'bareMetal' || binding.provisioning === 'none';
|
|
1474
|
+
}))
|
|
1475
|
+
throw Error('This infrastructure resource has no verifiable native preservation identity');
|
|
1476
|
+
if (stack.type === 'local') {
|
|
1477
|
+
const infrastructure = await this.infraManager.reviewPreservation(project, stackName, stack, effective ?? {}, definitions, this.registry.get(project).path);
|
|
1478
|
+
report.targets.push({ node: 'local', infrastructure });
|
|
1479
|
+
}
|
|
1480
|
+
else {
|
|
1481
|
+
const services = await this.collectDeclaredServices(project, stack, config, { definitions });
|
|
1482
|
+
for (const { node } of resolveNodePlacements(stack, entries)) {
|
|
1483
|
+
const infrastructure = await this.reviewNodeInfrastructure(node, mergeInfrastructure(config, stack) ?? {}, services, { project, stack: stackName, overrides: await this.resolveOverrideSecrets(project, stack.serviceOverrides) }, this.registry.get(project).path, release?.staticsDir ?? undefined);
|
|
1484
|
+
report.targets.push({ node: stackNodeKey(node.host, node.port ?? 9700), infrastructure });
|
|
1485
|
+
}
|
|
1486
|
+
}
|
|
1487
|
+
// Bind launch bytes as well as declarations; release installation verification binds the full payload closure.
|
|
1488
|
+
const launch = stack.type === 'local' ? entries.map(entry => {
|
|
1489
|
+
const script = entry.bootstrap ?? entry.script;
|
|
1490
|
+
return script ? [entry.name, createHash('sha256').update(fs.readFileSync(resolveAppPath(this.registry.get(project).path, entry, script))).digest('hex')] : [entry.name];
|
|
1491
|
+
}) : [];
|
|
1492
|
+
if (report.targets.length && report.targets.every(target => target.infrastructure?.digest))
|
|
1493
|
+
report.digest = stackDefinitionFingerprint([
|
|
1494
|
+
'omnitron/stack-start/preserve/v1', { project, stack: stackName }, this.registry.get(project).registeredAt,
|
|
1495
|
+
stack, entries, [...definitions], environment, release?.manifestSha256 ?? null, launch,
|
|
1496
|
+
report.targets.map(target => [target.node, target.infrastructure.digest]),
|
|
1497
|
+
]);
|
|
1498
|
+
}
|
|
1499
|
+
catch {
|
|
1500
|
+
// Source errors may contain credential values. Evidence contains only reviewed fingerprints and controlled diagnostics.
|
|
1501
|
+
report.targets.push({ node: 'preflight', reason: 'Complete launch and infrastructure preservation could not be verified. Check release preparation, current declarations and recipient capability.' });
|
|
1502
|
+
}
|
|
1503
|
+
return report;
|
|
1504
|
+
}
|
|
1388
1505
|
async startStack(projectName, stackName, opts) {
|
|
1389
1506
|
if (this.resumingInstalled)
|
|
1390
1507
|
throw new Error('An installed allocation is resuming');
|
|
@@ -1397,7 +1514,19 @@ export class ProjectService extends EventEmitter {
|
|
|
1397
1514
|
throw new Error('Project definitions are being updated; retry the start');
|
|
1398
1515
|
if (opts?.workingTree && opts.release)
|
|
1399
1516
|
throw new Error('Select either a release or the working tree');
|
|
1400
|
-
|
|
1517
|
+
if (opts?.infrastructure !== undefined && !['reconcile', 'preserve'].includes(opts.infrastructure))
|
|
1518
|
+
throw Error('Invalid infrastructure start mode');
|
|
1519
|
+
const policy = this.infrastructurePolicy(projectName, stackName);
|
|
1520
|
+
if (policy && opts?.infrastructure === 'reconcile' && (opts.source === 'boot' || opts.source === 'auto-resume'))
|
|
1521
|
+
throw Error('Automatic starts cannot expand preserved infrastructure authority');
|
|
1522
|
+
const mode = opts?.infrastructure ?? policy?.mode ?? 'reconcile';
|
|
1523
|
+
const review = opts?.review ?? (mode === 'preserve' ? policy?.review : undefined);
|
|
1524
|
+
if (mode === 'preserve' && (!review || !/^[a-f0-9]{64}$/.test(review)))
|
|
1525
|
+
throw Error('Preserved infrastructure requires a successful stack start review; inspect --dry-run --infrastructure preserve');
|
|
1526
|
+
if (opts?.review && mode !== 'preserve')
|
|
1527
|
+
throw Error('A preservation review cannot authorize reconciliation');
|
|
1528
|
+
const infrastructure = { mode, review: review ?? undefined, explicitReconcile: opts?.infrastructure === 'reconcile' };
|
|
1529
|
+
const selection = JSON.stringify([opts?.release, opts?.workingTree === true, opts?.reinstall === true, infrastructure]);
|
|
1401
1530
|
const running = this.startsInFlight.get(inFlightKey);
|
|
1402
1531
|
if (running) {
|
|
1403
1532
|
this.logger.info({ project: projectName, stack: stackName }, 'This stack is already being started — joining the run in progress');
|
|
@@ -1405,7 +1534,7 @@ export class ProjectService extends EventEmitter {
|
|
|
1405
1534
|
throw new Error('Another stack start selected different launch code; retry after it completes');
|
|
1406
1535
|
return running.promise;
|
|
1407
1536
|
}
|
|
1408
|
-
const started = this.startStackOnce(projectName, stackName, opts?.source ?? 'unknown', opts?.allowDirty === true, opts?.release, opts?.reinstall === true, opts?.workingTree === true).finally(() => {
|
|
1537
|
+
const started = this.startStackOnce(projectName, stackName, opts?.source ?? 'unknown', opts?.allowDirty === true, opts?.release, opts?.reinstall === true, opts?.workingTree === true, infrastructure).finally(() => {
|
|
1409
1538
|
this.startsInFlight.delete(inFlightKey);
|
|
1410
1539
|
});
|
|
1411
1540
|
this.startsInFlight.set(inFlightKey, { selection, promise: started });
|
|
@@ -1436,10 +1565,10 @@ export class ProjectService extends EventEmitter {
|
|
|
1436
1565
|
* deploys nothing, and the reconciler re-asks a node that does not answer
|
|
1437
1566
|
* every few minutes — rows about looking, not about changing anything.
|
|
1438
1567
|
*/
|
|
1439
|
-
async startStackOnce(projectName, stackName, source, allowDirty, releaseId, reinstall = false, workingTree = false) {
|
|
1568
|
+
async startStackOnce(projectName, stackName, source, allowDirty, releaseId, reinstall = false, workingTree = false, infrastructure) {
|
|
1440
1569
|
const known = { attach: false };
|
|
1441
1570
|
try {
|
|
1442
|
-
return await this.runStackStart(projectName, stackName, source, allowDirty, releaseId, known, reinstall, workingTree);
|
|
1571
|
+
return await this.runStackStart(projectName, stackName, source, allowDirty, releaseId, known, reinstall, workingTree, infrastructure);
|
|
1443
1572
|
}
|
|
1444
1573
|
catch (err) {
|
|
1445
1574
|
if (!known.attach) {
|
|
@@ -1451,6 +1580,8 @@ export class ProjectService extends EventEmitter {
|
|
|
1451
1580
|
error: err,
|
|
1452
1581
|
details: {
|
|
1453
1582
|
source,
|
|
1583
|
+
...(infrastructure?.mode === 'preserve' ? { infrastructureMode: 'preserve', review: infrastructure.review }
|
|
1584
|
+
: infrastructure?.explicitReconcile ? { infrastructureMode: 'reconcile' } : {}),
|
|
1454
1585
|
...(known.type ? { type: known.type } : {}),
|
|
1455
1586
|
...((releaseId ?? known.release) ? { release: releaseId ?? known.release } : {}),
|
|
1456
1587
|
...(known.commit !== undefined ? { commit: known.commit } : {}),
|
|
@@ -1460,8 +1591,10 @@ export class ProjectService extends EventEmitter {
|
|
|
1460
1591
|
throw err;
|
|
1461
1592
|
}
|
|
1462
1593
|
}
|
|
1463
|
-
async runStackStart(projectName, stackName, source, allowDirty, releaseId, known, reinstall = false, workingTree = false) {
|
|
1464
|
-
const config = await this.loadProjectConfig(projectName);
|
|
1594
|
+
async runStackStart(projectName, stackName, source, allowDirty, releaseId, known, reinstall = false, workingTree = false, infrastructure) {
|
|
1595
|
+
const config = infrastructure?.mode === 'preserve' ? this.getLoadedConfig(projectName) : await this.loadProjectConfig(projectName);
|
|
1596
|
+
if (!config)
|
|
1597
|
+
throw Error('Project configuration must be loaded before preservation');
|
|
1465
1598
|
const stacks = this.resolveStacks(config, projectName);
|
|
1466
1599
|
const stackConfig = stacks[stackName];
|
|
1467
1600
|
if (!stackConfig)
|
|
@@ -1475,6 +1608,13 @@ export class ProjectService extends EventEmitter {
|
|
|
1475
1608
|
return this.attachRemoteStack(projectName, stackName, stackConfig, source);
|
|
1476
1609
|
}
|
|
1477
1610
|
this.assertStartAuthority({ project: projectName, stack: stackName }, stackConfig);
|
|
1611
|
+
let preservation;
|
|
1612
|
+
if (infrastructure?.mode === 'preserve') {
|
|
1613
|
+
preservation = await this.reviewStackStart(projectName, stackName, { infrastructure: 'preserve', ...(releaseId && { release: releaseId }), ...(workingTree && { workingTree: true }) });
|
|
1614
|
+
if (!preservation.digest || preservation.digest !== infrastructure.review)
|
|
1615
|
+
throw Error('Stack launch or native infrastructure changed since review; no startup effects were admitted');
|
|
1616
|
+
this.setInfrastructurePolicy(projectName, stackName, preservation.digest);
|
|
1617
|
+
}
|
|
1478
1618
|
// A release, when one is named — and a stack that takes nothing else.
|
|
1479
1619
|
const local = stackConfig.type === 'local' && !stackConfig.managedOrigin;
|
|
1480
1620
|
if ((workingTree || (releaseId && stackConfig.type === 'local')) && !local)
|
|
@@ -1568,8 +1708,8 @@ export class ProjectService extends EventEmitter {
|
|
|
1568
1708
|
if (mounted.length && (!local || !release || !ownsStackInfrastructure(stackConfig)))
|
|
1569
1709
|
throw Error('Artifact volumes require a selected release on an independently owned local stack');
|
|
1570
1710
|
const installation = mounted.length && release
|
|
1571
|
-
? await this.installedRelease(projectName, stackName, stackConfig, config, release,
|
|
1572
|
-
const entries = installation?.entries ?? (local && release ? await this.installedEntries(projectName, stackName, stackConfig, config, release,
|
|
1711
|
+
? await this.installedRelease(projectName, stackName, stackConfig, config, release, !preservation) : undefined;
|
|
1712
|
+
const entries = installation?.entries ?? (local && release ? await this.installedEntries(projectName, stackName, stackConfig, config, release, !preservation) : undefined);
|
|
1573
1713
|
let localMembershipChanged = false;
|
|
1574
1714
|
if (local) {
|
|
1575
1715
|
const target = entries ?? this.resolveStackApps(stackConfig, config);
|
|
@@ -1613,7 +1753,7 @@ export class ProjectService extends EventEmitter {
|
|
|
1613
1753
|
// wrapper returns the info untouched.
|
|
1614
1754
|
const current = await this.withRemoteAppStatuses(projectName, this.toStackInfo(projectName, stackName, stackConfig));
|
|
1615
1755
|
const down = current.apps.filter((a) => a.status !== 'online');
|
|
1616
|
-
if (down.length === 0 && !current.notUp?.length) {
|
|
1756
|
+
if (down.length === 0 && !current.notUp?.length && !preservation && !infrastructure?.explicitReconcile) {
|
|
1617
1757
|
if (existing)
|
|
1618
1758
|
existing.status = 'running';
|
|
1619
1759
|
this.logger.warn({ project: projectName, stack: stackName }, 'Stack already running');
|
|
@@ -1661,20 +1801,22 @@ export class ProjectService extends EventEmitter {
|
|
|
1661
1801
|
if (changing && plan.phase !== 'stopped')
|
|
1662
1802
|
throw new LocalReleaseStopRequired('Artifact volume selection requires the owner stop fence');
|
|
1663
1803
|
return (await this.installedRelease(projectName, stackName, stackConfig, config, release, false)).infrastructure;
|
|
1664
|
-
} : undefined);
|
|
1804
|
+
} : undefined, preservation, infrastructure?.explicitReconcile);
|
|
1665
1805
|
}
|
|
1666
1806
|
else {
|
|
1667
|
-
await this.startLocalStack(projectName, stackName, stackConfig, config, undefined, async () => { await enable(); });
|
|
1807
|
+
await this.startLocalStack(projectName, stackName, stackConfig, config, undefined, async () => { await enable(); }, undefined, undefined, preservation, infrastructure?.explicitReconcile);
|
|
1668
1808
|
}
|
|
1669
1809
|
}
|
|
1670
1810
|
else {
|
|
1671
1811
|
this.rememberStackTargets(projectName, stackName, stackConfig, config);
|
|
1672
|
-
reach = await this.startRemoteStack(projectName, stackName, stackConfig, config, release, reinstall);
|
|
1812
|
+
reach = await this.startRemoteStack(projectName, stackName, stackConfig, config, release, reinstall, preservation, infrastructure?.explicitReconcile);
|
|
1673
1813
|
}
|
|
1674
1814
|
state.status = 'running';
|
|
1675
1815
|
state.startedAt = Date.now();
|
|
1676
1816
|
// Update registry with enabled stack
|
|
1677
1817
|
this.updateEnabledStacks(projectName, stackName, true);
|
|
1818
|
+
if (infrastructure?.explicitReconcile)
|
|
1819
|
+
this.setInfrastructurePolicy(projectName, stackName, null, true);
|
|
1678
1820
|
this.markPriorStopSuperseded(projectName, stackName);
|
|
1679
1821
|
const info = this.toStackInfo(projectName, stackName, stackConfig);
|
|
1680
1822
|
// What did not come up, said in the row. A boot at 12:14 UTC on
|
|
@@ -1726,6 +1868,8 @@ export class ProjectService extends EventEmitter {
|
|
|
1726
1868
|
apps: info.apps.length,
|
|
1727
1869
|
...(notUp.length > 0 ? { notUp: notUp.join(', ') } : {}),
|
|
1728
1870
|
source,
|
|
1871
|
+
...(preservation ? { infrastructureMode: 'preserve', review: preservation.digest }
|
|
1872
|
+
: infrastructure?.explicitReconcile ? { infrastructureMode: 'reconcile' } : {}),
|
|
1729
1873
|
...(reach ? { nodes: reach.nodes, reached: reach.reached } : {}),
|
|
1730
1874
|
...(reach && reach.skipped.length > 0 ? { skipped: reach.skipped } : {}),
|
|
1731
1875
|
...(reach && (reach.notReady?.length ?? 0) > 0 ? { notReady: reach.notReady } : {}),
|
|
@@ -1966,6 +2110,8 @@ export class ProjectService extends EventEmitter {
|
|
|
1966
2110
|
if (cluster)
|
|
1967
2111
|
await cluster.election.confirmStopAuthority();
|
|
1968
2112
|
this.updateEnabledStacks(projectName, stackName, false);
|
|
2113
|
+
if (keepInfrastructure)
|
|
2114
|
+
this.setInfrastructurePolicy(projectName, stackName, null);
|
|
1969
2115
|
progress.phase = 'stopping';
|
|
1970
2116
|
progress.intentCommitted = true;
|
|
1971
2117
|
persist();
|
|
@@ -1980,13 +2126,18 @@ export class ProjectService extends EventEmitter {
|
|
|
1980
2126
|
}
|
|
1981
2127
|
}
|
|
1982
2128
|
else if (localPlan && localInventory) {
|
|
1983
|
-
|
|
1984
|
-
if (keepInfrastructure) {
|
|
2129
|
+
const verifyRetained = async () => {
|
|
1985
2130
|
const retained = await this.infraManager.inspectStopContainers(projectName, stackName);
|
|
1986
|
-
if (!isDeepStrictEqual([...retained].sort((a, b) => a.id.localeCompare(b.id)), [...localInventory.containers].sort((a, b) => a.id.localeCompare(b.id))))
|
|
2131
|
+
if (!isDeepStrictEqual([...retained].sort((a, b) => a.id.localeCompare(b.id)), [...localInventory.containers].sort((a, b) => a.id.localeCompare(b.id))))
|
|
1987
2132
|
throw new Error('Retained stack infrastructure changed; inspect before confirming this application stop');
|
|
1988
|
-
|
|
2133
|
+
};
|
|
2134
|
+
if (keepInfrastructure) {
|
|
2135
|
+
await this.infraManager.getInstance(projectName, stackName)?.holdForPreservation();
|
|
2136
|
+
await verifyRetained();
|
|
1989
2137
|
}
|
|
2138
|
+
await this.orchestrator.stopStackExecution({ operationId: progress.id, plan: localPlan });
|
|
2139
|
+
if (keepInfrastructure)
|
|
2140
|
+
await verifyRetained();
|
|
1990
2141
|
else
|
|
1991
2142
|
await this.infraManager.teardownStack(projectName, stackName, localInventory.containers);
|
|
1992
2143
|
}
|
|
@@ -3298,7 +3449,7 @@ export class ProjectService extends EventEmitter {
|
|
|
3298
3449
|
// ===========================================================================
|
|
3299
3450
|
// Private — Stack Launchers
|
|
3300
3451
|
// ===========================================================================
|
|
3301
|
-
async startLocalStack(projectName, stackName, stackConfig, ecosystemConfig, selectedEntries, enable, verifyMigrations, verifyArtifactInfrastructure) {
|
|
3452
|
+
async startLocalStack(projectName, stackName, stackConfig, ecosystemConfig, selectedEntries, enable, verifyMigrations, verifyArtifactInfrastructure, preservation, reconcile = false) {
|
|
3302
3453
|
const ownsInfrastructure = ownsStackInfrastructure(stackConfig);
|
|
3303
3454
|
const stateKey = `${projectName}/${stackName}`;
|
|
3304
3455
|
const state = this.stackStates.get(stateKey);
|
|
@@ -3309,7 +3460,8 @@ export class ProjectService extends EventEmitter {
|
|
|
3309
3460
|
// start here (project/stack-env.ts).
|
|
3310
3461
|
const stackEnv = await this.stackEnvFor(projectName, stackName, stackConfig, appEntries.map((e) => e.name));
|
|
3311
3462
|
const project = this.registry.get(projectName);
|
|
3312
|
-
const appDefinitions =
|
|
3463
|
+
const appDefinitions = preservation ? this.preservationDefinitions(projectName, stackName, appEntries)
|
|
3464
|
+
: await this.loadAppDefinitions(projectName, stackConfig, ecosystemConfig, appEntries);
|
|
3313
3465
|
// 2. Provision per-stack infrastructure (built-in + app-declared services)
|
|
3314
3466
|
// Auto-detect core services (postgres, redis, minio) from app requirements,
|
|
3315
3467
|
// then merge with explicit infrastructure config (gateway, custom services).
|
|
@@ -3319,8 +3471,13 @@ export class ProjectService extends EventEmitter {
|
|
|
3319
3471
|
throw new Error(`Bootstrap declarations unavailable for ${entry.name}`);
|
|
3320
3472
|
}
|
|
3321
3473
|
const effectiveInfra = ownsInfrastructure ? mergeInferredInfrastructure(buildInfraFromRequirements(requirementsFromDefinitions(appDefinitions)), verifyArtifactInfrastructure ? await verifyArtifactInfrastructure() : mergeInfrastructure(ecosystemConfig, stackConfig)) : undefined;
|
|
3474
|
+
if (preservation) {
|
|
3475
|
+
const current = await this.reviewStackStart(projectName, stackName, { infrastructure: 'preserve', ...preservation.selection });
|
|
3476
|
+
if (!current.digest || current.digest !== preservation.digest)
|
|
3477
|
+
throw Error('Launch, release or native infrastructure changed immediately before start');
|
|
3478
|
+
}
|
|
3322
3479
|
// Refuse missing/tampered migrators before creating or changing infrastructure.
|
|
3323
|
-
if (effectiveInfra && project) {
|
|
3480
|
+
if (effectiveInfra && project && !preservation) {
|
|
3324
3481
|
const migrations = await localMigrationTargets(requestedMigrations(normalizeInfraConfig(effectiveInfra, createDefaultRegistry())), appEntries, project.path, verifyMigrations);
|
|
3325
3482
|
const resolveMigration = this.stackEntryBuilder(projectName, stackName, stackConfig, ecosystemConfig, appDefinitions, stackEnv);
|
|
3326
3483
|
validateMigrationDestinations(migrations.map(target => {
|
|
@@ -3330,7 +3487,7 @@ export class ProjectService extends EventEmitter {
|
|
|
3330
3487
|
}
|
|
3331
3488
|
if (effectiveInfra) {
|
|
3332
3489
|
try {
|
|
3333
|
-
const infraService = await this.infraManager.provisionStack(projectName, stackName, stackConfig, effectiveInfra, appDefinitions, project?.path);
|
|
3490
|
+
const infraService = preservation ? await this.infraManager.preserveStack(projectName, stackName, stackConfig, effectiveInfra, preservation.targets.find(target => target.node === 'local').infrastructure.digest, appDefinitions, project?.path) : await this.infraManager.provisionStack(projectName, stackName, stackConfig, effectiveInfra, appDefinitions, project?.path, reconcile);
|
|
3334
3491
|
state.infraService = infraService;
|
|
3335
3492
|
const outcome = summariseProvisioning(infraService.getDesiredServices(), infraService.getState().services);
|
|
3336
3493
|
if (!outcome.ready && !outcome.empty) {
|
|
@@ -3354,7 +3511,7 @@ export class ProjectService extends EventEmitter {
|
|
|
3354
3511
|
// `settings.allowDegradedInfra: true` (the field is namespaced under
|
|
3355
3512
|
// settings to remain forward-compatible with stack schema).
|
|
3356
3513
|
const allowDegraded = stackConfig.settings?.['allowDegradedInfra'] === true;
|
|
3357
|
-
if (!allowDegraded) {
|
|
3514
|
+
if (!allowDegraded || preservation) {
|
|
3358
3515
|
const cleanMsg = `Stack '${projectName}/${stackName}' infrastructure provisioning failed:\n` +
|
|
3359
3516
|
` ${reason}\n\n` +
|
|
3360
3517
|
`Apps were not started. Fix the underlying infrastructure issue (Docker socket, ` +
|
|
@@ -3373,7 +3530,7 @@ export class ProjectService extends EventEmitter {
|
|
|
3373
3530
|
// Resolve once for both migrations and application startup. Inferred service
|
|
3374
3531
|
// requirements are not permission to connect to a database or run migrations.
|
|
3375
3532
|
const appConfigBuilder = this.stackEntryBuilder(projectName, stackName, stackConfig, ecosystemConfig, appDefinitions, stackEnv);
|
|
3376
|
-
if (effectiveInfra && project) {
|
|
3533
|
+
if (effectiveInfra && project && !preservation) {
|
|
3377
3534
|
await this.runStackMigrations(projectName, stackName, this.infraManager.getNormalizedServices(projectName, stackName) ?? {}, appEntries, project.path, appConfigBuilder, verifyMigrations);
|
|
3378
3535
|
}
|
|
3379
3536
|
await enable?.(appConfigBuilder);
|
|
@@ -3435,10 +3592,10 @@ export class ProjectService extends EventEmitter {
|
|
|
3435
3592
|
* and silent about which of its seven steps was running. See
|
|
3436
3593
|
* `deploy-phases.ts`.
|
|
3437
3594
|
*/
|
|
3438
|
-
async startRemoteStack(projectName, stackName, stackConfig, ecosystemConfig, release = null, reinstall = false) {
|
|
3595
|
+
async startRemoteStack(projectName, stackName, stackConfig, ecosystemConfig, release = null, reinstall = false, preservation, reconcile = false) {
|
|
3439
3596
|
const phases = reportPhases(this.logger, { project: projectName, stack: stackName });
|
|
3440
3597
|
try {
|
|
3441
|
-
return await withDeploymentProgress({ project: projectName, stack: stackName, operationId: randomUUID(), ...(release && { releaseId: release.id }) }, () => this.deployRemoteStack(projectName, stackName, stackConfig, ecosystemConfig, phases, release, reinstall));
|
|
3598
|
+
return await withDeploymentProgress({ project: projectName, stack: stackName, operationId: randomUUID(), ...(release && { releaseId: release.id }) }, () => this.deployRemoteStack(projectName, stackName, stackConfig, ecosystemConfig, phases, release, reinstall, preservation, reconcile));
|
|
3442
3599
|
}
|
|
3443
3600
|
finally {
|
|
3444
3601
|
// Every exit, including the refusals: a reporter that outlives its
|
|
@@ -3446,7 +3603,7 @@ export class ProjectService extends EventEmitter {
|
|
|
3446
3603
|
phases.done();
|
|
3447
3604
|
}
|
|
3448
3605
|
}
|
|
3449
|
-
async deployRemoteStack(projectName, stackName, stackConfig, ecosystemConfig, phases, release = null, reinstall = false) {
|
|
3606
|
+
async deployRemoteStack(projectName, stackName, stackConfig, ecosystemConfig, phases, release = null, reinstall = false, preservation, reconcile = false) {
|
|
3450
3607
|
const nodes = stackConfig.nodes ?? [];
|
|
3451
3608
|
if (nodes.length === 0) {
|
|
3452
3609
|
throw new Error(`Remote stack '${stackName}' has no nodes configured`);
|
|
@@ -3482,8 +3639,9 @@ export class ProjectService extends EventEmitter {
|
|
|
3482
3639
|
const appEntries = this.resolveStackApps(stackConfig, ecosystemConfig);
|
|
3483
3640
|
const migrationRequests = requestedMigrations({ ...declaredServices,
|
|
3484
3641
|
...normalizeInfraConfig(mergeInfrastructure(ecosystemConfig, stackConfig) ?? {}, createDefaultRegistry()) });
|
|
3485
|
-
|
|
3486
|
-
|
|
3642
|
+
if (!preservation)
|
|
3643
|
+
validateMigrationOwners(migrationRequests, appEntries);
|
|
3644
|
+
const placements = orderMigrationPlacements(preservation ? [] : migrationRequests, resolveNodePlacements(stackConfig, appEntries));
|
|
3487
3645
|
// What the stack gives its apps, vault references resolved — before any
|
|
3488
3646
|
// node is touched: a key the vault does not hold refuses the deployment
|
|
3489
3647
|
// here, not after a node was provisioned (project/stack-env.ts). A remote
|
|
@@ -3611,6 +3769,11 @@ export class ProjectService extends EventEmitter {
|
|
|
3611
3769
|
machine: `${target.host}:${target.sshPort ?? 22}`,
|
|
3612
3770
|
run: this.deployer.leaseRunner(target),
|
|
3613
3771
|
})), `${projectName}/${stackName}`, this.logger, async (leases) => {
|
|
3772
|
+
if (preservation) {
|
|
3773
|
+
const current = await this.reviewStackStart(projectName, stackName, { infrastructure: 'preserve', ...preservation.selection });
|
|
3774
|
+
if (!current.digest || current.digest !== preservation.digest)
|
|
3775
|
+
throw Error('Preserved stack changed while acquiring deployment authority');
|
|
3776
|
+
}
|
|
3614
3777
|
if (release && leases.unreachable.size) {
|
|
3615
3778
|
const missing = [...leases.unreachable].map(([node, reason]) => `${node}: ${reason}`).join(', ');
|
|
3616
3779
|
throw Error('Release deployment requires every selected node to be reachable before activation: ' + missing);
|
|
@@ -3651,11 +3814,11 @@ export class ProjectService extends EventEmitter {
|
|
|
3651
3814
|
this.logger.warn({ node: nodeKey, because: master.reason }, 'Provisioning this node without a master address — it will be pulled from, not dial in');
|
|
3652
3815
|
}
|
|
3653
3816
|
await leases.confirm(nodeKey, `provisioning ${node.host}`);
|
|
3654
|
-
const lock = await this.holdForDeployment(target, projectName, stackName, stackConfig, ecosystemConfig, release, phases);
|
|
3817
|
+
const lock = preservation ? null : await this.holdForDeployment(target, projectName, stackName, stackConfig, ecosystemConfig, release, phases);
|
|
3655
3818
|
if (lock)
|
|
3656
3819
|
locks.set(nodeKey, lock);
|
|
3657
3820
|
phases.enter(`provisioning ${node.host}`);
|
|
3658
|
-
const provisioned = await this.deployer.provisionSlaveNode(target, master.host, masterPort, projectName);
|
|
3821
|
+
const provisioned = preservation ? true : await this.deployer.provisionSlaveNode(target, master.host, masterPort, projectName);
|
|
3659
3822
|
if (!provisioned) {
|
|
3660
3823
|
skipped.push(nodeKey);
|
|
3661
3824
|
this.logger.error({ node: nodeKey }, 'Failed to provision slave — skipping');
|
|
@@ -3682,7 +3845,7 @@ export class ProjectService extends EventEmitter {
|
|
|
3682
3845
|
// Sending only the stack's own block is why a remote deployment had
|
|
3683
3846
|
// databases and no gateway, and no onion address to reach it by.
|
|
3684
3847
|
const nodeInfra = mergeInfrastructure(ecosystemConfig, stackConfig);
|
|
3685
|
-
if (nodeInfra || Object.keys(declaredServices).length > 0) {
|
|
3848
|
+
if (preservation || nodeInfra || Object.keys(declaredServices).length > 0) {
|
|
3686
3849
|
await leases.confirm(nodeKey, `bringing up infrastructure on ${node.host}`);
|
|
3687
3850
|
phases.enter(`bringing up infrastructure on ${node.host}`);
|
|
3688
3851
|
const infrastructure = await this.provisionNodeInfrastructure(connector, node, (nodeInfra ?? {}), declaredServices,
|
|
@@ -3694,10 +3857,11 @@ export class ProjectService extends EventEmitter {
|
|
|
3694
3857
|
project: projectName,
|
|
3695
3858
|
stack: stackName,
|
|
3696
3859
|
overrides: await this.resolveOverrideSecrets(projectName, stackConfig.serviceOverrides),
|
|
3697
|
-
}, project?.path, release?.staticsDir ?? undefined)
|
|
3860
|
+
}, project?.path, release?.staticsDir ?? undefined, preservation ? { mode: 'preserve', review: preservation.targets.find(preservedTarget => preservedTarget.node === nodeKey)?.infrastructure?.digest ?? undefined }
|
|
3861
|
+
: reconcile ? { mode: 'reconcile' } : undefined);
|
|
3698
3862
|
if (!infrastructure.ready) {
|
|
3699
|
-
if (release) {
|
|
3700
|
-
throw new Error(`
|
|
3863
|
+
if (release || preservation) {
|
|
3864
|
+
throw new Error(`Deployment stopped before application installation on ${nodeKey}: ${infrastructure.detail}`);
|
|
3701
3865
|
}
|
|
3702
3866
|
// Not fatal: a node whose infrastructure is incomplete can still
|
|
3703
3867
|
// be looked at, and stopping here would leave the fleet in a state
|
|
@@ -3763,7 +3927,7 @@ export class ProjectService extends EventEmitter {
|
|
|
3763
3927
|
const results = await this.deployer.deployToStack([target], nodeArtifacts, projectName, {
|
|
3764
3928
|
apps: nodeApps,
|
|
3765
3929
|
appEnv,
|
|
3766
|
-
migrations: migrationRequests.filter(request => selectedNames.has(request.app)),
|
|
3930
|
+
migrations: preservation ? [] : migrationRequests.filter(request => selectedNames.has(request.app)),
|
|
3767
3931
|
stack: stackName,
|
|
3768
3932
|
// Past the node's record of what each app runs: ship and restart all.
|
|
3769
3933
|
...(reinstall ? { force: true } : {}),
|
|
@@ -4183,7 +4347,7 @@ export class ProjectService extends EventEmitter {
|
|
|
4183
4347
|
/** Filled with each service whose directory was sent → that directory's absolute path here. */
|
|
4184
4348
|
shippedDirs,
|
|
4185
4349
|
/** The services the stack's apps require — each may name a directory of its own, as the stack's may. */
|
|
4186
|
-
services) {
|
|
4350
|
+
services, strict = false) {
|
|
4187
4351
|
const { readConfigDirectory } = await import('../infrastructure/config-payload.js');
|
|
4188
4352
|
const { shippedDirsOf } = await import('../infrastructure/shipped-config.js');
|
|
4189
4353
|
const fsp = await import('node:fs/promises');
|
|
@@ -4206,6 +4370,8 @@ export class ProjectService extends EventEmitter {
|
|
|
4206
4370
|
shippedDirs?.set(name, abs);
|
|
4207
4371
|
}
|
|
4208
4372
|
if (skipped.length > 0) {
|
|
4373
|
+
if (strict)
|
|
4374
|
+
throw Error('Complete service configuration is required for preservation');
|
|
4209
4375
|
// Said out loud: a file left behind changes what the service does,
|
|
4210
4376
|
// and silence here would make the node's behaviour unexplainable
|
|
4211
4377
|
// from the master.
|
|
@@ -4213,6 +4379,8 @@ export class ProjectService extends EventEmitter {
|
|
|
4213
4379
|
}
|
|
4214
4380
|
}
|
|
4215
4381
|
catch (err) {
|
|
4382
|
+
if (strict)
|
|
4383
|
+
throw Error('Service configuration cannot be verified for preservation', { cause: err });
|
|
4216
4384
|
// Not fatal. A stack whose gateway config is missing still gets its
|
|
4217
4385
|
// databases, and the gateway fails visibly rather than the whole
|
|
4218
4386
|
// provisioning stopping before anything was done.
|
|
@@ -4221,6 +4389,12 @@ export class ProjectService extends EventEmitter {
|
|
|
4221
4389
|
}
|
|
4222
4390
|
return payload;
|
|
4223
4391
|
}
|
|
4392
|
+
async reviewNodeInfrastructure(node, infrastructure, services, owner, projectRoot, releasedStatics) {
|
|
4393
|
+
const result = await this.provisionNodeInfrastructure(this.getSlaveConnector(), node, infrastructure, services, owner, projectRoot, releasedStatics, { mode: 'review' });
|
|
4394
|
+
if (!result.preservation || result.preservation.protocol !== 1 || result.preservation.mode !== 'preserve')
|
|
4395
|
+
throw Error('Recipient does not confirm infrastructure preservation capability; update or inspect the node before recovery');
|
|
4396
|
+
return result.preservation;
|
|
4397
|
+
}
|
|
4224
4398
|
async provisionNodeInfrastructure(connector, node, infrastructure, services, owner,
|
|
4225
4399
|
/**
|
|
4226
4400
|
* This project's root on the MASTER, for reading the config files a
|
|
@@ -4235,7 +4409,7 @@ export class ProjectService extends EventEmitter {
|
|
|
4235
4409
|
*/
|
|
4236
4410
|
projectRoot,
|
|
4237
4411
|
/** A release's static bundle, shipped as it was built — see `shipStackStatics`. */
|
|
4238
|
-
releasedStatics) {
|
|
4412
|
+
releasedStatics, authority) {
|
|
4239
4413
|
const host = node.host;
|
|
4240
4414
|
const port = node.port ?? 9700;
|
|
4241
4415
|
await connector.addSlave({ host, port, label: node.label });
|
|
@@ -4248,7 +4422,8 @@ export class ProjectService extends EventEmitter {
|
|
|
4248
4422
|
}
|
|
4249
4423
|
try {
|
|
4250
4424
|
const shippedDirs = new Map();
|
|
4251
|
-
const
|
|
4425
|
+
const preserving = authority?.mode === 'review' || authority?.mode === 'preserve';
|
|
4426
|
+
const configFiles = projectRoot ? await this.readStackConfigFiles(infrastructure, projectRoot, shippedDirs, services, preserving) : {};
|
|
4252
4427
|
// A service's bind mounts inside its shipped directory name the node's
|
|
4253
4428
|
// copy, not this machine's path — the stack's own services and the ones
|
|
4254
4429
|
// its apps require alike. See `shipped-config.ts`.
|
|
@@ -4261,16 +4436,31 @@ export class ProjectService extends EventEmitter {
|
|
|
4261
4436
|
// by SSH rather than inside the call: 32 MB is the wrong size for an
|
|
4262
4437
|
// RPC argument, which is held whole on both sides and blocks the call
|
|
4263
4438
|
// it rides on. Config files are 65 KB and ride along; a build does not.
|
|
4264
|
-
const shipped = projectRoot
|
|
4439
|
+
const shipped = projectRoot && !preserving
|
|
4265
4440
|
? await this.shipStackStatics(infrastructure, projectRoot, node, releasedStatics, owner)
|
|
4266
4441
|
: { dirs: {}, roots: {} };
|
|
4267
4442
|
const staticRoots = shipped.dirs;
|
|
4268
4443
|
const staticLinkRoots = shipped.roots;
|
|
4444
|
+
const staticDigests = {};
|
|
4445
|
+
if (preserving && projectRoot && this.staticDirOf(infrastructure, projectRoot)) {
|
|
4446
|
+
const { releaseTreeDigest } = await import('../release/runtime-provenance.js');
|
|
4447
|
+
staticDigests['gateway'] = await releaseTreeDigest(releasedStatics ?? this.staticDirOf(infrastructure, projectRoot), true);
|
|
4448
|
+
}
|
|
4269
4449
|
// Retry once if the connection turns out to be gone: the deployer just
|
|
4270
4450
|
// restarted this node's daemon, so the mesh connection the master holds
|
|
4271
4451
|
// was established to the process that exited. `provisionStack` is a
|
|
4272
4452
|
// reconciler, so applying it twice is applying it once.
|
|
4273
|
-
const report = (await connector.invokeOnSlave(host, port, 'OmnitronInfra', 'provisionStack', [{ config: configForNode, services: servicesForNode, ...(owner ?? {}), configFiles, staticRoots, staticLinkRoots
|
|
4453
|
+
const report = (await connector.invokeOnSlave(host, port, 'OmnitronInfra', authority?.mode === 'review' ? 'reviewStack' : 'provisionStack', [{ config: configForNode, services: servicesForNode, ...(owner ?? {}), configFiles, staticRoots, staticLinkRoots,
|
|
4454
|
+
...(preserving ? { infrastructure: 'preserve', staticDigests, ...(authority?.review && { review: authority.review }) }
|
|
4455
|
+
: authority?.mode === 'reconcile' ? { infrastructure: 'reconcile' } : {}) }], { retryOnDisconnect: true }));
|
|
4456
|
+
const preservation = authority?.mode === 'review' ? report : report?.preservation;
|
|
4457
|
+
if (preserving) {
|
|
4458
|
+
if (preservation?.protocol !== 1 || preservation.mode !== 'preserve' || !Array.isArray(preservation.checks) || !Array.isArray(preservation.containers))
|
|
4459
|
+
throw Error('Recipient does not implement the preservation protocol');
|
|
4460
|
+
if (authority?.mode === 'preserve' && (!preservation.digest || preservation.digest !== authority.review))
|
|
4461
|
+
throw Error('Recipient did not confirm the reviewed infrastructure identity');
|
|
4462
|
+
return { ready: preservation.digest !== null, detail: 'Native infrastructure preservation checked', preservation };
|
|
4463
|
+
}
|
|
4274
4464
|
this.logger.info({
|
|
4275
4465
|
node: `${host}:${port}`,
|
|
4276
4466
|
ready: report?.ready ?? false,
|
|
@@ -4287,6 +4477,8 @@ export class ProjectService extends EventEmitter {
|
|
|
4287
4477
|
// touched, and going on would put new backends behind the old frontend.
|
|
4288
4478
|
if (err instanceof FrontendNotDeliveredError)
|
|
4289
4479
|
throw err;
|
|
4480
|
+
if (authority?.mode === 'review' || authority?.mode === 'preserve')
|
|
4481
|
+
return { ready: false, detail: 'Recipient preservation capability or complete native identity could not be verified; no provisioning fallback is permitted' };
|
|
4290
4482
|
this.logger.error({ node: `${host}:${port}`, error: err.message }, 'Could not bring up this node\'s infrastructure');
|
|
4291
4483
|
return { ready: false, detail: `could not ask the node: ${err.message}` };
|
|
4292
4484
|
}
|
|
@@ -4450,7 +4642,7 @@ export class ProjectService extends EventEmitter {
|
|
|
4450
4642
|
if (entry.bootstrap && project) {
|
|
4451
4643
|
try {
|
|
4452
4644
|
const bootstrapAbsPath = resolveAppPath(project.path, entry, entry.bootstrap);
|
|
4453
|
-
const { definition: loaded, metadata } = await readBootstrapMetadata(bootstrapAbsPath);
|
|
4645
|
+
const { definition: loaded, metadata } = await readBootstrapMetadata(bootstrapAbsPath, false, 30_000, entry.releaseRoot);
|
|
4454
4646
|
this.bootstrapMetadata.save(metadata);
|
|
4455
4647
|
// Preserve only declarative data across backend handoffs, never executable hooks.
|
|
4456
4648
|
this.bootstrapTopology.set(bootstrapAbsPath, structuredClone({ ...(loaded.requires ? { requires: loaded.requires } : {}), ...(loaded.omnitronConfig ? { omnitronConfig: loaded.omnitronConfig } : {}) }));
|
|
@@ -4517,37 +4709,34 @@ export class ProjectService extends EventEmitter {
|
|
|
4517
4709
|
throw new Error('Project is not registered');
|
|
4518
4710
|
return new LocalReleaseSelection(this.daemonStateStore, { project, stack }, registration.registeredAt);
|
|
4519
4711
|
}
|
|
4520
|
-
async installedEntries(project, stack, definition, config, release, prepare = false,
|
|
4521
|
-
return (await this.installedRelease(project, stack, definition, config, release, prepare,
|
|
4712
|
+
async installedEntries(project, stack, definition, config, release, prepare = false, requiredApps, requiredFiles) {
|
|
4713
|
+
return (await this.installedRelease(project, stack, definition, config, release, prepare, requiredApps, requiredFiles)).entries;
|
|
4522
4714
|
}
|
|
4523
|
-
async installedRelease(project, stack, definition, config, release, prepare = false,
|
|
4715
|
+
async installedRelease(project, stack, definition, config, release, prepare = false, requiredApps, requiredFiles, admittedOwner) {
|
|
4524
4716
|
const root = this.registry.get(project)?.path;
|
|
4525
4717
|
if (!root)
|
|
4526
4718
|
throw new Error('Project is not registered');
|
|
4527
4719
|
const { inspectLocalReleaseInstallation, inspectLocalReleasePayloads } = await import('../release/local-installation.js');
|
|
4528
4720
|
const stackEntries = this.resolveStackApps(definition, config);
|
|
4529
4721
|
// Validate the complete stack before narrowing executable verification to
|
|
4530
|
-
//
|
|
4722
|
+
// requested executable owners. Other applications still own their declared environment.
|
|
4531
4723
|
const env = await this.stackEnvFor(project, stack, definition, stackEntries.map(entry => entry.name));
|
|
4532
|
-
const configured = stackEntries.filter(entry => !
|
|
4724
|
+
const configured = stackEntries.filter(entry => !requiredApps || requiredApps.includes(entry.name));
|
|
4533
4725
|
const entries = configured.map(entry => ({
|
|
4534
4726
|
...entry, env: { ...entry.env, ...env[entry.name] },
|
|
4535
4727
|
...(!entry.releaseFrom || entry.cwd !== undefined ? { cwd: resolveAppPath(root, entry) } : {}),
|
|
4536
4728
|
...(entry.bootstrap && { bootstrap: resolveAppPath(root, entry, entry.bootstrap) }),
|
|
4537
4729
|
}));
|
|
4538
|
-
const
|
|
4539
|
-
|
|
4540
|
-
|
|
4541
|
-
const ownerPackage = await this.orchestrator.getExecutionOwnerPackage?.();
|
|
4542
|
-
if (!ownerPackage)
|
|
4543
|
-
throw new Error('Verified runtime origin requires an independently verified execution-owner package');
|
|
4730
|
+
const owner = admittedOwner ?? await this.orchestrator.getExecutionOwnerRuntime?.();
|
|
4731
|
+
assertResidentApplicationRuntime(owner);
|
|
4732
|
+
assertReleaseTarget(owner.target, owner.target);
|
|
4544
4733
|
const infrastructure = mergeInfrastructure(config, definition);
|
|
4545
4734
|
const payloads = artifactVolumeEntries(infrastructure, config.apps);
|
|
4546
4735
|
const roots = await inspectLocalReleasePayloads(release, payloads, prepare);
|
|
4547
4736
|
const resolved = await bindArtifactVolumes(infrastructure, roots);
|
|
4548
|
-
const installed = await inspectLocalReleaseInstallation(release, stack, entries, { prepare,
|
|
4549
|
-
|
|
4550
|
-
|
|
4737
|
+
const installed = await inspectLocalReleaseInstallation(release, stack, entries, { prepare,
|
|
4738
|
+
ownerPid: owner.execution.pid, ownerPackage: owner.root, runtime: owner.target,
|
|
4739
|
+
...(requiredApps && { requiredFiles: requiredFiles ?? Object.fromEntries(requiredApps.map(app => [app, [COMPILED_MIGRATION_ENTRY]])) }), });
|
|
4551
4740
|
const preparedArtifactVolumes = artifactVolumeBindings(infrastructure).map(([binding, value]) => {
|
|
4552
4741
|
const service = resolved?.services?.[value.service];
|
|
4553
4742
|
const mount = service && !('preset' in service) ? service.docker?.volumes?.[binding] : undefined;
|
|
@@ -4559,6 +4748,84 @@ export class ProjectService extends EventEmitter {
|
|
|
4559
4748
|
});
|
|
4560
4749
|
return { entries: installed, infrastructure: resolved, preparedArtifactVolumes };
|
|
4561
4750
|
}
|
|
4751
|
+
/** Read-only admission for an operation in an already selected, prepared release.
|
|
4752
|
+
* The returned fence repeats admission before each Xec attempt; it cannot install,
|
|
4753
|
+
* select a release, run setup, or silently adopt a different owner/registration. */
|
|
4754
|
+
async resolvePipelineRelease(target, options) {
|
|
4755
|
+
const { readInstalledReleaseReceipt } = await import('../release/installed-origin.js');
|
|
4756
|
+
const { frozenMember } = await import('../release/frozen-installation.js');
|
|
4757
|
+
const inspect = async () => {
|
|
4758
|
+
assertReleaseLoaderEnvironment(process.env);
|
|
4759
|
+
assertReleaseLoaderEnvironment(options.env);
|
|
4760
|
+
const scope = await this.resolveProjectStack({ project: target.project, stack: target.stack }, { readOnly: true });
|
|
4761
|
+
this.assertStackMutable(scope);
|
|
4762
|
+
const registered = this.registry.get(scope.project);
|
|
4763
|
+
const config = this.getLoadedConfig(scope.project);
|
|
4764
|
+
if (!registered || !config)
|
|
4765
|
+
throw Error('Release pipeline target requires a registered, loaded project');
|
|
4766
|
+
const registration = structuredClone(registered);
|
|
4767
|
+
const definition = this.resolveStacks(config, scope.project)[scope.stack];
|
|
4768
|
+
if (!definition || definition.type !== 'local' || definition.managedOrigin)
|
|
4769
|
+
throw Error('Release pipeline target requires an independently owned local stack; remote execution is unsupported');
|
|
4770
|
+
const fingerprint = () => createHash('sha256').update(JSON.stringify({
|
|
4771
|
+
definition: this.resolveStacks(this.getLoadedConfig(scope.project), scope.project)[scope.stack],
|
|
4772
|
+
config: this.getLoadedConfig(scope.project),
|
|
4773
|
+
})).digest('hex');
|
|
4774
|
+
const definitionFingerprint = fingerprint();
|
|
4775
|
+
const selection = this.localSelection(scope.project, scope.stack).read();
|
|
4776
|
+
if (!selection.release)
|
|
4777
|
+
throw Error('Release pipeline target has no selected immutable release');
|
|
4778
|
+
const owner = await this.orchestrator.getExecutionOwnerRuntime?.();
|
|
4779
|
+
assertResidentApplicationRuntime(owner);
|
|
4780
|
+
assertReleaseTarget(owner.target, owner.target);
|
|
4781
|
+
if (!owner.execution?.lifetime)
|
|
4782
|
+
throw Error('Release pipeline execution requires native owner lifetime evidence');
|
|
4783
|
+
const release = await this.admitRelease(scope.project, scope.stack, definition, config, selection.release.id, 'check');
|
|
4784
|
+
if (release.manifestSha256 !== selection.release.manifestSha256)
|
|
4785
|
+
throw Error('Selected release manifest changed on disk');
|
|
4786
|
+
const source = releaseArtifactOrigins(release.manifest, release.manifestSha256).find(item => item.app === target.app);
|
|
4787
|
+
const origin = source?.origin;
|
|
4788
|
+
const artifact = origin?.runtimeOrigin && { releaseId: origin.releaseId, manifestSha256: origin.manifestSha256,
|
|
4789
|
+
artifactSha256: origin.artifactSha256, producerApp: origin.runtimeOrigin.producerApp,
|
|
4790
|
+
projectCommit: origin.runtimeOrigin.projectCommit, frameworkCommit: origin.frameworkCommit };
|
|
4791
|
+
if (!artifact || !isDeepStrictEqual(artifact, target.artifact))
|
|
4792
|
+
throw Error('Selected release does not contain the pinned pipeline artifact');
|
|
4793
|
+
const installed = await this.installedRelease(scope.project, scope.stack, definition, config, release, false, [target.app], { [target.app]: [target.entry] }, owner);
|
|
4794
|
+
if (installed.entries.length !== 1 || !installed.entries[0]?.releaseRoot)
|
|
4795
|
+
throw Error('Pipeline application is absent from the selected stack');
|
|
4796
|
+
const entry = installed.entries[0], root = entry.releaseRoot;
|
|
4797
|
+
const file = await frozenMember(root, target.entry);
|
|
4798
|
+
const receipt = await readInstalledReleaseReceipt(root);
|
|
4799
|
+
const env = { ...Object.fromEntries(Object.entries(process.env).filter((item) => typeof item[1] === 'string')),
|
|
4800
|
+
...entry.env, ...options.env, NODE_OPTIONS: releaseOptionsForEnv(entry.env), NODE_PATH: '',
|
|
4801
|
+
PATH: path.dirname(owner.executable) + path.delimiter + '/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin' };
|
|
4802
|
+
const cwd = options.cwd ?? entry.cwd ?? root;
|
|
4803
|
+
if (!path.isAbsolute(cwd) || await fs.promises.realpath(cwd) !== cwd || !(await fs.promises.stat(cwd)).isDirectory())
|
|
4804
|
+
throw Error('A release pipeline working directory must be an absolute physical directory');
|
|
4805
|
+
const command = { command: owner.executable, args: [file, ...options.args ?? []], shell: false,
|
|
4806
|
+
cwd,
|
|
4807
|
+
env: withClosureLibraryPath(env, { verifiedRoot: root }, {}) ?? env };
|
|
4808
|
+
const evidence = { artifact,
|
|
4809
|
+
consumer: origin.runtimeOrigin.consumer,
|
|
4810
|
+
installationRoot: root, treeSha256: receipt.treeSha256, entry: file, executable: owner.executable, cwd,
|
|
4811
|
+
environmentSha256: createHash('sha256').update(JSON.stringify(Object.entries(command.env).sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0))).digest('hex'), target: owner.target,
|
|
4812
|
+
owner: { id: owner.execution.ownerId, pid: owner.execution.pid, epoch: owner.execution.epoch,
|
|
4813
|
+
lifetime: owner.execution.lifetime, frameworkCommit: owner.commit } };
|
|
4814
|
+
const observed = await this.orchestrator.getExecutionOwnerRuntime?.();
|
|
4815
|
+
assertSameApplicationRuntime(owner, observed);
|
|
4816
|
+
assertReleaseTarget(owner.target, observed?.target);
|
|
4817
|
+
this.assertStackMutable(scope);
|
|
4818
|
+
if (!isDeepStrictEqual(registration, this.registry.get(scope.project)) ||
|
|
4819
|
+
!isDeepStrictEqual(selection, this.localSelection(scope.project, scope.stack).read()) || fingerprint() !== definitionFingerprint)
|
|
4820
|
+
throw Error('Pipeline registration, configuration or release selection changed during admission');
|
|
4821
|
+
return { command, evidence, registration, selection, scope, definition: definitionFingerprint };
|
|
4822
|
+
};
|
|
4823
|
+
const admitted = await inspect();
|
|
4824
|
+
return { command: admitted.command, evidence: admitted.evidence, verify: async () => {
|
|
4825
|
+
if (!isDeepStrictEqual(admitted, await inspect()))
|
|
4826
|
+
throw Error('Pipeline release, application environment or execution owner changed after admission');
|
|
4827
|
+
} };
|
|
4828
|
+
}
|
|
4562
4829
|
/** All consumers of executable declarations must see the same physical entry as the owner. */
|
|
4563
4830
|
async launchEntries(project, stack, definition, config, requiredMigrations, migrationFiles) {
|
|
4564
4831
|
const selected = definition.type === 'local' && !definition.managedOrigin ? this.localSelection(project, stack).read().release : null;
|
|
@@ -4927,6 +5194,7 @@ export class ProjectService extends EventEmitter {
|
|
|
4927
5194
|
const selectedRelease = config.type === 'local' && !config.managedOrigin ? this.localSelection(projectName, stackName).read().release : null;
|
|
4928
5195
|
return {
|
|
4929
5196
|
name: stackName,
|
|
5197
|
+
infrastructureMode: this.infrastructurePolicy(projectName, stackName)?.mode ?? 'reconcile',
|
|
4930
5198
|
type: config.type,
|
|
4931
5199
|
...(selectedRelease && { selectedRelease }),
|
|
4932
5200
|
status,
|