@omnitron-dev/omnitron 0.2.0 → 0.3.1
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 +4 -0
- package/dist/BUILD.json +5 -0
- package/dist/cli/omnitron.js +375 -29
- package/dist/cli/omnitron.js.map +1 -1
- package/dist/cluster/cluster.rpc-service.d.ts +40 -0
- package/dist/cluster/cluster.rpc-service.d.ts.map +1 -1
- package/dist/cluster/cluster.rpc-service.js +49 -3
- package/dist/cluster/cluster.rpc-service.js.map +1 -1
- package/dist/cluster/config-sync.d.ts.map +1 -1
- package/dist/cluster/config-sync.js.map +1 -1
- package/dist/cluster/leader-election.d.ts +90 -2
- package/dist/cluster/leader-election.d.ts.map +1 -1
- package/dist/cluster/leader-election.js +176 -22
- package/dist/cluster/leader-election.js.map +1 -1
- package/dist/cluster/mesh-link.d.ts +136 -0
- package/dist/cluster/mesh-link.d.ts.map +1 -0
- package/dist/cluster/mesh-link.js +212 -0
- package/dist/cluster/mesh-link.js.map +1 -0
- package/dist/cluster/mesh.d.ts +59 -0
- package/dist/cluster/mesh.d.ts.map +1 -0
- package/dist/cluster/mesh.js +117 -0
- package/dist/cluster/mesh.js.map +1 -0
- package/dist/cluster/slave-connector.d.ts +193 -9
- package/dist/cluster/slave-connector.d.ts.map +1 -1
- package/dist/cluster/slave-connector.js +409 -37
- package/dist/cluster/slave-connector.js.map +1 -1
- package/dist/commands/audit.d.ts +65 -0
- package/dist/commands/audit.d.ts.map +1 -0
- package/dist/commands/audit.js +233 -0
- package/dist/commands/audit.js.map +1 -0
- package/dist/commands/backup.d.ts +4 -0
- package/dist/commands/backup.d.ts.map +1 -1
- package/dist/commands/backup.js +255 -33
- package/dist/commands/backup.js.map +1 -1
- package/dist/commands/cluster.d.ts.map +1 -1
- package/dist/commands/cluster.js +78 -47
- package/dist/commands/cluster.js.map +1 -1
- package/dist/commands/daemon-cmd.d.ts +7 -1
- package/dist/commands/daemon-cmd.d.ts.map +1 -1
- package/dist/commands/daemon-cmd.js +87 -21
- package/dist/commands/daemon-cmd.js.map +1 -1
- package/dist/commands/daemon-required.d.ts +34 -0
- package/dist/commands/daemon-required.d.ts.map +1 -0
- package/dist/commands/daemon-required.js +92 -0
- package/dist/commands/daemon-required.js.map +1 -0
- package/dist/commands/deploy.d.ts +70 -8
- package/dist/commands/deploy.d.ts.map +1 -1
- package/dist/commands/deploy.js +99 -64
- package/dist/commands/deploy.js.map +1 -1
- package/dist/commands/discover.d.ts +25 -4
- package/dist/commands/discover.d.ts.map +1 -1
- package/dist/commands/discover.js +155 -102
- package/dist/commands/discover.js.map +1 -1
- package/dist/commands/doctor.d.ts +293 -0
- package/dist/commands/doctor.d.ts.map +1 -0
- package/dist/commands/doctor.js +1706 -0
- package/dist/commands/doctor.js.map +1 -0
- package/dist/commands/env.d.ts +8 -1
- package/dist/commands/env.d.ts.map +1 -1
- package/dist/commands/env.js +13 -8
- package/dist/commands/env.js.map +1 -1
- package/dist/commands/exec.d.ts.map +1 -1
- package/dist/commands/exec.js +3 -2
- package/dist/commands/exec.js.map +1 -1
- package/dist/commands/fleet-asking.d.ts +87 -0
- package/dist/commands/fleet-asking.d.ts.map +1 -0
- package/dist/commands/fleet-asking.js +176 -0
- package/dist/commands/fleet-asking.js.map +1 -0
- package/dist/commands/fleet-recover.d.ts +2 -0
- package/dist/commands/fleet-recover.d.ts.map +1 -0
- package/dist/commands/fleet-recover.js +30 -0
- package/dist/commands/fleet-recover.js.map +1 -0
- package/dist/commands/fleet.d.ts +18 -0
- package/dist/commands/fleet.d.ts.map +1 -1
- package/dist/commands/fleet.js +314 -74
- package/dist/commands/fleet.js.map +1 -1
- package/dist/commands/health-check.d.ts +8 -6
- package/dist/commands/health-check.d.ts.map +1 -1
- package/dist/commands/health-check.js +26 -23
- package/dist/commands/health-check.js.map +1 -1
- package/dist/commands/health.d.ts.map +1 -1
- package/dist/commands/health.js +22 -8
- package/dist/commands/health.js.map +1 -1
- package/dist/commands/host-ingress.d.ts +3 -0
- package/dist/commands/host-ingress.d.ts.map +1 -0
- package/dist/commands/host-ingress.js +31 -0
- package/dist/commands/host-ingress.js.map +1 -0
- package/dist/commands/infra.d.ts +42 -1
- package/dist/commands/infra.d.ts.map +1 -1
- package/dist/commands/infra.js +418 -49
- package/dist/commands/infra.js.map +1 -1
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +21 -36
- package/dist/commands/init.js.map +1 -1
- package/dist/commands/inspect-graph.d.ts.map +1 -1
- package/dist/commands/inspect-graph.js +2 -6
- package/dist/commands/inspect-graph.js.map +1 -1
- package/dist/commands/inspect.d.ts.map +1 -1
- package/dist/commands/inspect.js +110 -14
- package/dist/commands/inspect.js.map +1 -1
- package/dist/commands/k8s.d.ts.map +1 -1
- package/dist/commands/k8s.js +23 -18
- package/dist/commands/k8s.js.map +1 -1
- package/dist/commands/kb.d.ts.map +1 -1
- package/dist/commands/kb.js +88 -29
- package/dist/commands/kb.js.map +1 -1
- package/dist/commands/list.d.ts.map +1 -1
- package/dist/commands/list.js +21 -9
- package/dist/commands/list.js.map +1 -1
- package/dist/commands/log-health.d.ts +120 -0
- package/dist/commands/log-health.d.ts.map +1 -0
- package/dist/commands/log-health.js +212 -0
- package/dist/commands/log-health.js.map +1 -0
- package/dist/commands/logs.d.ts +96 -2
- package/dist/commands/logs.d.ts.map +1 -1
- package/dist/commands/logs.js +522 -196
- package/dist/commands/logs.js.map +1 -1
- package/dist/commands/metrics.d.ts.map +1 -1
- package/dist/commands/metrics.js +35 -17
- package/dist/commands/metrics.js.map +1 -1
- package/dist/commands/monit.d.ts.map +1 -1
- package/dist/commands/monit.js +3 -2
- package/dist/commands/monit.js.map +1 -1
- package/dist/commands/node.d.ts +56 -0
- package/dist/commands/node.d.ts.map +1 -1
- package/dist/commands/node.js +297 -69
- package/dist/commands/node.js.map +1 -1
- package/dist/commands/output.d.ts +10 -0
- package/dist/commands/output.d.ts.map +1 -1
- package/dist/commands/output.js +41 -1
- package/dist/commands/output.js.map +1 -1
- package/dist/commands/pipeline.d.ts.map +1 -1
- package/dist/commands/pipeline.js +33 -19
- package/dist/commands/pipeline.js.map +1 -1
- package/dist/commands/project.d.ts +19 -0
- package/dist/commands/project.d.ts.map +1 -1
- package/dist/commands/project.js +125 -27
- package/dist/commands/project.js.map +1 -1
- package/dist/commands/release-store.d.ts +28 -0
- package/dist/commands/release-store.d.ts.map +1 -0
- package/dist/commands/release-store.js +158 -0
- package/dist/commands/release-store.js.map +1 -0
- package/dist/commands/release.d.ts +61 -0
- package/dist/commands/release.d.ts.map +1 -0
- package/dist/commands/release.js +555 -0
- package/dist/commands/release.js.map +1 -0
- package/dist/commands/reload.d.ts +16 -0
- package/dist/commands/reload.d.ts.map +1 -1
- package/dist/commands/reload.js +21 -5
- package/dist/commands/reload.js.map +1 -1
- package/dist/commands/remote.d.ts +40 -0
- package/dist/commands/remote.d.ts.map +1 -1
- package/dist/commands/remote.js +146 -35
- package/dist/commands/remote.js.map +1 -1
- package/dist/commands/restart.d.ts +16 -0
- package/dist/commands/restart.d.ts.map +1 -1
- package/dist/commands/restart.js +21 -5
- package/dist/commands/restart.js.map +1 -1
- package/dist/commands/scale.d.ts.map +1 -1
- package/dist/commands/scale.js +4 -3
- package/dist/commands/scale.js.map +1 -1
- package/dist/commands/secret.d.ts +54 -0
- package/dist/commands/secret.d.ts.map +1 -1
- package/dist/commands/secret.js +236 -34
- package/dist/commands/secret.js.map +1 -1
- package/dist/commands/service-units.d.ts +181 -0
- package/dist/commands/service-units.d.ts.map +1 -0
- package/dist/commands/service-units.js +291 -0
- package/dist/commands/service-units.js.map +1 -0
- package/dist/commands/service.d.ts +39 -0
- package/dist/commands/service.d.ts.map +1 -0
- package/dist/commands/service.js +370 -0
- package/dist/commands/service.js.map +1 -0
- package/dist/commands/spinner.d.ts +29 -0
- package/dist/commands/spinner.d.ts.map +1 -0
- package/dist/commands/spinner.js +44 -0
- package/dist/commands/spinner.js.map +1 -0
- package/dist/commands/stack.d.ts +49 -1
- package/dist/commands/stack.d.ts.map +1 -1
- package/dist/commands/stack.js +325 -12
- package/dist/commands/stack.js.map +1 -1
- package/dist/commands/start.d.ts +16 -0
- package/dist/commands/start.d.ts.map +1 -1
- package/dist/commands/start.js +20 -3
- package/dist/commands/start.js.map +1 -1
- package/dist/commands/status.d.ts +9 -0
- package/dist/commands/status.d.ts.map +1 -1
- package/dist/commands/status.js +45 -29
- package/dist/commands/status.js.map +1 -1
- package/dist/commands/stop.d.ts +16 -0
- package/dist/commands/stop.d.ts.map +1 -1
- package/dist/commands/stop.js +34 -6
- package/dist/commands/stop.js.map +1 -1
- package/dist/commands/tor.d.ts +14 -7
- package/dist/commands/tor.d.ts.map +1 -1
- package/dist/commands/tor.js +113 -33
- package/dist/commands/tor.js.map +1 -1
- package/dist/commands/up.d.ts +85 -1
- package/dist/commands/up.d.ts.map +1 -1
- package/dist/commands/up.js +180 -32
- package/dist/commands/up.js.map +1 -1
- package/dist/commands/webapp.d.ts.map +1 -1
- package/dist/commands/webapp.js +2 -1
- package/dist/commands/webapp.js.map +1 -1
- package/dist/config/defaults.d.ts +16 -0
- package/dist/config/defaults.d.ts.map +1 -1
- package/dist/config/defaults.js +26 -3
- package/dist/config/defaults.js.map +1 -1
- package/dist/config/define-ecosystem.d.ts.map +1 -1
- package/dist/config/define-ecosystem.js +2 -0
- package/dist/config/define-ecosystem.js.map +1 -1
- package/dist/config/index.d.ts +1 -1
- package/dist/config/index.d.ts.map +1 -1
- package/dist/config/index.js +1 -1
- package/dist/config/index.js.map +1 -1
- package/dist/config/loader.d.ts +45 -1
- package/dist/config/loader.d.ts.map +1 -1
- package/dist/config/loader.js +64 -6
- package/dist/config/loader.js.map +1 -1
- package/dist/config/omnitron-config.schema.d.ts.map +1 -1
- package/dist/config/omnitron-config.schema.js +16 -0
- package/dist/config/omnitron-config.schema.js.map +1 -1
- package/dist/config/types.d.ts +416 -44
- package/dist/config/types.d.ts.map +1 -1
- package/dist/daemon/config-reload.d.ts +27 -0
- package/dist/daemon/config-reload.d.ts.map +1 -0
- package/dist/daemon/config-reload.js +42 -0
- package/dist/daemon/config-reload.js.map +1 -0
- package/dist/daemon/daemon-client.d.ts +136 -2
- package/dist/daemon/daemon-client.d.ts.map +1 -1
- package/dist/daemon/daemon-client.js +307 -21
- package/dist/daemon/daemon-client.js.map +1 -1
- package/dist/daemon/daemon-entry.js +90 -30
- package/dist/daemon/daemon-entry.js.map +1 -1
- package/dist/daemon/daemon-scheduler.d.ts +4 -0
- package/dist/daemon/daemon-scheduler.d.ts.map +1 -1
- package/dist/daemon/daemon-scheduler.js +137 -7
- package/dist/daemon/daemon-scheduler.js.map +1 -1
- package/dist/daemon/daemon-state-store.service.d.ts +263 -0
- package/dist/daemon/daemon-state-store.service.d.ts.map +1 -0
- package/dist/daemon/daemon-state-store.service.js +446 -0
- package/dist/daemon/daemon-state-store.service.js.map +1 -0
- package/dist/daemon/daemon.d.ts +194 -4
- package/dist/daemon/daemon.d.ts.map +1 -1
- package/dist/daemon/daemon.js +1160 -157
- package/dist/daemon/daemon.js.map +1 -1
- package/dist/daemon/daemon.module.d.ts.map +1 -1
- package/dist/daemon/daemon.module.js +163 -85
- package/dist/daemon/daemon.module.js.map +1 -1
- package/dist/daemon/daemon.rpc-service.d.ts +90 -1
- package/dist/daemon/daemon.rpc-service.d.ts.map +1 -1
- package/dist/daemon/daemon.rpc-service.js +439 -97
- package/dist/daemon/daemon.rpc-service.js.map +1 -1
- package/dist/daemon/pid-manager.d.ts +16 -3
- package/dist/daemon/pid-manager.d.ts.map +1 -1
- package/dist/daemon/pid-manager.js +31 -4
- package/dist/daemon/pid-manager.js.map +1 -1
- package/dist/daemon/spawn-daemon.d.ts +73 -0
- package/dist/daemon/spawn-daemon.d.ts.map +1 -0
- package/dist/daemon/spawn-daemon.js +101 -0
- package/dist/daemon/spawn-daemon.js.map +1 -0
- package/dist/daemon/state-store.d.ts +65 -3
- package/dist/daemon/state-store.d.ts.map +1 -1
- package/dist/daemon/state-store.js +100 -58
- package/dist/daemon/state-store.js.map +1 -1
- package/dist/database/connection.d.ts +71 -0
- package/dist/database/connection.d.ts.map +1 -0
- package/dist/database/connection.js +129 -0
- package/dist/database/connection.js.map +1 -0
- package/dist/database/dialect.d.ts +60 -0
- package/dist/database/dialect.d.ts.map +1 -0
- package/dist/database/dialect.js +80 -0
- package/dist/database/dialect.js.map +1 -0
- package/dist/database/migrate.d.ts +5 -2
- package/dist/database/migrate.d.ts.map +1 -1
- package/dist/database/migrate.js +23 -33
- package/dist/database/migrate.js.map +1 -1
- package/dist/database/migration-runner.d.ts +50 -0
- package/dist/database/migration-runner.d.ts.map +1 -0
- package/dist/database/migration-runner.js +143 -0
- package/dist/database/migration-runner.js.map +1 -0
- package/dist/database/migrations/006_login_throttle.d.ts +17 -0
- package/dist/database/migrations/006_login_throttle.d.ts.map +1 -0
- package/dist/database/migrations/006_login_throttle.js +33 -0
- package/dist/database/migrations/006_login_throttle.js.map +1 -0
- package/dist/database/migrations/007_sync_ingested.d.ts +21 -0
- package/dist/database/migrations/007_sync_ingested.d.ts.map +1 -0
- package/dist/database/migrations/007_sync_ingested.js +39 -0
- package/dist/database/migrations/007_sync_ingested.js.map +1 -0
- package/dist/database/migrations/008_node_health_version_text.d.ts +24 -0
- package/dist/database/migrations/008_node_health_version_text.d.ts.map +1 -0
- package/dist/database/migrations/008_node_health_version_text.js +35 -0
- package/dist/database/migrations/008_node_health_version_text.js.map +1 -0
- package/dist/database/migrations/009_node_health_unmeasured.d.ts +40 -0
- package/dist/database/migrations/009_node_health_unmeasured.d.ts.map +1 -0
- package/dist/database/migrations/009_node_health_unmeasured.js +63 -0
- package/dist/database/migrations/009_node_health_unmeasured.js.map +1 -0
- package/dist/database/migrations/010_metrics_raw_had_no_reader.d.ts +22 -0
- package/dist/database/migrations/010_metrics_raw_had_no_reader.d.ts.map +1 -0
- package/dist/database/migrations/010_metrics_raw_had_no_reader.js +26 -0
- package/dist/database/migrations/010_metrics_raw_had_no_reader.js.map +1 -0
- package/dist/database/migrations/011_alerts_nobody_had_to_write.d.ts +56 -0
- package/dist/database/migrations/011_alerts_nobody_had_to_write.d.ts.map +1 -0
- package/dist/database/migrations/011_alerts_nobody_had_to_write.js +79 -0
- package/dist/database/migrations/011_alerts_nobody_had_to_write.js.map +1 -0
- package/dist/database/migrations/012_a_disk_nobody_watched.d.ts +26 -0
- package/dist/database/migrations/012_a_disk_nobody_watched.d.ts.map +1 -0
- package/dist/database/migrations/012_a_disk_nobody_watched.js +40 -0
- package/dist/database/migrations/012_a_disk_nobody_watched.js.map +1 -0
- package/dist/database/migrations/013_an_alarm_delivered.d.ts +14 -0
- package/dist/database/migrations/013_an_alarm_delivered.d.ts.map +1 -0
- package/dist/database/migrations/013_an_alarm_delivered.js +28 -0
- package/dist/database/migrations/013_an_alarm_delivered.js.map +1 -0
- package/dist/database/migrations/index.d.ts +21 -0
- package/dist/database/migrations/index.d.ts.map +1 -0
- package/dist/database/migrations/index.js +47 -0
- package/dist/database/migrations/index.js.map +1 -0
- package/dist/database/schema.d.ts +27 -11
- package/dist/database/schema.d.ts.map +1 -1
- package/dist/database/sqlite-date-binding.d.ts +71 -0
- package/dist/database/sqlite-date-binding.d.ts.map +1 -0
- package/dist/database/sqlite-date-binding.js +174 -0
- package/dist/database/sqlite-date-binding.js.map +1 -0
- package/dist/execution/data-channel.d.ts +48 -0
- package/dist/execution/data-channel.d.ts.map +1 -0
- package/dist/execution/data-channel.js +55 -0
- package/dist/execution/data-channel.js.map +1 -0
- package/dist/execution/execution.service.d.ts +146 -0
- package/dist/execution/execution.service.d.ts.map +1 -1
- package/dist/execution/execution.service.js +330 -10
- package/dist/execution/execution.service.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/infrastructure/bare-metal-plan.d.ts +208 -0
- package/dist/infrastructure/bare-metal-plan.d.ts.map +1 -0
- package/dist/infrastructure/bare-metal-plan.js +363 -0
- package/dist/infrastructure/bare-metal-plan.js.map +1 -0
- package/dist/infrastructure/bare-metal-runner.d.ts +65 -0
- package/dist/infrastructure/bare-metal-runner.d.ts.map +1 -0
- package/dist/infrastructure/bare-metal-runner.js +354 -0
- package/dist/infrastructure/bare-metal-runner.js.map +1 -0
- package/dist/infrastructure/config-payload.d.ts +149 -0
- package/dist/infrastructure/config-payload.d.ts.map +1 -0
- package/dist/infrastructure/config-payload.js +235 -0
- package/dist/infrastructure/config-payload.js.map +1 -0
- package/dist/infrastructure/container-runtime.d.ts +170 -10
- package/dist/infrastructure/container-runtime.d.ts.map +1 -1
- package/dist/infrastructure/container-runtime.js +565 -52
- package/dist/infrastructure/container-runtime.js.map +1 -1
- package/dist/infrastructure/gateway-reachability.d.ts +85 -0
- package/dist/infrastructure/gateway-reachability.d.ts.map +1 -0
- package/dist/infrastructure/gateway-reachability.js +116 -0
- package/dist/infrastructure/gateway-reachability.js.map +1 -0
- package/dist/infrastructure/gateway-redis-db.d.ts +29 -0
- package/dist/infrastructure/gateway-redis-db.d.ts.map +1 -0
- package/dist/infrastructure/gateway-redis-db.js +32 -0
- package/dist/infrastructure/gateway-redis-db.js.map +1 -0
- package/dist/infrastructure/host-ingress-config.d.ts +12 -0
- package/dist/infrastructure/host-ingress-config.d.ts.map +1 -0
- package/dist/infrastructure/host-ingress-config.js +197 -0
- package/dist/infrastructure/host-ingress-config.js.map +1 -0
- package/dist/infrastructure/host-ingress-lock.d.ts +4 -0
- package/dist/infrastructure/host-ingress-lock.d.ts.map +1 -0
- package/dist/infrastructure/host-ingress-lock.js +36 -0
- package/dist/infrastructure/host-ingress-lock.js.map +1 -0
- package/dist/infrastructure/host-ingress-runtime.d.ts +25 -0
- package/dist/infrastructure/host-ingress-runtime.d.ts.map +1 -0
- package/dist/infrastructure/host-ingress-runtime.js +114 -0
- package/dist/infrastructure/host-ingress-runtime.js.map +1 -0
- package/dist/infrastructure/host-ingress-takeover.d.ts +18 -0
- package/dist/infrastructure/host-ingress-takeover.d.ts.map +1 -0
- package/dist/infrastructure/host-ingress-takeover.js +142 -0
- package/dist/infrastructure/host-ingress-takeover.js.map +1 -0
- package/dist/infrastructure/host-ingress-types.d.ts +173 -0
- package/dist/infrastructure/host-ingress-types.d.ts.map +1 -0
- package/dist/infrastructure/host-ingress-types.js +2 -0
- package/dist/infrastructure/host-ingress-types.js.map +1 -0
- package/dist/infrastructure/host-ingress.service.d.ts +47 -0
- package/dist/infrastructure/host-ingress.service.d.ts.map +1 -0
- package/dist/infrastructure/host-ingress.service.js +514 -0
- package/dist/infrastructure/host-ingress.service.js.map +1 -0
- package/dist/infrastructure/host-inspection.d.ts +223 -0
- package/dist/infrastructure/host-inspection.d.ts.map +1 -0
- package/dist/infrastructure/host-inspection.js +501 -0
- package/dist/infrastructure/host-inspection.js.map +1 -0
- package/dist/infrastructure/infrastructure-gate.d.ts.map +1 -1
- package/dist/infrastructure/infrastructure.service.d.ts +223 -7
- package/dist/infrastructure/infrastructure.service.d.ts.map +1 -1
- package/dist/infrastructure/infrastructure.service.js +500 -93
- package/dist/infrastructure/infrastructure.service.js.map +1 -1
- package/dist/infrastructure/known-machines.d.ts +76 -0
- package/dist/infrastructure/known-machines.d.ts.map +1 -0
- package/dist/infrastructure/known-machines.js +94 -0
- package/dist/infrastructure/known-machines.js.map +1 -0
- package/dist/infrastructure/node-credentials.d.ts +45 -0
- package/dist/infrastructure/node-credentials.d.ts.map +1 -0
- package/dist/infrastructure/node-credentials.js +80 -0
- package/dist/infrastructure/node-credentials.js.map +1 -0
- package/dist/infrastructure/orphan-containers.d.ts +99 -0
- package/dist/infrastructure/orphan-containers.d.ts.map +1 -0
- package/dist/infrastructure/orphan-containers.js +106 -0
- package/dist/infrastructure/orphan-containers.js.map +1 -0
- package/dist/infrastructure/phantom-endpoint-janitor.d.ts +20 -0
- package/dist/infrastructure/phantom-endpoint-janitor.d.ts.map +1 -1
- package/dist/infrastructure/phantom-endpoint-janitor.js +56 -6
- package/dist/infrastructure/phantom-endpoint-janitor.js.map +1 -1
- package/dist/infrastructure/presets/gateway-openresty.d.ts.map +1 -1
- package/dist/infrastructure/presets/gateway-openresty.js +29 -3
- package/dist/infrastructure/presets/gateway-openresty.js.map +1 -1
- package/dist/infrastructure/presets/minio.d.ts.map +1 -1
- package/dist/infrastructure/presets/minio.js +36 -3
- package/dist/infrastructure/presets/minio.js.map +1 -1
- package/dist/infrastructure/presets/postgres.d.ts.map +1 -1
- package/dist/infrastructure/presets/postgres.js +69 -2
- package/dist/infrastructure/presets/postgres.js.map +1 -1
- package/dist/infrastructure/presets/redis.d.ts.map +1 -1
- package/dist/infrastructure/presets/redis.js +13 -0
- package/dist/infrastructure/presets/redis.js.map +1 -1
- package/dist/infrastructure/presets/registry.d.ts.map +1 -1
- package/dist/infrastructure/presets/registry.js +81 -12
- package/dist/infrastructure/presets/registry.js.map +1 -1
- package/dist/infrastructure/presets/tor.d.ts +38 -0
- package/dist/infrastructure/presets/tor.d.ts.map +1 -1
- package/dist/infrastructure/presets/tor.js +78 -69
- package/dist/infrastructure/presets/tor.js.map +1 -1
- package/dist/infrastructure/presets/torrc.d.ts +54 -0
- package/dist/infrastructure/presets/torrc.d.ts.map +1 -0
- package/dist/infrastructure/presets/torrc.js +153 -0
- package/dist/infrastructure/presets/torrc.js.map +1 -0
- package/dist/infrastructure/presets/types.d.ts +17 -0
- package/dist/infrastructure/presets/types.d.ts.map +1 -1
- package/dist/infrastructure/provisioning-outcome.d.ts +85 -0
- package/dist/infrastructure/provisioning-outcome.d.ts.map +1 -0
- package/dist/infrastructure/provisioning-outcome.js +122 -0
- package/dist/infrastructure/provisioning-outcome.js.map +1 -0
- package/dist/infrastructure/server-registry.d.ts +27 -0
- package/dist/infrastructure/server-registry.d.ts.map +1 -1
- package/dist/infrastructure/server-registry.js +60 -5
- package/dist/infrastructure/server-registry.js.map +1 -1
- package/dist/infrastructure/service-binding.d.ts +60 -0
- package/dist/infrastructure/service-binding.d.ts.map +1 -0
- package/dist/infrastructure/service-binding.js +90 -0
- package/dist/infrastructure/service-binding.js.map +1 -0
- package/dist/infrastructure/service-credentials.d.ts +105 -0
- package/dist/infrastructure/service-credentials.d.ts.map +1 -0
- package/dist/infrastructure/service-credentials.js +141 -0
- package/dist/infrastructure/service-credentials.js.map +1 -0
- package/dist/infrastructure/service-resolver.d.ts +166 -12
- package/dist/infrastructure/service-resolver.d.ts.map +1 -1
- package/dist/infrastructure/service-resolver.js +535 -82
- package/dist/infrastructure/service-resolver.js.map +1 -1
- package/dist/infrastructure/shipped-config.d.ts +43 -0
- package/dist/infrastructure/shipped-config.d.ts.map +1 -0
- package/dist/infrastructure/shipped-config.js +159 -0
- package/dist/infrastructure/shipped-config.js.map +1 -0
- package/dist/infrastructure/stack-infra-manager.d.ts +16 -1
- package/dist/infrastructure/stack-infra-manager.d.ts.map +1 -1
- package/dist/infrastructure/stack-infra-manager.js +81 -53
- package/dist/infrastructure/stack-infra-manager.js.map +1 -1
- package/dist/infrastructure/types.d.ts +263 -6
- package/dist/infrastructure/types.d.ts.map +1 -1
- package/dist/mcp/mcp-bridge.d.ts.map +1 -1
- package/dist/mcp/mcp-bridge.js.map +1 -1
- package/dist/mcp/tool-groups/apps.tools.d.ts +2 -1
- package/dist/mcp/tool-groups/apps.tools.d.ts.map +1 -1
- package/dist/mcp/tool-groups/apps.tools.js +2 -2
- package/dist/mcp/tool-groups/apps.tools.js.map +1 -1
- package/dist/mcp/tool-groups/infra.tools.d.ts +16 -2
- package/dist/mcp/tool-groups/infra.tools.d.ts.map +1 -1
- package/dist/mcp/tool-groups/infra.tools.js +51 -41
- package/dist/mcp/tool-groups/infra.tools.js.map +1 -1
- package/dist/mcp/tool-groups/kb.tools.d.ts.map +1 -1
- package/dist/mcp/tool-groups/kb.tools.js +4 -6
- package/dist/mcp/tool-groups/kb.tools.js.map +1 -1
- package/dist/mcp/tool-groups/management.tools.d.ts +20 -3
- package/dist/mcp/tool-groups/management.tools.d.ts.map +1 -1
- package/dist/mcp/tool-groups/management.tools.js +158 -125
- package/dist/mcp/tool-groups/management.tools.js.map +1 -1
- package/dist/mcp/tool-groups/monitoring.tools.d.ts +10 -1
- package/dist/mcp/tool-groups/monitoring.tools.d.ts.map +1 -1
- package/dist/mcp/tool-groups/monitoring.tools.js +65 -18
- package/dist/mcp/tool-groups/monitoring.tools.js.map +1 -1
- package/dist/mcp/unavailable-tools.d.ts +35 -0
- package/dist/mcp/unavailable-tools.d.ts.map +1 -0
- package/dist/mcp/unavailable-tools.js +59 -0
- package/dist/mcp/unavailable-tools.js.map +1 -0
- package/dist/monitoring/app-health.indicator.d.ts.map +1 -1
- package/dist/monitoring/app-health.indicator.js +12 -0
- package/dist/monitoring/app-health.indicator.js.map +1 -1
- package/dist/monitoring/desktop-notifier.d.ts +26 -0
- package/dist/monitoring/desktop-notifier.d.ts.map +1 -0
- package/dist/monitoring/desktop-notifier.js +49 -0
- package/dist/monitoring/desktop-notifier.js.map +1 -0
- package/dist/monitoring/docker-health.indicator.d.ts +17 -1
- package/dist/monitoring/docker-health.indicator.d.ts.map +1 -1
- package/dist/monitoring/docker-health.indicator.js +38 -8
- package/dist/monitoring/docker-health.indicator.js.map +1 -1
- package/dist/monitoring/event-loop-stall.indicator.d.ts +26 -0
- package/dist/monitoring/event-loop-stall.indicator.d.ts.map +1 -0
- package/dist/monitoring/event-loop-stall.indicator.js +47 -0
- package/dist/monitoring/event-loop-stall.indicator.js.map +1 -0
- package/dist/monitoring/event-loop-watch.d.ts +124 -0
- package/dist/monitoring/event-loop-watch.d.ts.map +1 -0
- package/dist/monitoring/event-loop-watch.js +179 -0
- package/dist/monitoring/event-loop-watch.js.map +1 -0
- package/dist/monitoring/host-disk.d.ts +6 -0
- package/dist/monitoring/host-disk.d.ts.map +1 -0
- package/dist/monitoring/host-disk.js +26 -0
- package/dist/monitoring/host-disk.js.map +1 -0
- package/dist/monitoring/log-line.d.ts +188 -0
- package/dist/monitoring/log-line.d.ts.map +1 -0
- package/dist/monitoring/log-line.js +458 -0
- package/dist/monitoring/log-line.js.map +1 -0
- package/dist/monitoring/log-manager.d.ts +89 -8
- package/dist/monitoring/log-manager.d.ts.map +1 -1
- package/dist/monitoring/log-manager.js +246 -77
- package/dist/monitoring/log-manager.js.map +1 -1
- package/dist/observability/metrics-bridge.d.ts.map +1 -1
- package/dist/observability/metrics-server.d.ts.map +1 -1
- package/dist/operations.d.ts +16 -0
- package/dist/operations.d.ts.map +1 -0
- package/dist/operations.js +13 -0
- package/dist/operations.js.map +1 -0
- package/dist/orchestrator/app-handle.d.ts +157 -2
- package/dist/orchestrator/app-handle.d.ts.map +1 -1
- package/dist/orchestrator/app-handle.js +165 -3
- package/dist/orchestrator/app-handle.js.map +1 -1
- package/dist/orchestrator/app-health.d.ts +33 -0
- package/dist/orchestrator/app-health.d.ts.map +1 -0
- package/dist/orchestrator/app-health.js +74 -0
- package/dist/orchestrator/app-health.js.map +1 -0
- package/dist/orchestrator/app-traffic.d.ts +14 -0
- package/dist/orchestrator/app-traffic.d.ts.map +1 -0
- package/dist/orchestrator/app-traffic.js +33 -0
- package/dist/orchestrator/app-traffic.js.map +1 -0
- package/dist/orchestrator/bootstrap-loader-child.d.ts +19 -0
- package/dist/orchestrator/bootstrap-loader-child.d.ts.map +1 -0
- package/dist/orchestrator/bootstrap-loader-child.js +43 -0
- package/dist/orchestrator/bootstrap-loader-child.js.map +1 -0
- package/dist/orchestrator/bootstrap-loader.d.ts +72 -0
- package/dist/orchestrator/bootstrap-loader.d.ts.map +1 -1
- package/dist/orchestrator/bootstrap-loader.js +350 -5
- package/dist/orchestrator/bootstrap-loader.js.map +1 -1
- package/dist/orchestrator/bootstrap-process.d.ts +99 -4
- package/dist/orchestrator/bootstrap-process.d.ts.map +1 -1
- package/dist/orchestrator/bootstrap-process.js +332 -20
- package/dist/orchestrator/bootstrap-process.js.map +1 -1
- package/dist/orchestrator/build-service.d.ts +6 -12
- package/dist/orchestrator/build-service.d.ts.map +1 -1
- package/dist/orchestrator/build-service.js +120 -83
- package/dist/orchestrator/build-service.js.map +1 -1
- package/dist/orchestrator/call-exposed-service.d.ts +7 -0
- package/dist/orchestrator/call-exposed-service.d.ts.map +1 -0
- package/dist/orchestrator/call-exposed-service.js +50 -0
- package/dist/orchestrator/call-exposed-service.js.map +1 -0
- package/dist/orchestrator/classic-launcher.d.ts +1 -0
- package/dist/orchestrator/classic-launcher.d.ts.map +1 -1
- package/dist/orchestrator/classic-launcher.js +40 -4
- package/dist/orchestrator/classic-launcher.js.map +1 -1
- package/dist/orchestrator/file-watcher.d.ts +79 -1
- package/dist/orchestrator/file-watcher.d.ts.map +1 -1
- package/dist/orchestrator/file-watcher.js +193 -7
- package/dist/orchestrator/file-watcher.js.map +1 -1
- package/dist/orchestrator/http-routes.d.ts +4 -0
- package/dist/orchestrator/http-routes.d.ts.map +1 -0
- package/dist/orchestrator/http-routes.js +19 -0
- package/dist/orchestrator/http-routes.js.map +1 -0
- package/dist/orchestrator/module-worker-process.d.ts.map +1 -1
- package/dist/orchestrator/module-worker-process.js +10 -0
- package/dist/orchestrator/module-worker-process.js.map +1 -1
- package/dist/orchestrator/orchestrator.service.d.ts +385 -11
- package/dist/orchestrator/orchestrator.service.d.ts.map +1 -1
- package/dist/orchestrator/orchestrator.service.js +1738 -364
- package/dist/orchestrator/orchestrator.service.js.map +1 -1
- package/dist/orchestrator/owned-pids.d.ts +83 -0
- package/dist/orchestrator/owned-pids.d.ts.map +1 -0
- package/dist/orchestrator/owned-pids.js +93 -0
- package/dist/orchestrator/owned-pids.js.map +1 -0
- package/dist/orchestrator/port-conflict.d.ts +102 -0
- package/dist/orchestrator/port-conflict.d.ts.map +1 -0
- package/dist/orchestrator/port-conflict.js +131 -0
- package/dist/orchestrator/port-conflict.js.map +1 -0
- package/dist/orchestrator/process-janitor.d.ts +78 -4
- package/dist/orchestrator/process-janitor.d.ts.map +1 -1
- package/dist/orchestrator/process-janitor.js +246 -30
- package/dist/orchestrator/process-janitor.js.map +1 -1
- package/dist/orchestrator/ps-metrics.d.ts +37 -0
- package/dist/orchestrator/ps-metrics.d.ts.map +1 -0
- package/dist/orchestrator/ps-metrics.js +60 -0
- package/dist/orchestrator/ps-metrics.js.map +1 -0
- package/dist/orchestrator/service-router.d.ts +58 -2
- package/dist/orchestrator/service-router.d.ts.map +1 -1
- package/dist/orchestrator/service-router.js +157 -25
- package/dist/orchestrator/service-router.js.map +1 -1
- package/dist/orchestrator/topology-query.d.ts +49 -0
- package/dist/orchestrator/topology-query.d.ts.map +1 -0
- package/dist/orchestrator/topology-query.js +54 -0
- package/dist/orchestrator/topology-query.js.map +1 -0
- package/dist/project/artifact-builder.d.ts +174 -3
- package/dist/project/artifact-builder.d.ts.map +1 -1
- package/dist/project/artifact-builder.js +454 -42
- package/dist/project/artifact-builder.js.map +1 -1
- package/dist/project/build-decision.d.ts +58 -0
- package/dist/project/build-decision.d.ts.map +1 -0
- package/dist/project/build-decision.js +59 -0
- package/dist/project/build-decision.js.map +1 -0
- package/dist/project/config-resolver.d.ts +6 -4
- package/dist/project/config-resolver.d.ts.map +1 -1
- package/dist/project/config-resolver.js +128 -74
- package/dist/project/config-resolver.js.map +1 -1
- package/dist/project/declared-config.d.ts +42 -0
- package/dist/project/declared-config.d.ts.map +1 -0
- package/dist/project/declared-config.js +60 -0
- package/dist/project/declared-config.js.map +1 -0
- package/dist/project/dependency-analyzer.d.ts.map +1 -1
- package/dist/project/dependency-analyzer.js.map +1 -1
- package/dist/project/deploy-phases.d.ts +35 -0
- package/dist/project/deploy-phases.d.ts.map +1 -0
- package/dist/project/deploy-phases.js +75 -0
- package/dist/project/deploy-phases.js.map +1 -0
- package/dist/project/exec-in-group.d.ts +49 -0
- package/dist/project/exec-in-group.d.ts.map +1 -0
- package/dist/project/exec-in-group.js +100 -0
- package/dist/project/exec-in-group.js.map +1 -0
- package/dist/project/node-app-config.d.ts +101 -0
- package/dist/project/node-app-config.d.ts.map +1 -0
- package/dist/project/node-app-config.js +134 -0
- package/dist/project/node-app-config.js.map +1 -0
- package/dist/project/node-app-health.d.ts +76 -0
- package/dist/project/node-app-health.d.ts.map +1 -0
- package/dist/project/node-app-health.js +119 -0
- package/dist/project/node-app-health.js.map +1 -0
- package/dist/project/operator-account.d.ts +293 -0
- package/dist/project/operator-account.d.ts.map +1 -0
- package/dist/project/operator-account.js +373 -0
- package/dist/project/operator-account.js.map +1 -0
- package/dist/project/registry.d.ts +45 -8
- package/dist/project/registry.d.ts.map +1 -1
- package/dist/project/registry.js +214 -64
- package/dist/project/registry.js.map +1 -1
- package/dist/project/requirements-scanner.d.ts +1 -0
- package/dist/project/requirements-scanner.d.ts.map +1 -1
- package/dist/project/requirements-scanner.js +28 -3
- package/dist/project/requirements-scanner.js.map +1 -1
- package/dist/project/stack-env.d.ts +37 -0
- package/dist/project/stack-env.d.ts.map +1 -0
- package/dist/project/stack-env.js +85 -0
- package/dist/project/stack-env.js.map +1 -0
- package/dist/project/stand-containers.d.ts +11 -0
- package/dist/project/stand-containers.d.ts.map +1 -0
- package/dist/project/stand-containers.js +27 -0
- package/dist/project/stand-containers.js.map +1 -0
- package/dist/project/working-tree.d.ts +62 -0
- package/dist/project/working-tree.d.ts.map +1 -0
- package/dist/project/working-tree.js +101 -0
- package/dist/project/working-tree.js.map +1 -0
- package/dist/release/artifact-store.d.ts +360 -0
- package/dist/release/artifact-store.d.ts.map +1 -0
- package/dist/release/artifact-store.js +1040 -0
- package/dist/release/artifact-store.js.map +1 -0
- package/dist/release/attest-on-node.d.ts +93 -0
- package/dist/release/attest-on-node.d.ts.map +1 -0
- package/dist/release/attest-on-node.js +207 -0
- package/dist/release/attest-on-node.js.map +1 -0
- package/dist/release/attest.d.ts +135 -0
- package/dist/release/attest.d.ts.map +1 -0
- package/dist/release/attest.js +199 -0
- package/dist/release/attest.js.map +1 -0
- package/dist/release/build-run.d.ts +151 -0
- package/dist/release/build-run.d.ts.map +1 -0
- package/dist/release/build-run.js +609 -0
- package/dist/release/build-run.js.map +1 -0
- package/dist/release/builder.d.ts +57 -0
- package/dist/release/builder.d.ts.map +1 -0
- package/dist/release/builder.js +120 -0
- package/dist/release/builder.js.map +1 -0
- package/dist/release/daemon-omni.d.ts +41 -0
- package/dist/release/daemon-omni.d.ts.map +1 -0
- package/dist/release/daemon-omni.js +59 -0
- package/dist/release/daemon-omni.js.map +1 -0
- package/dist/release/definition-inputs.d.ts +34 -0
- package/dist/release/definition-inputs.d.ts.map +1 -0
- package/dist/release/definition-inputs.js +90 -0
- package/dist/release/definition-inputs.js.map +1 -0
- package/dist/release/delivered.d.ts +53 -0
- package/dist/release/delivered.d.ts.map +1 -0
- package/dist/release/delivered.js +81 -0
- package/dist/release/delivered.js.map +1 -0
- package/dist/release/layout.d.ts +61 -0
- package/dist/release/layout.d.ts.map +1 -0
- package/dist/release/layout.js +114 -0
- package/dist/release/layout.js.map +1 -0
- package/dist/release/load.d.ts +44 -0
- package/dist/release/load.d.ts.map +1 -0
- package/dist/release/load.js +84 -0
- package/dist/release/load.js.map +1 -0
- package/dist/release/manifest.d.ts +299 -0
- package/dist/release/manifest.d.ts.map +1 -0
- package/dist/release/manifest.js +176 -0
- package/dist/release/manifest.js.map +1 -0
- package/dist/release/migrations.d.ts +32 -0
- package/dist/release/migrations.d.ts.map +1 -0
- package/dist/release/migrations.js +51 -0
- package/dist/release/migrations.js.map +1 -0
- package/dist/release/publish.d.ts +71 -0
- package/dist/release/publish.d.ts.map +1 -0
- package/dist/release/publish.js +102 -0
- package/dist/release/publish.js.map +1 -0
- package/dist/release/store.d.ts +174 -0
- package/dist/release/store.d.ts.map +1 -0
- package/dist/release/store.js +257 -0
- package/dist/release/store.js.map +1 -0
- package/dist/services/alert.rpc-service.d.ts +11 -15
- package/dist/services/alert.rpc-service.d.ts.map +1 -1
- package/dist/services/alert.rpc-service.js +29 -14
- package/dist/services/alert.rpc-service.js.map +1 -1
- package/dist/services/alert.service.d.ts +96 -59
- package/dist/services/alert.service.d.ts.map +1 -1
- package/dist/services/alert.service.js +455 -70
- package/dist/services/alert.service.js.map +1 -1
- package/dist/services/anonymous-input.d.ts +26 -0
- package/dist/services/anonymous-input.d.ts.map +1 -0
- package/dist/services/anonymous-input.js +44 -0
- package/dist/services/anonymous-input.js.map +1 -0
- package/dist/services/audit.rpc-service.d.ts +27 -0
- package/dist/services/audit.rpc-service.d.ts.map +1 -0
- package/dist/services/audit.rpc-service.js +57 -0
- package/dist/services/audit.rpc-service.js.map +1 -0
- package/dist/services/audit.service.d.ts +165 -0
- package/dist/services/audit.service.d.ts.map +1 -0
- package/dist/services/audit.service.js +242 -0
- package/dist/services/audit.service.js.map +1 -0
- package/dist/services/auth-context.d.ts +21 -1
- package/dist/services/auth-context.d.ts.map +1 -1
- package/dist/services/auth-context.js +51 -5
- package/dist/services/auth-context.js.map +1 -1
- package/dist/services/auth.rpc-service.d.ts +0 -1
- package/dist/services/auth.rpc-service.d.ts.map +1 -1
- package/dist/services/auth.rpc-service.js +23 -9
- package/dist/services/auth.rpc-service.js.map +1 -1
- package/dist/services/auth.service.d.ts +47 -33
- package/dist/services/auth.service.d.ts.map +1 -1
- package/dist/services/auth.service.js +155 -23
- package/dist/services/auth.service.js.map +1 -1
- package/dist/services/backup-pipeline.d.ts +97 -0
- package/dist/services/backup-pipeline.d.ts.map +1 -0
- package/dist/services/backup-pipeline.js +286 -0
- package/dist/services/backup-pipeline.js.map +1 -0
- package/dist/services/backup-schedule.d.ts +69 -0
- package/dist/services/backup-schedule.d.ts.map +1 -0
- package/dist/services/backup-schedule.js +132 -0
- package/dist/services/backup-schedule.js.map +1 -0
- package/dist/services/backup.rpc-service.d.ts +31 -2
- package/dist/services/backup.rpc-service.d.ts.map +1 -1
- package/dist/services/backup.rpc-service.js +53 -0
- package/dist/services/backup.rpc-service.js.map +1 -1
- package/dist/services/backup.service.d.ts +458 -14
- package/dist/services/backup.service.d.ts.map +1 -1
- package/dist/services/backup.service.js +1368 -170
- package/dist/services/backup.service.js.map +1 -1
- package/dist/services/bundle-build-worker.d.ts +19 -0
- package/dist/services/bundle-build-worker.d.ts.map +1 -0
- package/dist/services/bundle-build-worker.js +40 -0
- package/dist/services/bundle-build-worker.js.map +1 -0
- package/dist/services/bundle-builder.d.ts +490 -0
- package/dist/services/bundle-builder.d.ts.map +1 -0
- package/dist/services/bundle-builder.js +1159 -0
- package/dist/services/bundle-builder.js.map +1 -0
- package/dist/services/bundle-worker-protocol.d.ts +19 -0
- package/dist/services/bundle-worker-protocol.d.ts.map +1 -0
- package/dist/services/bundle-worker-protocol.js +32 -0
- package/dist/services/bundle-worker-protocol.js.map +1 -0
- package/dist/services/deploy.rpc-service.d.ts +4 -1
- package/dist/services/deploy.rpc-service.d.ts.map +1 -1
- package/dist/services/deploy.rpc-service.js +10 -0
- package/dist/services/deploy.rpc-service.js.map +1 -1
- package/dist/services/deploy.service.d.ts +14 -24
- package/dist/services/deploy.service.d.ts.map +1 -1
- package/dist/services/deploy.service.js +46 -5
- package/dist/services/deploy.service.js.map +1 -1
- package/dist/services/discovery.rpc-service.d.ts +2 -1
- package/dist/services/discovery.rpc-service.d.ts.map +1 -1
- package/dist/services/discovery.rpc-service.js.map +1 -1
- package/dist/services/discovery.service.d.ts +32 -28
- package/dist/services/discovery.service.d.ts.map +1 -1
- package/dist/services/discovery.service.js +108 -133
- package/dist/services/discovery.service.js.map +1 -1
- package/dist/services/event-broadcaster.rpc-service.d.ts +36 -23
- package/dist/services/event-broadcaster.rpc-service.d.ts.map +1 -1
- package/dist/services/event-broadcaster.rpc-service.js +70 -32
- package/dist/services/event-broadcaster.rpc-service.js.map +1 -1
- package/dist/services/event-broadcaster.service.d.ts.map +1 -1
- package/dist/services/event-broadcaster.service.js.map +1 -1
- package/dist/services/fleet.rpc-service.d.ts +2 -1
- package/dist/services/fleet.rpc-service.d.ts.map +1 -1
- package/dist/services/fleet.rpc-service.js +3 -1
- package/dist/services/fleet.rpc-service.js.map +1 -1
- package/dist/services/fleet.service.d.ts +32 -34
- package/dist/services/fleet.service.d.ts.map +1 -1
- package/dist/services/fleet.service.js +66 -10
- package/dist/services/fleet.service.js.map +1 -1
- package/dist/services/health-check.rpc-service.d.ts +2 -1
- package/dist/services/health-check.rpc-service.d.ts.map +1 -1
- package/dist/services/health-check.rpc-service.js.map +1 -1
- package/dist/services/health-check.service.d.ts +50 -36
- package/dist/services/health-check.service.d.ts.map +1 -1
- package/dist/services/health-check.service.js +179 -182
- package/dist/services/health-check.service.js.map +1 -1
- package/dist/services/host-operation-barrier.d.ts +8 -0
- package/dist/services/host-operation-barrier.d.ts.map +1 -0
- package/dist/services/host-operation-barrier.js +44 -0
- package/dist/services/host-operation-barrier.js.map +1 -0
- package/dist/services/host-upgrade.d.ts +129 -0
- package/dist/services/host-upgrade.d.ts.map +1 -0
- package/dist/services/host-upgrade.js +352 -0
- package/dist/services/host-upgrade.js.map +1 -0
- package/dist/services/infrastructure.rpc-service.d.ts +312 -2
- package/dist/services/infrastructure.rpc-service.d.ts.map +1 -1
- package/dist/services/infrastructure.rpc-service.js +584 -11
- package/dist/services/infrastructure.rpc-service.js.map +1 -1
- package/dist/services/kubernetes.rpc-service.d.ts +2 -1
- package/dist/services/kubernetes.rpc-service.d.ts.map +1 -1
- package/dist/services/kubernetes.rpc-service.js.map +1 -1
- package/dist/services/kubernetes.service.d.ts +5 -39
- package/dist/services/kubernetes.service.d.ts.map +1 -1
- package/dist/services/kubernetes.service.js +29 -5
- package/dist/services/kubernetes.service.js.map +1 -1
- package/dist/services/local-bundle.d.ts +160 -0
- package/dist/services/local-bundle.d.ts.map +1 -0
- package/dist/services/local-bundle.js +242 -0
- package/dist/services/local-bundle.js.map +1 -0
- package/dist/services/lockout.d.ts +28 -0
- package/dist/services/lockout.d.ts.map +1 -0
- package/dist/services/lockout.js +41 -0
- package/dist/services/lockout.js.map +1 -0
- package/dist/services/log-collector.rpc-service.d.ts +16 -0
- package/dist/services/log-collector.rpc-service.d.ts.map +1 -1
- package/dist/services/log-collector.rpc-service.js +16 -0
- package/dist/services/log-collector.rpc-service.js.map +1 -1
- package/dist/services/log-collector.service.d.ts +172 -54
- package/dist/services/log-collector.service.d.ts.map +1 -1
- package/dist/services/log-collector.service.js +492 -102
- package/dist/services/log-collector.service.js.map +1 -1
- package/dist/services/log-retention.d.ts +50 -0
- package/dist/services/log-retention.d.ts.map +1 -0
- package/dist/services/log-retention.js +52 -0
- package/dist/services/log-retention.js.map +1 -0
- package/dist/services/maintenance-lock.d.ts +162 -0
- package/dist/services/maintenance-lock.d.ts.map +1 -0
- package/dist/services/maintenance-lock.js +316 -0
- package/dist/services/maintenance-lock.js.map +1 -0
- package/dist/services/master-address.d.ts +107 -0
- package/dist/services/master-address.d.ts.map +1 -0
- package/dist/services/master-address.js +154 -0
- package/dist/services/master-address.js.map +1 -0
- package/dist/services/migration-output.d.ts +16 -0
- package/dist/services/migration-output.d.ts.map +1 -0
- package/dist/services/migration-output.js +22 -0
- package/dist/services/migration-output.js.map +1 -0
- package/dist/services/netron-telemetry-transport.d.ts.map +1 -1
- package/dist/services/netron-telemetry-transport.js.map +1 -1
- package/dist/services/node-deploy-lease.d.ts +171 -0
- package/dist/services/node-deploy-lease.d.ts.map +1 -0
- package/dist/services/node-deploy-lease.js +460 -0
- package/dist/services/node-deploy-lease.js.map +1 -0
- package/dist/services/node-health.repository.d.ts +63 -4
- package/dist/services/node-health.repository.d.ts.map +1 -1
- package/dist/services/node-health.repository.js +108 -67
- package/dist/services/node-health.repository.js.map +1 -1
- package/dist/services/node-manager.rpc-service.d.ts +360 -10
- package/dist/services/node-manager.rpc-service.d.ts.map +1 -1
- package/dist/services/node-manager.rpc-service.js +754 -34
- package/dist/services/node-manager.rpc-service.js.map +1 -1
- package/dist/services/node-manager.service.d.ts +267 -95
- package/dist/services/node-manager.service.d.ts.map +1 -1
- package/dist/services/node-manager.service.js +805 -103
- package/dist/services/node-manager.service.js.map +1 -1
- package/dist/services/node-upgrade.d.ts +129 -0
- package/dist/services/node-upgrade.d.ts.map +1 -0
- package/dist/services/node-upgrade.js +161 -0
- package/dist/services/node-upgrade.js.map +1 -0
- package/dist/services/node-upgrade.service.d.ts +232 -0
- package/dist/services/node-upgrade.service.d.ts.map +1 -0
- package/dist/services/node-upgrade.service.js +468 -0
- package/dist/services/node-upgrade.service.js.map +1 -0
- package/dist/services/pipeline.rpc-service.d.ts +6 -1
- package/dist/services/pipeline.rpc-service.d.ts.map +1 -1
- package/dist/services/pipeline.rpc-service.js +9 -0
- package/dist/services/pipeline.rpc-service.js.map +1 -1
- package/dist/services/pipeline.service.d.ts +17 -46
- package/dist/services/pipeline.service.d.ts.map +1 -1
- package/dist/services/pipeline.service.js +84 -9
- package/dist/services/pipeline.service.js.map +1 -1
- package/dist/services/project.rpc-service.d.ts +140 -5
- package/dist/services/project.rpc-service.d.ts.map +1 -1
- package/dist/services/project.rpc-service.js +260 -22
- package/dist/services/project.rpc-service.js.map +1 -1
- package/dist/services/project.service.d.ts +687 -15
- package/dist/services/project.service.d.ts.map +1 -1
- package/dist/services/project.service.js +2846 -337
- package/dist/services/project.service.js.map +1 -1
- package/dist/services/redeploy-decision.d.ts +176 -0
- package/dist/services/redeploy-decision.d.ts.map +1 -0
- package/dist/services/redeploy-decision.js +142 -0
- package/dist/services/redeploy-decision.js.map +1 -0
- package/dist/services/release.rpc-service.d.ts +159 -0
- package/dist/services/release.rpc-service.d.ts.map +1 -0
- package/dist/services/release.rpc-service.js +395 -0
- package/dist/services/release.rpc-service.js.map +1 -0
- package/dist/services/release.service.d.ts +114 -0
- package/dist/services/release.service.d.ts.map +1 -0
- package/dist/services/release.service.js +208 -0
- package/dist/services/release.service.js.map +1 -0
- package/dist/services/remote-deployer.service.d.ts +580 -8
- package/dist/services/remote-deployer.service.d.ts.map +1 -1
- package/dist/services/remote-deployer.service.js +1786 -169
- package/dist/services/remote-deployer.service.js.map +1 -1
- package/dist/services/remote-ops.service.d.ts +51 -9
- package/dist/services/remote-ops.service.d.ts.map +1 -1
- package/dist/services/remote-ops.service.js +235 -39
- package/dist/services/remote-ops.service.js.map +1 -1
- package/dist/services/remote-provisioner.d.ts +124 -0
- package/dist/services/remote-provisioner.d.ts.map +1 -0
- package/dist/services/remote-provisioner.js +258 -0
- package/dist/services/remote-provisioner.js.map +1 -0
- package/dist/services/secrets.rpc-service.d.ts +51 -2
- package/dist/services/secrets.rpc-service.d.ts.map +1 -1
- package/dist/services/secrets.rpc-service.js +81 -2
- package/dist/services/secrets.rpc-service.js.map +1 -1
- package/dist/services/secrets.service.d.ts +108 -9
- package/dist/services/secrets.service.d.ts.map +1 -1
- package/dist/services/secrets.service.js +313 -48
- package/dist/services/secrets.service.js.map +1 -1
- package/dist/services/slave-storage.service.d.ts +107 -0
- package/dist/services/slave-storage.service.d.ts.map +1 -1
- package/dist/services/slave-storage.service.js +171 -2
- package/dist/services/slave-storage.service.js.map +1 -1
- package/dist/services/static-carry-forward.d.ts +110 -0
- package/dist/services/static-carry-forward.d.ts.map +1 -0
- package/dist/services/static-carry-forward.js +433 -0
- package/dist/services/static-carry-forward.js.map +1 -0
- package/dist/services/sync-policy.d.ts +167 -0
- package/dist/services/sync-policy.d.ts.map +1 -0
- package/dist/services/sync-policy.js +161 -0
- package/dist/services/sync-policy.js.map +1 -0
- package/dist/services/sync-summary.d.ts +39 -0
- package/dist/services/sync-summary.d.ts.map +1 -0
- package/dist/services/sync-summary.js +62 -0
- package/dist/services/sync-summary.js.map +1 -0
- package/dist/services/sync.rpc-service.d.ts +19 -7
- package/dist/services/sync.rpc-service.d.ts.map +1 -1
- package/dist/services/sync.rpc-service.js +36 -5
- package/dist/services/sync.rpc-service.js.map +1 -1
- package/dist/services/sync.service.d.ts +350 -18
- package/dist/services/sync.service.d.ts.map +1 -1
- package/dist/services/sync.service.js +864 -116
- package/dist/services/sync.service.js.map +1 -1
- package/dist/services/system-info.rpc-service.d.ts +2 -1
- package/dist/services/system-info.rpc-service.d.ts.map +1 -1
- package/dist/services/system-info.rpc-service.js.map +1 -1
- package/dist/services/system-info.service.d.ts +56 -68
- package/dist/services/system-info.service.d.ts.map +1 -1
- package/dist/services/system-info.service.js +240 -127
- package/dist/services/system-info.service.js.map +1 -1
- package/dist/services/telemetry.rpc-service.d.ts.map +1 -1
- package/dist/services/telemetry.rpc-service.js +9 -3
- package/dist/services/telemetry.rpc-service.js.map +1 -1
- package/dist/services/telemetry.service.d.ts +4 -2
- package/dist/services/telemetry.service.d.ts.map +1 -1
- package/dist/services/telemetry.service.js +4 -2
- package/dist/services/telemetry.service.js.map +1 -1
- package/dist/services/trace-collector.rpc-service.d.ts +2 -1
- package/dist/services/trace-collector.rpc-service.d.ts.map +1 -1
- package/dist/services/trace-collector.rpc-service.js.map +1 -1
- package/dist/services/trace-collector.service.d.ts +2 -41
- package/dist/services/trace-collector.service.d.ts.map +1 -1
- package/dist/services/trace-collector.service.js +27 -7
- package/dist/services/trace-collector.service.js.map +1 -1
- package/dist/services/wait-for-postgres.d.ts +28 -0
- package/dist/services/wait-for-postgres.d.ts.map +1 -0
- package/dist/services/wait-for-postgres.js +77 -0
- package/dist/services/wait-for-postgres.js.map +1 -0
- package/dist/shared/alert-expression.d.ts +60 -0
- package/dist/shared/alert-expression.d.ts.map +1 -0
- package/dist/shared/alert-expression.js +124 -0
- package/dist/shared/alert-expression.js.map +1 -0
- package/dist/shared/audit-outcome.d.ts +39 -0
- package/dist/shared/audit-outcome.d.ts.map +1 -0
- package/dist/shared/audit-outcome.js +45 -0
- package/dist/shared/audit-outcome.js.map +1 -0
- package/dist/shared/build-freshness.d.ts +59 -0
- package/dist/shared/build-freshness.d.ts.map +1 -0
- package/dist/shared/build-freshness.js +113 -0
- package/dist/shared/build-freshness.js.map +1 -0
- package/dist/shared/build-stamp.d.ts +26 -0
- package/dist/shared/build-stamp.d.ts.map +1 -0
- package/dist/shared/build-stamp.js +36 -0
- package/dist/shared/build-stamp.js.map +1 -0
- package/dist/shared/describe-error.d.ts +22 -0
- package/dist/shared/describe-error.d.ts.map +1 -0
- package/dist/shared/describe-error.js +39 -0
- package/dist/shared/describe-error.js.map +1 -0
- package/dist/shared/dto/alerts.d.ts +101 -0
- package/dist/shared/dto/alerts.d.ts.map +1 -0
- package/dist/shared/dto/alerts.js +9 -0
- package/dist/shared/dto/alerts.js.map +1 -0
- package/dist/shared/dto/auth.d.ts +49 -0
- package/dist/shared/dto/auth.d.ts.map +1 -0
- package/dist/shared/dto/auth.js +14 -0
- package/dist/shared/dto/auth.js.map +1 -0
- package/dist/shared/dto/backups.d.ts +16 -0
- package/dist/shared/dto/backups.d.ts.map +1 -0
- package/dist/shared/dto/backups.js +9 -0
- package/dist/shared/dto/backups.js.map +1 -0
- package/dist/shared/dto/deploy.d.ts +28 -0
- package/dist/shared/dto/deploy.d.ts.map +1 -0
- package/dist/shared/dto/deploy.js +8 -0
- package/dist/shared/dto/deploy.js.map +1 -0
- package/dist/shared/dto/discovery.d.ts +24 -0
- package/dist/shared/dto/discovery.d.ts.map +1 -0
- package/dist/shared/dto/discovery.js +9 -0
- package/dist/shared/dto/discovery.js.map +1 -0
- package/dist/shared/dto/fleet.d.ts +35 -0
- package/dist/shared/dto/fleet.d.ts.map +1 -0
- package/dist/shared/dto/fleet.js +8 -0
- package/dist/shared/dto/fleet.js.map +1 -0
- package/dist/shared/dto/health.d.ts +26 -0
- package/dist/shared/dto/health.d.ts.map +1 -0
- package/dist/shared/dto/health.js +9 -0
- package/dist/shared/dto/health.js.map +1 -0
- package/dist/shared/dto/kubernetes.d.ts +44 -0
- package/dist/shared/dto/kubernetes.d.ts.map +1 -0
- package/dist/shared/dto/kubernetes.js +9 -0
- package/dist/shared/dto/kubernetes.js.map +1 -0
- package/dist/shared/dto/logs.d.ts +77 -0
- package/dist/shared/dto/logs.d.ts.map +1 -0
- package/dist/shared/dto/logs.js +10 -0
- package/dist/shared/dto/logs.js.map +1 -0
- package/dist/shared/dto/metrics.d.ts +65 -0
- package/dist/shared/dto/metrics.d.ts.map +1 -0
- package/dist/shared/dto/metrics.js +17 -0
- package/dist/shared/dto/metrics.js.map +1 -0
- package/dist/shared/dto/nodes.d.ts +347 -0
- package/dist/shared/dto/nodes.d.ts.map +1 -0
- package/dist/shared/dto/nodes.js +8 -0
- package/dist/shared/dto/nodes.js.map +1 -0
- package/dist/shared/dto/pipelines.d.ts +53 -0
- package/dist/shared/dto/pipelines.d.ts.map +1 -0
- package/dist/shared/dto/pipelines.js +9 -0
- package/dist/shared/dto/pipelines.js.map +1 -0
- package/dist/shared/dto/project.d.ts +142 -8
- package/dist/shared/dto/project.d.ts.map +1 -1
- package/dist/shared/dto/services.d.ts +858 -7
- package/dist/shared/dto/services.d.ts.map +1 -1
- package/dist/shared/dto/system-info.d.ts +93 -0
- package/dist/shared/dto/system-info.d.ts.map +1 -0
- package/dist/shared/dto/system-info.js +9 -0
- package/dist/shared/dto/system-info.js.map +1 -0
- package/dist/shared/dto/traces.d.ts +49 -0
- package/dist/shared/dto/traces.d.ts.map +1 -0
- package/dist/shared/dto/traces.js +9 -0
- package/dist/shared/dto/traces.js.map +1 -0
- package/dist/shared/env-config.d.ts +74 -0
- package/dist/shared/env-config.d.ts.map +1 -0
- package/dist/shared/env-config.js +99 -0
- package/dist/shared/env-config.js.map +1 -0
- package/dist/shared/events.d.ts +45 -39
- package/dist/shared/events.d.ts.map +1 -1
- package/dist/shared/events.js +6 -0
- package/dist/shared/events.js.map +1 -1
- package/dist/shared/format.d.ts +1 -1
- package/dist/shared/format.d.ts.map +1 -1
- package/dist/shared/format.js +4 -0
- package/dist/shared/format.js.map +1 -1
- package/dist/shared/local-infra-host.d.ts +28 -0
- package/dist/shared/local-infra-host.d.ts.map +1 -0
- package/dist/shared/local-infra-host.js +28 -0
- package/dist/shared/local-infra-host.js.map +1 -0
- package/dist/shared/node-check.d.ts +37 -0
- package/dist/shared/node-check.d.ts.map +1 -0
- package/dist/shared/node-check.js +41 -0
- package/dist/shared/node-check.js.map +1 -0
- package/dist/shared/paths.d.ts +29 -0
- package/dist/shared/paths.d.ts.map +1 -0
- package/dist/shared/paths.js +54 -0
- package/dist/shared/paths.js.map +1 -0
- package/dist/shared/pnpm.d.ts +29 -0
- package/dist/shared/pnpm.d.ts.map +1 -0
- package/dist/shared/pnpm.js +57 -0
- package/dist/shared/pnpm.js.map +1 -0
- package/dist/shared/private-files.d.ts +29 -0
- package/dist/shared/private-files.d.ts.map +1 -0
- package/dist/shared/private-files.js +109 -0
- package/dist/shared/private-files.js.map +1 -0
- package/dist/shared/redact-env.d.ts +24 -0
- package/dist/shared/redact-env.d.ts.map +1 -0
- package/dist/shared/redact-env.js +36 -0
- package/dist/shared/redact-env.js.map +1 -0
- package/dist/shared/release-reading.d.ts +85 -0
- package/dist/shared/release-reading.d.ts.map +1 -0
- package/dist/shared/release-reading.js +81 -0
- package/dist/shared/release-reading.js.map +1 -0
- package/dist/shared/roles.d.ts +58 -3
- package/dist/shared/roles.d.ts.map +1 -1
- package/dist/shared/roles.js +55 -0
- package/dist/shared/roles.js.map +1 -1
- package/dist/shared/rpcauth.d.ts +13 -0
- package/dist/shared/rpcauth.d.ts.map +1 -0
- package/dist/shared/rpcauth.js +23 -0
- package/dist/shared/rpcauth.js.map +1 -0
- package/dist/shared/shell-escape.d.ts +9 -0
- package/dist/shared/shell-escape.d.ts.map +1 -0
- package/dist/shared/shell-escape.js +11 -0
- package/dist/shared/shell-escape.js.map +1 -0
- package/dist/shared/sync-reading.d.ts +45 -0
- package/dist/shared/sync-reading.d.ts.map +1 -0
- package/dist/shared/sync-reading.js +42 -0
- package/dist/shared/sync-reading.js.map +1 -0
- package/dist/shared/tokens.d.ts +19 -0
- package/dist/shared/tokens.d.ts.map +1 -1
- package/dist/shared/tokens.js +28 -0
- package/dist/shared/tokens.js.map +1 -1
- package/dist/webapp/webapp.service.d.ts +33 -2
- package/dist/webapp/webapp.service.d.ts.map +1 -1
- package/dist/webapp/webapp.service.js +143 -31
- package/dist/webapp/webapp.service.js.map +1 -1
- package/dist/workers/health-monitor-process.d.ts.map +1 -1
- package/dist/workers/health-monitor-process.js +10 -0
- package/dist/workers/health-monitor-process.js.map +1 -1
- package/dist/workers/health-monitor.service.d.ts +45 -3
- package/dist/workers/health-monitor.service.d.ts.map +1 -1
- package/dist/workers/health-monitor.service.js +152 -25
- package/dist/workers/health-monitor.service.js.map +1 -1
- package/dist/workers/system-worker-manager.d.ts +43 -0
- package/dist/workers/system-worker-manager.d.ts.map +1 -1
- package/dist/workers/system-worker-manager.js +150 -3
- package/dist/workers/system-worker-manager.js.map +1 -1
- package/dist/workers/types.d.ts +8 -2
- package/dist/workers/types.d.ts.map +1 -1
- package/package.json +68 -30
- package/webapp/dist/BUILD.json +5 -0
- package/webapp/dist/assets/FormControlLabel-BbSxpQwN.js +1 -0
- package/webapp/dist/assets/Grid-DizFZYue.js +1 -0
- package/webapp/dist/assets/Switch-Bfiqzx8l.js +1 -0
- package/webapp/dist/assets/alerts-DH_RVRS3.js +1 -0
- package/webapp/dist/assets/apps-B96355sz.js +1 -0
- package/webapp/dist/assets/audit-CYGtbi4k.js +1 -0
- package/webapp/dist/assets/client-BmqFExWT.js +1 -0
- package/webapp/dist/assets/constants-veKGFkC6.js +1 -0
- package/webapp/dist/assets/containers-B_TpThln.js +2 -0
- package/webapp/dist/assets/daemon-memory-CCPZ4cV_.js +1 -0
- package/webapp/dist/assets/dashboard-DHf2_65N.js +1 -0
- package/webapp/dist/assets/dashboard-builder-CWY2pjeG.js +1 -0
- package/webapp/dist/assets/deploy-release-dialog-Dp-zpSAd.js +1 -0
- package/webapp/dist/assets/deployments-tJI6O3fK.js +1 -0
- package/webapp/dist/assets/detail-B2zP1Hto.js +8 -0
- package/webapp/dist/assets/detail-CoIrwmsy.js +1 -0
- package/webapp/dist/assets/detail-DWaGaOtL.js +2 -0
- package/webapp/dist/assets/formatters-BqjL73N3.js +1 -0
- package/webapp/dist/assets/index-Dvo0iprx.css +1 -0
- package/webapp/dist/assets/index-vMnY_4jR.js +412 -0
- package/webapp/dist/assets/logs-DTRjOfX8.js +12 -0
- package/webapp/dist/assets/maplibre-gl-DowHoZAx.js +814 -0
- package/webapp/dist/assets/metrics-UCajtdox.js +2 -0
- package/webapp/dist/assets/nodes-DuTVU0f6.js +7 -0
- package/webapp/dist/assets/pipelines-C8BeDcAl.js +4 -0
- package/webapp/dist/assets/projects-Dp_zFfLY.js +1 -0
- package/webapp/dist/assets/react-DlmbjZJV.js +1 -0
- package/webapp/dist/assets/react-apexcharts.esm-C8pn45ml.js +97 -0
- package/webapp/dist/assets/release-bits-BzTKz5fM.js +1 -0
- package/webapp/dist/assets/release-build-panel-DcW9YcXQ.js +1 -0
- package/webapp/dist/assets/releases-B4hcr1bE.js +2 -0
- package/webapp/dist/assets/rolldown-runtime-XKyaHtl9.js +1 -0
- package/webapp/dist/assets/settings-DIteZTbe.js +1 -0
- package/webapp/dist/assets/settled-pair-C7SjZuns.js +1 -0
- package/webapp/dist/assets/sign-in-D4kMBOHp.js +1 -0
- package/webapp/dist/assets/stacks-Brnf-EB7.js +1 -0
- package/webapp/dist/assets/sync-reading-C3PnJ7AX.js +1 -0
- package/webapp/dist/assets/system-info-CFDNVKkU.js +1 -0
- package/webapp/dist/assets/topology-7WeTUsDN.js +12 -0
- package/webapp/dist/assets/topology-DLioOiRN.css +1 -0
- package/webapp/dist/assets/traces-9c3gwCN-.js +1 -0
- package/webapp/dist/assets/use-stack-context-CnA5FTHu.js +1 -0
- package/webapp/dist/index.html +6 -6
- package/dist/commands/dev.d.ts +0 -23
- package/dist/commands/dev.d.ts.map +0 -1
- package/dist/commands/dev.js +0 -76
- package/dist/commands/dev.js.map +0 -1
- package/dist/config/define-app.d.ts +0 -6
- package/dist/config/define-app.d.ts.map +0 -1
- package/dist/config/define-app.js +0 -38
- package/dist/config/define-app.js.map +0 -1
- package/dist/infrastructure/gateway-generator.d.ts +0 -63
- package/dist/infrastructure/gateway-generator.d.ts.map +0 -1
- package/dist/infrastructure/gateway-generator.js +0 -253
- package/dist/infrastructure/gateway-generator.js.map +0 -1
- package/dist/monitoring/health.service.d.ts +0 -14
- package/dist/monitoring/health.service.d.ts.map +0 -1
- package/dist/monitoring/health.service.js +0 -50
- package/dist/monitoring/health.service.js.map +0 -1
- package/dist/monitoring/metrics.service.d.ts +0 -14
- package/dist/monitoring/metrics.service.d.ts.map +0 -1
- package/dist/monitoring/metrics.service.js +0 -40
- package/dist/monitoring/metrics.service.js.map +0 -1
- package/dist/orchestrator/bootstrap-runner.d.ts +0 -15
- package/dist/orchestrator/bootstrap-runner.d.ts.map +0 -1
- package/dist/orchestrator/bootstrap-runner.js +0 -152
- package/dist/orchestrator/bootstrap-runner.js.map +0 -1
- package/dist/orchestrator/topology-proxy.d.ts +0 -65
- package/dist/orchestrator/topology-proxy.d.ts.map +0 -1
- package/dist/orchestrator/topology-proxy.js +0 -209
- package/dist/orchestrator/topology-proxy.js.map +0 -1
- package/dist/orchestrator/topology-relay.d.ts +0 -111
- package/dist/orchestrator/topology-relay.d.ts.map +0 -1
- package/dist/orchestrator/topology-relay.js +0 -255
- package/dist/orchestrator/topology-relay.js.map +0 -1
- package/dist/services/auth-middleware.d.ts +0 -47
- package/dist/services/auth-middleware.d.ts.map +0 -1
- package/dist/services/auth-middleware.js +0 -158
- package/dist/services/auth-middleware.js.map +0 -1
- package/dist/services/environment.rpc-service.d.ts +0 -22
- package/dist/services/environment.rpc-service.d.ts.map +0 -1
- package/dist/services/environment.rpc-service.js +0 -65
- package/dist/services/environment.rpc-service.js.map +0 -1
- package/dist/services/environment.service.d.ts +0 -87
- package/dist/services/environment.service.d.ts.map +0 -1
- package/dist/services/environment.service.js +0 -220
- package/dist/services/environment.service.js.map +0 -1
- package/dist/services/metrics-collector.service.d.ts +0 -103
- package/dist/services/metrics-collector.service.d.ts.map +0 -1
- package/dist/services/metrics-collector.service.js +0 -235
- package/dist/services/metrics-collector.service.js.map +0 -1
- package/dist/services/metrics.rpc-service.d.ts +0 -41
- package/dist/services/metrics.rpc-service.d.ts.map +0 -1
- package/dist/services/metrics.rpc-service.js +0 -90
- package/dist/services/metrics.rpc-service.js.map +0 -1
- package/dist/services/metrics.service.d.ts +0 -72
- package/dist/services/metrics.service.d.ts.map +0 -1
- package/dist/services/metrics.service.js +0 -185
- package/dist/services/metrics.service.js.map +0 -1
- package/dist/supervisor/omnitron-supervisor.d.ts +0 -21
- package/dist/supervisor/omnitron-supervisor.d.ts.map +0 -1
- package/dist/supervisor/omnitron-supervisor.js +0 -43
- package/dist/supervisor/omnitron-supervisor.js.map +0 -1
- package/dist/webapp/static-server.d.ts +0 -24
- package/dist/webapp/static-server.d.ts.map +0 -1
- package/dist/webapp/static-server.js +0 -152
- package/dist/webapp/static-server.js.map +0 -1
- package/webapp/dist/assets/Badge-D2ZBP5iK.js +0 -1
- package/webapp/dist/assets/Card-vO88g06B.js +0 -1
- package/webapp/dist/assets/CardActions-C9h7Q2wW.js +0 -1
- package/webapp/dist/assets/CardContent-BA_Xmmhk.js +0 -1
- package/webapp/dist/assets/CardHeader-ceIs__6x.js +0 -1
- package/webapp/dist/assets/Chip-Ckk0xUhK.js +0 -1
- package/webapp/dist/assets/DialogActions-BhHM7hov.js +0 -1
- package/webapp/dist/assets/DialogContentText-BT2JQRmK.js +0 -1
- package/webapp/dist/assets/FormControlLabel-CcQByVS8.js +0 -1
- package/webapp/dist/assets/Grid-14PETRQW.js +0 -1
- package/webapp/dist/assets/InputAdornment-B8aHIsQ7.js +0 -1
- package/webapp/dist/assets/Skeleton-B74-Ds6O.js +0 -37
- package/webapp/dist/assets/Switch-DvWD2wh4.js +0 -1
- package/webapp/dist/assets/SwitchBase-CDU4nY8O.js +0 -1
- package/webapp/dist/assets/Tab-DcCOYYp5.js +0 -1
- package/webapp/dist/assets/TableContainer-CzTh4mu-.js +0 -1
- package/webapp/dist/assets/TableRow-BySEreZR.js +0 -2
- package/webapp/dist/assets/TextField-BLf-BHz2.js +0 -1
- package/webapp/dist/assets/alerts-DnRbT41C.js +0 -1
- package/webapp/dist/assets/apps-CdR0VH2d.js +0 -1
- package/webapp/dist/assets/chunk-DtcAfS_W.js +0 -1
- package/webapp/dist/assets/client-DU8I_lDc.js +0 -2
- package/webapp/dist/assets/constants-BgnT-Xdy.js +0 -1
- package/webapp/dist/assets/containers-Dw-rRXV1.js +0 -1
- package/webapp/dist/assets/dashboard-CzgB0uZz.js +0 -1
- package/webapp/dist/assets/dashboard-builder-DsA0yg33.js +0 -1
- package/webapp/dist/assets/deployments-DtQAoIcQ.js +0 -1
- package/webapp/dist/assets/detail-BJR-G7JG.js +0 -1
- package/webapp/dist/assets/detail-nlCeSltl.js +0 -8
- package/webapp/dist/assets/formatters-Do41SRmE.js +0 -1
- package/webapp/dist/assets/index-ByQDwp2r.js +0 -566
- package/webapp/dist/assets/index-CHVXOsTS.css +0 -1
- package/webapp/dist/assets/isMuiElement-DwF2naaF.js +0 -1
- package/webapp/dist/assets/logs-Kn8o4H7k.js +0 -11
- package/webapp/dist/assets/metrics-DN8Zu5wT.js +0 -2
- package/webapp/dist/assets/nodes-Bgch7AtN.js +0 -7
- package/webapp/dist/assets/pipelines-Cry3qm-S.js +0 -4
- package/webapp/dist/assets/popper-BJFSj6rc.js +0 -1
- package/webapp/dist/assets/preload-helper-DSXbuxSR.js +0 -1
- package/webapp/dist/assets/projects-Dym2GI1U.js +0 -1
- package/webapp/dist/assets/react-apexcharts.esm-CSGK5Cxn.js +0 -958
- package/webapp/dist/assets/react-apexcharts.esm-DEAWOwx5.js +0 -1003
- package/webapp/dist/assets/settings-va3zTcow.js +0 -1
- package/webapp/dist/assets/sign-in-ik35umbp.js +0 -1
- package/webapp/dist/assets/stacks-CVFOnxbF.js +0 -1
- package/webapp/dist/assets/system-info-npIXAjX2.js +0 -1
- package/webapp/dist/assets/topology-CHpVij2M.css +0 -1
- package/webapp/dist/assets/topology-bDe70vLs.js +0 -12
- package/webapp/dist/assets/traces-DIIcT8DF.js +0 -1
- package/webapp/dist/assets/use-stack-context-DHLwZv1r.js +0 -1
- /package/webapp/dist/assets/{fs-CgZLtHiQ.js → fs-Ccl3sAcp.js} +0 -0
|
@@ -13,23 +13,248 @@
|
|
|
13
13
|
*
|
|
14
14
|
* Artifact path on remote: /opt/omnitron/artifacts/<project>/<app>/<version>/
|
|
15
15
|
* Remote daemon config: /etc/omnitron/omnitron.config.ts
|
|
16
|
+
*
|
|
17
|
+
* NEVER EXECUTED, as of 2026-09-14. Measured rather than assumed: no log in
|
|
18
|
+
* `~/.omnitron/logs` carries a single line this class emits ("Starting
|
|
19
|
+
* deployment to node", "Slave provisioned", "Deployment successful"), no
|
|
20
|
+
* artifact directory exists, and — decisively — no project config declares
|
|
21
|
+
* `stacks.nodes`, which `startRemoteStack` requires before it can reach here.
|
|
22
|
+
* So both entry points are unreachable in every current configuration.
|
|
23
|
+
*
|
|
24
|
+
* This matters to anyone reading the code below and taking it for a working
|
|
25
|
+
* path. It is careful code — arguments are quoted, path segments validated,
|
|
26
|
+
* heredocs replaced with base64 — and none of that has ever met a real host.
|
|
27
|
+
* Its first run will be its first test. Treat a green read of this file as
|
|
28
|
+
* evidence about intent, not about behaviour.
|
|
29
|
+
*
|
|
30
|
+
* The first thing that first run found, 2026-09-14: **it could not log in.**
|
|
31
|
+
* Every command here went through `ssh -o BatchMode=yes` with an optional
|
|
32
|
+
* `-i <keyfile>`, and `BatchMode=yes` disables every interactive method —
|
|
33
|
+
* password and key passphrase both. The console's Add Node dialog collects
|
|
34
|
+
* exactly those, encrypts them in the daemon's vault, and the health monitor
|
|
35
|
+
* uses them through `ExecutionService.ssh()` on every check round. So the
|
|
36
|
+
* product had two SSH implementations: one that can present what the operator
|
|
37
|
+
* gave it, and one — this — that cannot, on the path where it matters most.
|
|
38
|
+
*
|
|
39
|
+
* Measured against the test host, whose SSH answers a password in 311 ms:
|
|
40
|
+
*
|
|
41
|
+
* ssh -o BatchMode=yes root@<host> 'echo ok'
|
|
42
|
+
* → root@<host>: Permission denied (publickey,password).
|
|
43
|
+
*
|
|
44
|
+
* That is the answer to "why can a machine added in the console not be
|
|
45
|
+
* deployed to": not policy, not a missing feature — the deployer could not
|
|
46
|
+
* authenticate as the operator had arranged. It goes through the same
|
|
47
|
+
* `ExecutionService` as the checks now, and takes a `DeployTarget` carrying
|
|
48
|
+
* the credentials rather than an `IStackNode` whose `ISSHConfig` has nowhere
|
|
49
|
+
* to put them.
|
|
16
50
|
*/
|
|
17
|
-
import {
|
|
18
|
-
import {
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
51
|
+
import { PLATFORM_PROBE, parsePlatformProbe, planProvisioning, describePlan, } from './remote-provisioner.js';
|
|
52
|
+
import { installSteps, installPreflightCommand, versionDir, activateSteps, pruneSteps } from './bundle-builder.js';
|
|
53
|
+
import { reachabilityRule, reachabilityCommand } from '../infrastructure/gateway-reachability.js';
|
|
54
|
+
import { readNodeHealth, readNodeStatus } from '../project/node-app-health.js';
|
|
55
|
+
import { decideRedeploy, decideNodeStackStart, decideSlaveDaemonRestart, artifactChanged, readChecksumCommand, parseRecordedChecksum, ARTIFACT_CHECKSUM_FILE, NODE_CONFIG_HASH_FILE, } from './redeploy-decision.js';
|
|
56
|
+
import { deliveredProbeCommand, parseDeliveredProbe, checkDelivered, } from '../release/delivered.js';
|
|
57
|
+
import crypto from 'node:crypto';
|
|
58
|
+
import { AsyncLocalStorage } from 'node:async_hooks';
|
|
59
|
+
import { assertControlledDaemonPort, assertServingBundle, bundleProbeCommand, contractPreflightCommand, executeHostUpgrade, hookCommand, inventoryCommand, planHostUpgrade, receiptCommand, verifyRestoredInventory, updateContractBindingsCommand, runtimeProbeCommand, runtimeStartCommand, runtimeStopCommand, appHealthCommand, pendingReceiptsCommand, recoverHostUpgrade, stoppedDaemonCommand, } from './host-upgrade.js';
|
|
60
|
+
import { gunzipSync } from 'node:zlib';
|
|
61
|
+
import { shellEscape } from '../shared/shell-escape.js';
|
|
62
|
+
import { fromDataChannel, throughDataChannel } from '../execution/data-channel.js';
|
|
63
|
+
/** A node that could not be asked at all — the SSH call itself failed. */
|
|
64
|
+
const UNREADABLE = -3;
|
|
65
|
+
import { withNodeLeases } from './node-deploy-lease.js';
|
|
66
|
+
import { hostOperationBarrierCommand } from './host-operation-barrier.js';
|
|
67
|
+
import { migrationOutput } from './migration-output.js';
|
|
68
|
+
/**
|
|
69
|
+
* A name that is safe to place in a remote filesystem path.
|
|
70
|
+
*
|
|
71
|
+
* Project, app and version names are interpolated into
|
|
72
|
+
* `/opt/omnitron/artifacts/<project>/<app>/<version>` and the result is sent
|
|
73
|
+
* to a remote shell running as the SSH user, which defaults to root. Quoting
|
|
74
|
+
* makes that safe against injection but not against the path itself: a
|
|
75
|
+
* project literally named `../..` resolves outside the artifact root, and
|
|
76
|
+
* `mkdir -p` then creates it there. Quoting is not a substitute for the
|
|
77
|
+
* value meaning what the path assumes it means.
|
|
78
|
+
*
|
|
79
|
+
* These names come from the project registry and the console's own forms —
|
|
80
|
+
* operator-supplied, not attacker-supplied in the ordinary case. It is
|
|
81
|
+
* defence in depth, and the cost of it is a regular expression.
|
|
82
|
+
*
|
|
83
|
+
* @throws Error naming the segment and what is allowed.
|
|
84
|
+
*/
|
|
85
|
+
export function assertRemotePathSegment(kind, value) {
|
|
86
|
+
if (!/^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(value) || value.includes('..')) {
|
|
87
|
+
throw new Error(`Refusing to deploy: ${kind} ${JSON.stringify(value)} is not usable in a remote path. ` +
|
|
88
|
+
`Allowed: letters, digits, dot, dash and underscore, starting with a letter or digit, and no "..".`);
|
|
89
|
+
}
|
|
90
|
+
return value;
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Where an app's artifact lives on a node — one place, because the install
|
|
94
|
+
* writes there and the step that starts the app records there.
|
|
95
|
+
*/
|
|
96
|
+
export function artifactDirOnNode(project, app, version) {
|
|
97
|
+
return (`/opt/omnitron/artifacts/${assertRemotePathSegment('project name', project)}` +
|
|
98
|
+
`/${assertRemotePathSegment('app name', app)}` +
|
|
99
|
+
`/${assertRemotePathSegment('version', version)}`);
|
|
100
|
+
}
|
|
101
|
+
/** The credentials half, in the shape `ExecutionService` takes. */
|
|
102
|
+
function sshTargetOf(target) {
|
|
103
|
+
const ssh = { host: target.host };
|
|
104
|
+
if (target.sshPort != null)
|
|
105
|
+
ssh.port = target.sshPort;
|
|
106
|
+
if (target.username)
|
|
107
|
+
ssh.username = target.username;
|
|
108
|
+
if (target.privateKey)
|
|
109
|
+
ssh.privateKey = target.privateKey;
|
|
110
|
+
if (target.passphrase)
|
|
111
|
+
ssh.passphrase = target.passphrase;
|
|
112
|
+
if (target.password)
|
|
113
|
+
ssh.password = target.password;
|
|
114
|
+
return ssh;
|
|
115
|
+
}
|
|
116
|
+
/** Project a node declared in a project config into a deploy target. */
|
|
117
|
+
export function stackNodeToDeployTarget(node) {
|
|
118
|
+
const target = { host: node.host };
|
|
119
|
+
if (node.port != null)
|
|
120
|
+
target.daemonPort = node.port;
|
|
121
|
+
if (node.ssh?.port != null)
|
|
122
|
+
target.sshPort = node.ssh.port;
|
|
123
|
+
if (node.ssh?.user)
|
|
124
|
+
target.username = node.ssh.user;
|
|
125
|
+
// A path, read by the engine. `ISSHConfig` has no passphrase field, so a
|
|
126
|
+
// key declared here must be one that needs none — which is worth knowing
|
|
127
|
+
// when a deployment from a config fails and the same node works from the
|
|
128
|
+
// console.
|
|
129
|
+
if (node.ssh?.privateKey)
|
|
130
|
+
target.privateKey = node.ssh.privateKey;
|
|
131
|
+
if (node.apps)
|
|
132
|
+
target.apps = node.apps;
|
|
133
|
+
if (node.label)
|
|
134
|
+
target.label = node.label;
|
|
135
|
+
return target;
|
|
23
136
|
}
|
|
137
|
+
/**
|
|
138
|
+
* Fill a stack node's connection details from the node registry.
|
|
139
|
+
*
|
|
140
|
+
* A stack says WHICH host an app runs on; the node registry says HOW to
|
|
141
|
+
* reach it — the user, the port, and the key or password, which are held
|
|
142
|
+
* encrypted in the daemon's vault because they are credentials and a config
|
|
143
|
+
* file in a repository is not where those go.
|
|
144
|
+
*
|
|
145
|
+
* Nothing joined the two. `stackNodeToDeployTarget` read only the stack's
|
|
146
|
+
* own `ssh` block, so deploying to a machine that the console had already
|
|
147
|
+
* registered, provisioned and been connected to failed at the first
|
|
148
|
+
* connection:
|
|
149
|
+
*
|
|
150
|
+
* Invalid SSH options: Either privateKey or password must be provided
|
|
151
|
+
*
|
|
152
|
+
* — for a node the same daemon was, at that moment, holding an SSH tunnel to.
|
|
153
|
+
*
|
|
154
|
+
* The stack's explicit values win where it has them: an operator who wrote
|
|
155
|
+
* `ssh.user` in the stack config meant it, and a registry entry is the
|
|
156
|
+
* default, not an override. Everything the stack leaves out comes from the
|
|
157
|
+
* registry.
|
|
158
|
+
*/
|
|
159
|
+
export function withNodeCredentials(target, registered) {
|
|
160
|
+
if (!registered)
|
|
161
|
+
return target;
|
|
162
|
+
const merged = { ...target };
|
|
163
|
+
if (merged.sshPort == null && registered.port != null)
|
|
164
|
+
merged.sshPort = registered.port;
|
|
165
|
+
if (!merged.username && registered.username)
|
|
166
|
+
merged.username = registered.username;
|
|
167
|
+
if (!merged.privateKey && registered.privateKey)
|
|
168
|
+
merged.privateKey = registered.privateKey;
|
|
169
|
+
if (!merged.passphrase && registered.passphrase)
|
|
170
|
+
merged.passphrase = registered.passphrase;
|
|
171
|
+
if (!merged.password && registered.password)
|
|
172
|
+
merged.password = registered.password;
|
|
173
|
+
return merged;
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* How long a freshly provisioned slave has to answer.
|
|
177
|
+
*
|
|
178
|
+
* A daemon start is an application boot: a DI container, a module graph, a
|
|
179
|
+
* SQLite open. On a loaded host this is tens of seconds, and the old
|
|
180
|
+
* three-second sleep was shorter than the work by an order of magnitude.
|
|
181
|
+
*/
|
|
182
|
+
const SLAVE_START_TIMEOUT_MS = 120_000;
|
|
183
|
+
/** How often it is asked, while it is starting. */
|
|
184
|
+
const DAEMON_POLL_MS = 3_000;
|
|
24
185
|
// =============================================================================
|
|
25
186
|
// RemoteDeployer
|
|
26
187
|
// =============================================================================
|
|
188
|
+
/**
|
|
189
|
+
* The parts of a postgres URL, for the environment a migrator expects.
|
|
190
|
+
*
|
|
191
|
+
* Returns null rather than guessing at a URL it cannot read: a migration run
|
|
192
|
+
* against half-parsed connection details is worse than one that does not run,
|
|
193
|
+
* because it can succeed somewhere nobody meant.
|
|
194
|
+
*/
|
|
195
|
+
function parseDatabaseUrl(url) {
|
|
196
|
+
try {
|
|
197
|
+
const u = new URL(url);
|
|
198
|
+
if (!u.protocol.startsWith('postgres'))
|
|
199
|
+
return null;
|
|
200
|
+
return {
|
|
201
|
+
host: u.hostname,
|
|
202
|
+
port: u.port || '5432',
|
|
203
|
+
database: decodeURIComponent(u.pathname.replace(/^\//, '')),
|
|
204
|
+
user: decodeURIComponent(u.username),
|
|
205
|
+
password: decodeURIComponent(u.password),
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
catch {
|
|
209
|
+
return null;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
/**
|
|
213
|
+
* Is this a gzip stream of a COMPLETE tar archive?
|
|
214
|
+
*
|
|
215
|
+
* Two ways to be short, told apart: a gzip stream cut off fails to
|
|
216
|
+
* decompress («unexpected end of file»), and a tar cut off inside a gzip
|
|
217
|
+
* that finished — `tar` killed, `gzip` still closing its own stream —
|
|
218
|
+
* decompresses to something that is not a whole number of 512-byte blocks,
|
|
219
|
+
* or does not end in the two zero blocks every tar writer closes an archive
|
|
220
|
+
* with. Answers `true` or the reason it is not.
|
|
221
|
+
*/
|
|
222
|
+
export function wholeTarGz(archive) {
|
|
223
|
+
let tar;
|
|
224
|
+
try {
|
|
225
|
+
tar = gunzipSync(archive);
|
|
226
|
+
}
|
|
227
|
+
catch (err) {
|
|
228
|
+
return `the gzip stream is broken: ${err.message}`;
|
|
229
|
+
}
|
|
230
|
+
if (tar.length < 1024 || tar.length % 512 !== 0)
|
|
231
|
+
return `the tar inside is ${tar.length} bytes, not whole blocks`;
|
|
232
|
+
return tar.subarray(tar.length - 1024).every((b) => b === 0) ? true : 'the tar inside has no end-of-archive marker';
|
|
233
|
+
}
|
|
27
234
|
export class RemoteDeployer {
|
|
28
235
|
logger;
|
|
236
|
+
execution;
|
|
237
|
+
deploymentLeases = new AsyncLocalStorage();
|
|
29
238
|
/** Active deployment progress handlers */
|
|
30
239
|
progressHandlers = [];
|
|
31
|
-
|
|
240
|
+
/**
|
|
241
|
+
* The last thing that happened to each app on each node.
|
|
242
|
+
*
|
|
243
|
+
* A deployment publishes progress as it goes and the console polls; without
|
|
244
|
+
* somewhere for an event to WAIT, those two never meet, and a deployment
|
|
245
|
+
* that takes a quarter of an hour showed the operator one row that said
|
|
246
|
+
* `deploying` from beginning to end. Everything that said WHERE it was —
|
|
247
|
+
* transferring, installing, the message naming the failing step — reached a
|
|
248
|
+
* handler that re-emitted it to nobody.
|
|
249
|
+
*
|
|
250
|
+
* Keyed by node and app, so this is bounded by the fleet rather than by how
|
|
251
|
+
* often anyone deploys, and the entry for an app is always its current
|
|
252
|
+
* state rather than a scroll an operator has to read to the end of.
|
|
253
|
+
*/
|
|
254
|
+
lastProgress = new Map();
|
|
255
|
+
constructor(logger, execution) {
|
|
32
256
|
this.logger = logger;
|
|
257
|
+
this.execution = execution;
|
|
33
258
|
}
|
|
34
259
|
/**
|
|
35
260
|
* Register a progress handler for real-time deployment updates.
|
|
@@ -45,34 +270,266 @@ export class RemoteDeployer {
|
|
|
45
270
|
/**
|
|
46
271
|
* Deploy an artifact to a single remote node.
|
|
47
272
|
*/
|
|
48
|
-
async deployToNode(
|
|
273
|
+
async deployToNode(target, artifact, project, options) {
|
|
49
274
|
const startTime = Date.now();
|
|
50
|
-
const nodeKey = `${
|
|
275
|
+
const nodeKey = `${target.host}:${target.daemonPort ?? 9700}`;
|
|
51
276
|
this.logger.info({ node: nodeKey, app: artifact.app, version: artifact.version }, 'Starting deployment to node');
|
|
52
277
|
try {
|
|
53
278
|
// 1. Verify SSH connectivity
|
|
54
279
|
this.emitProgress(nodeKey, artifact.app, 'pending', 0, 'Connecting via SSH...');
|
|
55
|
-
await this.verifySSH(
|
|
280
|
+
await this.verifySSH(target);
|
|
56
281
|
// 2. Ensure remote directory structure
|
|
57
|
-
const remotePath =
|
|
58
|
-
await this.sshExec(
|
|
282
|
+
const remotePath = artifactDirOnNode(project, artifact.app, artifact.version);
|
|
283
|
+
await this.sshExec(target, `mkdir -p ${shellEscape(remotePath)}`);
|
|
284
|
+
// 2a. What the node already has.
|
|
285
|
+
//
|
|
286
|
+
// A master's boot-time autostart deploys every remote stack again, so
|
|
287
|
+
// the same artifact was rebuilt, transferred, unpacked, `npm
|
|
288
|
+
// install`ed and its app restarted on every master restart — four
|
|
289
|
+
// times in one afternoon here, twenty-four application restarts on a
|
|
290
|
+
// node whose files were identical each time.
|
|
291
|
+
//
|
|
292
|
+
// The comparison is the artifact's own sha256 against the one the node
|
|
293
|
+
// recorded when the app last came up healthy on it. `options.force`
|
|
294
|
+
// deploys anyway, for an operator who has reason to doubt the record —
|
|
295
|
+
// `omnitron stack start --reinstall`, which until 2026-09-24 nothing
|
|
296
|
+
// could pass: the escape hatch this comment named had no door.
|
|
297
|
+
if (options?.force !== true) {
|
|
298
|
+
const recorded = parseRecordedChecksum(await this.sshExec(target, readChecksumCommand(shellEscape(remotePath))));
|
|
299
|
+
if (artifactChanged(recorded, artifact.checksum) === false) {
|
|
300
|
+
const duration = Date.now() - startTime;
|
|
301
|
+
this.emitProgress(nodeKey, artifact.app, 'success', 75, 'Already installed — unchanged');
|
|
302
|
+
this.logger.info({ node: nodeKey, app: artifact.app, version: artifact.version, sha: recorded.slice(0, 12) }, 'The node already has this artifact — not transferring it again');
|
|
303
|
+
return {
|
|
304
|
+
node: nodeKey,
|
|
305
|
+
app: artifact.app,
|
|
306
|
+
version: artifact.version,
|
|
307
|
+
status: 'success',
|
|
308
|
+
duration,
|
|
309
|
+
unchanged: true,
|
|
310
|
+
};
|
|
311
|
+
}
|
|
312
|
+
}
|
|
59
313
|
// 3. Transfer artifact
|
|
60
314
|
this.emitProgress(nodeKey, artifact.app, 'transferring', 20, 'Transferring artifact...');
|
|
61
315
|
const remoteFile = `${remotePath}/${artifact.app}-${artifact.version}.tar.gz`;
|
|
62
|
-
await this.scpTransfer(
|
|
63
|
-
//
|
|
316
|
+
await this.scpTransfer(target, artifact.path, remoteFile);
|
|
317
|
+
// 3b. And ask the node whether what it has is what was sent.
|
|
318
|
+
//
|
|
319
|
+
// This used to go straight to `tar -xzf`. The `.artifact-sha256`
|
|
320
|
+
// written further down reads like this check and is not one: it holds
|
|
321
|
+
// `artifact.checksum`, the hash of the build INPUTS, it is written
|
|
322
|
+
// AFTER the unpack, and it is compared against the next build rather
|
|
323
|
+
// than against the file. So a truncated transfer reached tar, which
|
|
324
|
+
// failed with whatever tar says about a corrupt archive, and one that
|
|
325
|
+
// happened to end on a record boundary unpacked a subset and reported
|
|
326
|
+
// success.
|
|
327
|
+
//
|
|
328
|
+
// Skipped only when the artifact carries no tarball hash — an
|
|
329
|
+
// artifact found on disk by `listArtifacts` rather than built here.
|
|
330
|
+
// That is an absence of information, not a verdict, and it is said out
|
|
331
|
+
// loud rather than passed over in silence.
|
|
332
|
+
if (artifact.tarballSha256) {
|
|
333
|
+
const probe = await this.sshExec(target, deliveredProbeCommand(remoteFile));
|
|
334
|
+
const verdict = checkDelivered({ sha256: artifact.tarballSha256, bytes: artifact.size }, parseDeliveredProbe(probe));
|
|
335
|
+
if (!verdict.ok) {
|
|
336
|
+
const duration = Date.now() - startTime;
|
|
337
|
+
this.emitProgress(nodeKey, artifact.app, 'failed', 30, verdict.because);
|
|
338
|
+
this.logger.error({ node: nodeKey, app: artifact.app, file: remoteFile }, `Artifact transfer failed verification — ${verdict.because}`);
|
|
339
|
+
return {
|
|
340
|
+
node: nodeKey,
|
|
341
|
+
app: artifact.app,
|
|
342
|
+
version: artifact.version,
|
|
343
|
+
status: 'failed',
|
|
344
|
+
duration,
|
|
345
|
+
error: verdict.because,
|
|
346
|
+
};
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
else {
|
|
350
|
+
this.logger.warn({ node: nodeKey, app: artifact.app }, 'Artifact carries no tarball hash — the transfer was not verified');
|
|
351
|
+
}
|
|
352
|
+
// 4. Extract on remote, into an empty directory.
|
|
353
|
+
//
|
|
354
|
+
// `tar -xzf` over whatever was there before leaves everything the new
|
|
355
|
+
// archive does not name — and what was there before is a `node_modules`
|
|
356
|
+
// of a different SHAPE. The first deployment after this builder changed
|
|
357
|
+
// put an npm manifest beside a pnpm tree and `npm install` answered
|
|
358
|
+
//
|
|
359
|
+
// npm error Cannot read properties of null (reading 'edgesOut')
|
|
360
|
+
//
|
|
361
|
+
// which is npm walking a dependency graph through symlinks it did not
|
|
362
|
+
// create. The same reasoning as `installSteps`, which removes the
|
|
363
|
+
// version directory before unpacking for exactly this: an install
|
|
364
|
+
// reconciling against a tree from another tool is not this version.
|
|
365
|
+
//
|
|
366
|
+
// Everything except the archive just uploaded, and only inside the
|
|
367
|
+
// artifact's own version directory — every segment of which was checked
|
|
368
|
+
// by `assertRemotePathSegment` above.
|
|
64
369
|
this.emitProgress(nodeKey, artifact.app, 'extracting', 50, 'Extracting artifact...');
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
//
|
|
370
|
+
const archiveName = `${artifact.app}-${artifact.version}.tar.gz`;
|
|
371
|
+
await this.sshExec(target, `cd ${shellEscape(remotePath)} && ` +
|
|
372
|
+
`find . -mindepth 1 -maxdepth 1 ! -name ${shellEscape(archiveName)} -exec rm -rf {} + && ` +
|
|
373
|
+
`tar -xzf ${shellEscape(archiveName)}`);
|
|
374
|
+
// 5. Install the dependencies ON THE NODE.
|
|
375
|
+
//
|
|
376
|
+
// Three shapes of this step, and the two that were wrong are worth
|
|
377
|
+
// keeping written down because each looked finished.
|
|
378
|
+
//
|
|
379
|
+
// It first ran `npm install --production --ignore-scripts 2>/dev/null
|
|
380
|
+
// || true`. That could not work even once — the manifests use pnpm's
|
|
381
|
+
// `workspace:` protocol, npm answers `EUNSUPPORTEDPROTOCOL` — and the
|
|
382
|
+
// `|| true` meant nobody found out.
|
|
383
|
+
//
|
|
384
|
+
// It then trusted the artifact to arrive complete, from `pnpm deploy`,
|
|
385
|
+
// and checked `test -d node_modules`. The artifact was NOT complete:
|
|
386
|
+
// every `@omnitron-dev/*` package was a symlink into a home directory
|
|
387
|
+
// on the master, twenty-three of them, and a directory of dangling
|
|
388
|
+
// symlinks passes `test -d`. A symlink to `/opt/omnitron/current`
|
|
389
|
+
// patched the one the first import happened to name; the other
|
|
390
|
+
// twenty-two were waiting behind it.
|
|
391
|
+
//
|
|
392
|
+
// Now the artifact carries `vendor/*.tgz` and a manifest that installs
|
|
393
|
+
// them, exactly like the daemon's own bundle, and the install runs
|
|
394
|
+
// here. That is not a cost to be minimised — it is the only step that
|
|
395
|
+
// happens on a machine that knows it is Linux, which is what decides
|
|
396
|
+
// between `@esbuild/darwin-arm64` and `@esbuild/linux-x64`.
|
|
397
|
+
this.emitProgress(nodeKey, artifact.app, 'installing', 60, 'Installing dependencies on the node...');
|
|
398
|
+
try {
|
|
399
|
+
await this.sshExec(target, `cd ${shellEscape(remotePath)} && npm install --omit=dev --no-audit --no-fund`, 900_000);
|
|
400
|
+
}
|
|
401
|
+
catch (err) {
|
|
402
|
+
const duration = Date.now() - startTime;
|
|
403
|
+
const detail = `dependency install failed on the node: ${err.message.slice(0, 400)}`;
|
|
404
|
+
this.emitProgress(nodeKey, artifact.app, 'failed', 60, detail);
|
|
405
|
+
this.logger.error({ node: nodeKey, app: artifact.app, path: remotePath }, detail);
|
|
406
|
+
return { node: nodeKey, app: artifact.app, version: artifact.version, status: 'failed', duration, error: detail };
|
|
407
|
+
}
|
|
408
|
+
// 5a. One physical copy of every package the daemon shares with the app.
|
|
409
|
+
//
|
|
410
|
+
// A supervised app does not run alone in its process: omnitron's own
|
|
411
|
+
// `bootstrap-process.js` is the entry point, and the app's modules are
|
|
412
|
+
// loaded into it. Titan keeps identities at module scope — the
|
|
413
|
+
// `Container` class among them — so two physical installations of
|
|
414
|
+
// `@omnitron-dev/titan` in one process means `@Inject(Container)`
|
|
415
|
+
// resolves against a class that is not the one the daemon registered,
|
|
416
|
+
// and the failure reads `Dependency 'Container' not found`.
|
|
417
|
+
//
|
|
418
|
+
// `bootstrap-process.ts` checks for exactly this and refuses to start,
|
|
419
|
+
// which is how a freshly installed artifact answered:
|
|
420
|
+
//
|
|
421
|
+
// Titan package identity mismatch detected between omnitron daemon
|
|
422
|
+
// and app 'geo'. Daemon resolves @omnitron-dev/titan from
|
|
423
|
+
// /opt/omnitron/versions/…; App resolves … from /opt/omnitron/
|
|
424
|
+
// artifacts/sample/geo/0.0.1/node_modules/@omnitron-dev/titan
|
|
425
|
+
//
|
|
426
|
+
// That is the correct refusal, and the answer is not to install less:
|
|
427
|
+
// the vendored copies are what let `npm install` resolve a coherent
|
|
428
|
+
// tree in the first place. They are replaced afterwards by links to
|
|
429
|
+
// the daemon's own, computed ON THE NODE from what the daemon actually
|
|
430
|
+
// has, so the set stays right as the daemon's closure changes rather
|
|
431
|
+
// than being a list here that drifts from it.
|
|
432
|
+
//
|
|
433
|
+
// `@omnitron-dev/omnitron` is deliberately NOT one of them, and this is
|
|
434
|
+
// the second time that lesson has been paid for. The daemon IS that
|
|
435
|
+
// package, so it is not inside its own `node_modules`, and linking to
|
|
436
|
+
// the install root instead gives node a directory whose package.json
|
|
437
|
+
// carries `name`, `version`, `type` and `dependencies` and NO `main`
|
|
438
|
+
// and NO `exports` — `bundleRootManifest` writes an install manifest,
|
|
439
|
+
// not a library manifest. Resolution then falls back to `index.js`,
|
|
440
|
+
// which is not there:
|
|
441
|
+
//
|
|
442
|
+
// Cannot find package
|
|
443
|
+
// '.../node_modules/@omnitron-dev/omnitron/index.js'
|
|
444
|
+
//
|
|
445
|
+
// — the exact error this whole line of work started from. The vendored
|
|
446
|
+
// copy that `pnpm pack` produces from `apps/omnitron` has the real
|
|
447
|
+
// manifest, so it is left alone. Its identity is not shared across the
|
|
448
|
+
// process boundary the way titan's is: `defineSystem` returns data that
|
|
449
|
+
// the daemon's bootstrap reads, not a class anything does `instanceof`
|
|
450
|
+
// against.
|
|
451
|
+
//
|
|
452
|
+
// `rm -rf` before each `ln -sfn`: linking onto an existing DIRECTORY
|
|
453
|
+
// puts the link INSIDE it, leaving a directory with no package.json
|
|
454
|
+
// that node reports as a missing `index.js` — a message about a file
|
|
455
|
+
// that was never the problem.
|
|
456
|
+
const daemonRoot = (await this.inspectInstalledBundle(target)).root;
|
|
457
|
+
await this.sshExec(target, `set -e; APP=${shellEscape(`${remotePath}/node_modules/@omnitron-dev`)}; ` +
|
|
458
|
+
`mkdir -p "$APP"; ` +
|
|
459
|
+
`for p in ${shellEscape(daemonRoot)}/node_modules/@omnitron-dev/*; do ` +
|
|
460
|
+
`[ -e "$p" ] || continue; n=$(basename "$p"); ` +
|
|
461
|
+
`rm -rf "$APP/$n"; ln -sfn "$p" "$APP/$n"; done`);
|
|
462
|
+
// The install either produced a tree that resolves or it did not, and
|
|
463
|
+
// `test -d node_modules` cannot tell those apart — it was true for the
|
|
464
|
+
// whole time nothing worked. Asking node to RESOLVE the package the
|
|
465
|
+
// app imports first is the smallest question with a real answer.
|
|
466
|
+
//
|
|
467
|
+
// The package, not a file inside it: my first version of this resolved
|
|
468
|
+
// `@omnitron-dev/omnitron/package.json` and a correct install answered
|
|
469
|
+
// `ERR_PACKAGE_PATH_NOT_EXPORTED`, because a package with an `exports`
|
|
470
|
+
// map publishes what it lists and nothing else. A probe that fails on
|
|
471
|
+
// a healthy tree is worse than none — it would have failed every
|
|
472
|
+
// deployment from here on, for the one reason that is not a fault.
|
|
473
|
+
this.emitProgress(nodeKey, artifact.app, 'extracting', 70, 'Checking dependencies...');
|
|
474
|
+
const resolved = await this.sshExec(target, `cd ${shellEscape(remotePath)} && node -e ${shellEscape("const r=require('module').createRequire(process.cwd()+'/package.json');" +
|
|
475
|
+
"r.resolve('@omnitron-dev/omnitron');r.resolve('@omnitron-dev/titan')")} >/dev/null 2>&1 && echo resolves || echo broken`).catch(() => 'broken');
|
|
476
|
+
if (resolved.trim() !== 'resolves') {
|
|
477
|
+
const duration = Date.now() - startTime;
|
|
478
|
+
const detail = 'the installed tree cannot resolve @omnitron-dev/omnitron or @omnitron-dev/titan, ' +
|
|
479
|
+
'so the app will fail at its first import';
|
|
480
|
+
this.emitProgress(nodeKey, artifact.app, 'failed', 70, detail);
|
|
481
|
+
this.logger.error({ node: nodeKey, app: artifact.app, path: remotePath }, detail);
|
|
482
|
+
return { node: nodeKey, app: artifact.app, version: artifact.version, status: 'failed', duration, error: detail };
|
|
483
|
+
}
|
|
484
|
+
// 5b. No record yet. The next deployment reads `.artifact-sha256` as
|
|
485
|
+
// «the app runs this», and at this point it does not: the files are
|
|
486
|
+
// unpacked, the process still runs what it started with. The record
|
|
487
|
+
// used to be written here, and on sample/test (2026-09-24) a deployment
|
|
488
|
+
// that stalled after installing three artifacts left them recorded;
|
|
489
|
+
// the retry read «installed, running» and left all three on the code
|
|
490
|
+
// they had started with an hour earlier. It is written once the app
|
|
491
|
+
// has come up healthy on these files — `recordRunningArtifact`.
|
|
492
|
+
// 6. Installed. Starting is a SEPARATE phase, and the order matters:
|
|
493
|
+
// a node cannot start an app it has no definition for, and the
|
|
494
|
+
// definitions are written once all the artifacts have landed. Starting
|
|
495
|
+
// here made the two mutually exclusive — the restart failed with
|
|
496
|
+
// `Unknown app`, the result was marked failed, and the registration,
|
|
497
|
+
// which only writes apps whose deployment succeeded, wrote nothing.
|
|
498
|
+
if (options?.startAfterInstall === false) {
|
|
499
|
+
const duration = Date.now() - startTime;
|
|
500
|
+
this.emitProgress(nodeKey, artifact.app, 'success', 75, 'Installed');
|
|
501
|
+
return {
|
|
502
|
+
node: nodeKey,
|
|
503
|
+
app: artifact.app,
|
|
504
|
+
version: artifact.version,
|
|
505
|
+
status: 'success',
|
|
506
|
+
duration,
|
|
507
|
+
...(artifact.checksum ? { checksum: artifact.checksum } : {}),
|
|
508
|
+
};
|
|
509
|
+
}
|
|
70
510
|
this.emitProgress(nodeKey, artifact.app, 'restarting', 80, 'Restarting app on remote...');
|
|
71
|
-
await this.signalRemoteDaemon(
|
|
511
|
+
const started = await this.signalRemoteDaemon(target, artifact.app);
|
|
72
512
|
// 7. Verify health
|
|
73
513
|
this.emitProgress(nodeKey, artifact.app, 'verifying', 90, 'Verifying health...');
|
|
74
|
-
await this.verifyHealth(
|
|
514
|
+
const health = await this.verifyHealth(target, artifact.app, project);
|
|
75
515
|
const duration = Date.now() - startTime;
|
|
516
|
+
// An artifact on disk is not a running application. Reporting success
|
|
517
|
+
// for the transfer alone is what let a fleet show six deployed apps and
|
|
518
|
+
// run none of them.
|
|
519
|
+
if (!started.ok || !health.online) {
|
|
520
|
+
const why = !started.ok ? started.detail : health.detail;
|
|
521
|
+
this.emitProgress(nodeKey, artifact.app, 'failed', 90, `artifact installed, app not running: ${why}`);
|
|
522
|
+
this.logger.error({ node: nodeKey, app: artifact.app, version: artifact.version, duration, detail: why }, 'Artifact installed, but the app is not running on the node');
|
|
523
|
+
return {
|
|
524
|
+
node: nodeKey,
|
|
525
|
+
app: artifact.app,
|
|
526
|
+
version: artifact.version,
|
|
527
|
+
status: 'failed',
|
|
528
|
+
duration,
|
|
529
|
+
error: `artifact installed, app not running: ${why}`,
|
|
530
|
+
};
|
|
531
|
+
}
|
|
532
|
+
await this.recordRunningArtifact(target, remotePath, artifact.app, artifact.checksum);
|
|
76
533
|
this.emitProgress(nodeKey, artifact.app, 'success', 100, `Deployed in ${Math.round(duration / 1000)}s`);
|
|
77
534
|
this.logger.info({ node: nodeKey, app: artifact.app, version: artifact.version, duration }, 'Deployment successful');
|
|
78
535
|
return { node: nodeKey, app: artifact.app, version: artifact.version, status: 'success', duration };
|
|
@@ -88,24 +545,29 @@ export class RemoteDeployer {
|
|
|
88
545
|
/**
|
|
89
546
|
* Deploy artifacts to all nodes in a stack (parallel).
|
|
90
547
|
*/
|
|
91
|
-
async deployToStack(
|
|
548
|
+
async deployToStack(targets, artifacts, project, options) {
|
|
92
549
|
const concurrency = options?.concurrency ?? 3;
|
|
93
550
|
const results = [];
|
|
94
551
|
// Build deployment matrix: each app to each node (or node-specific apps)
|
|
95
552
|
const tasks = [];
|
|
96
|
-
for (const
|
|
553
|
+
for (const target of targets) {
|
|
97
554
|
for (const artifact of artifacts) {
|
|
98
555
|
// If node has explicit app list, only deploy matching apps
|
|
99
|
-
if (
|
|
556
|
+
if (target.apps && !target.apps.includes(artifact.app))
|
|
100
557
|
continue;
|
|
101
|
-
tasks.push({
|
|
558
|
+
tasks.push({ target, artifact });
|
|
102
559
|
}
|
|
103
560
|
}
|
|
104
561
|
// Execute with concurrency limit
|
|
105
562
|
const executing = new Set();
|
|
106
563
|
for (const task of tasks) {
|
|
107
564
|
const promise = (async () => {
|
|
108
|
-
|
|
565
|
+
// Install only when the definitions are coming: the app cannot be
|
|
566
|
+
// started before the node knows it exists.
|
|
567
|
+
const result = await this.deployToNode(task.target, task.artifact, project, {
|
|
568
|
+
startAfterInstall: !options?.apps?.length,
|
|
569
|
+
...(options?.force === true ? { force: true } : {}),
|
|
570
|
+
});
|
|
109
571
|
results.push(result);
|
|
110
572
|
})();
|
|
111
573
|
executing.add(promise);
|
|
@@ -115,8 +577,392 @@ export class RemoteDeployer {
|
|
|
115
577
|
}
|
|
116
578
|
}
|
|
117
579
|
await Promise.all(executing);
|
|
580
|
+
// Tell each node what it now has. After the transfers, because a config
|
|
581
|
+
// naming an artifact that has not landed yet is a config the node will
|
|
582
|
+
// fail on — and only for the apps that actually arrived, which is what
|
|
583
|
+
// `results` knows and the artifact list does not.
|
|
584
|
+
if (options?.apps?.length) {
|
|
585
|
+
for (const target of targets) {
|
|
586
|
+
// The SAME key `deployToNode` builds — `host:daemonPort`. Comparing
|
|
587
|
+
// against the SSH port here matched nothing, so the registration and
|
|
588
|
+
// the start phase would both have run over an empty list and reported
|
|
589
|
+
// nothing wrong.
|
|
590
|
+
const nodeKey = `${target.host}:${target.daemonPort ?? 9700}`;
|
|
591
|
+
const landed = results
|
|
592
|
+
.filter((r) => r.node === nodeKey && r.status === 'success')
|
|
593
|
+
.map((r) => ({ app: r.app, version: r.version }));
|
|
594
|
+
const registration = await this.registerNodeApps(target, project, options.apps, landed, options.appEnv, options.alertSink);
|
|
595
|
+
// Schema before the apps that read it — and an app whose schema did
|
|
596
|
+
// not arrive does not get its new code started on the old one.
|
|
597
|
+
const migrationFailed = await this.migrateNodeApps(target, project, landed, options.appEnv);
|
|
598
|
+
// And the path from the gateway to them, which is the one crossing
|
|
599
|
+
// no configuration can open by itself. See `gateway-reachability`.
|
|
600
|
+
if (options.stack)
|
|
601
|
+
await this.openGatewayPath(target, project, options.stack);
|
|
602
|
+
// What the node is running right now, asked once for the whole node
|
|
603
|
+
// rather than once per app: the decision below needs it for every
|
|
604
|
+
// app, and `omnitron status --json` answers about all of them.
|
|
605
|
+
const runningNow = await this.appsOnline(target, project);
|
|
606
|
+
// Now that the node knows what these apps are, start them. Their
|
|
607
|
+
// result is upgraded in place, so a caller reading `results` sees
|
|
608
|
+
// running apps rather than installed files.
|
|
609
|
+
for (const entry of landed) {
|
|
610
|
+
const result = results.find((r) => r.app === entry.app && r.node === nodeKey);
|
|
611
|
+
if (!result)
|
|
612
|
+
continue;
|
|
613
|
+
// Before anything decides to restart it. The code that was just
|
|
614
|
+
// unpacked expects the schema its migrations produce; starting it
|
|
615
|
+
// on the schema they failed to produce is the one outcome worse
|
|
616
|
+
// than not deploying — it can come up healthy and fail on a request.
|
|
617
|
+
const schemaError = migrationFailed.get(entry.app);
|
|
618
|
+
if (schemaError !== undefined) {
|
|
619
|
+
result.status = 'failed';
|
|
620
|
+
result.error = `database migrations failed, so the new code was not started: ${schemaError}`;
|
|
621
|
+
this.emitProgress(result.node, entry.app, 'failed', 80, result.error);
|
|
622
|
+
continue;
|
|
623
|
+
}
|
|
624
|
+
// An app whose artifact did not move, whose configuration did not
|
|
625
|
+
// move, and which is running, has nothing that a restart would
|
|
626
|
+
// change — and a restart is downtime. See `decideRedeploy`.
|
|
627
|
+
const decision = decideRedeploy({
|
|
628
|
+
// `unchanged` is set by `deployToNode` when the node's recorded
|
|
629
|
+
// checksum matched the artifact's; anything else means files
|
|
630
|
+
// were written.
|
|
631
|
+
artifactChanged: result.unchanged === true ? false : true,
|
|
632
|
+
online: runningNow.has(entry.app),
|
|
633
|
+
configChanged: registration.changed,
|
|
634
|
+
});
|
|
635
|
+
if (decision.action === 'leave') {
|
|
636
|
+
this.emitProgress(result.node, entry.app, 'success', 100, 'Running — nothing to change');
|
|
637
|
+
this.logger.info({ node: result.node, app: entry.app, version: entry.version, because: decision.because }, 'Left running — this deployment changes nothing for this app');
|
|
638
|
+
continue;
|
|
639
|
+
}
|
|
640
|
+
const started = await this.signalRemoteDaemon(target, entry.app);
|
|
641
|
+
const health = started.ok
|
|
642
|
+
? await this.verifyHealth(target, entry.app, project)
|
|
643
|
+
: { online: false, detail: started.detail };
|
|
644
|
+
if (health.online) {
|
|
645
|
+
await this.recordRunningArtifact(target, artifactDirOnNode(project, entry.app, entry.version), entry.app, result.checksum);
|
|
646
|
+
this.emitProgress(result.node, entry.app, 'success', 100, 'Running');
|
|
647
|
+
// Said, because its opposite is. «Left running» above logs when
|
|
648
|
+
// this deployment leaves an application alone; restarting one
|
|
649
|
+
// logged nothing at all, so a log full of six «Left running»
|
|
650
|
+
// lines and a log of six silent restarts differ only by the
|
|
651
|
+
// absence of something — and absence is what a reader cannot
|
|
652
|
+
// see. Measured 2026-09-22: six artifacts delivered and every
|
|
653
|
+
// application restarted, with nothing in the log between
|
|
654
|
+
// «Opened the gateway's path» and «Stack started» to say so.
|
|
655
|
+
this.logger.info({
|
|
656
|
+
node: result.node,
|
|
657
|
+
app: entry.app,
|
|
658
|
+
version: entry.version,
|
|
659
|
+
because: decision.because,
|
|
660
|
+
}, 'Restarted onto the artifact this deployment installed');
|
|
661
|
+
continue;
|
|
662
|
+
}
|
|
663
|
+
// Installed and not running is a failure, and it is the state this
|
|
664
|
+
// fleet reported as success for as long as it has existed.
|
|
665
|
+
result.status = 'failed';
|
|
666
|
+
result.error = `artifact installed, app not running: ${started.ok ? health.detail : started.detail}`;
|
|
667
|
+
this.emitProgress(result.node, entry.app, 'failed', 90, result.error);
|
|
668
|
+
this.logger.error({ node: result.node, app: entry.app, version: entry.version, detail: result.error }, 'Artifact installed, but the app is not running on the node');
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
}
|
|
118
672
|
return results;
|
|
119
673
|
}
|
|
674
|
+
/**
|
|
675
|
+
* Give a node the config that tells it what to run.
|
|
676
|
+
*
|
|
677
|
+
* Written, then registered. Registering a path with no config in it leaves
|
|
678
|
+
* the daemon with a project it cannot read, which reports as a broken
|
|
679
|
+
* project rather than a missing file.
|
|
680
|
+
*
|
|
681
|
+
* Reported and not thrown: artifacts that transferred are on the node
|
|
682
|
+
* either way, and failing the whole deployment over the registration would
|
|
683
|
+
* discard a transfer that succeeded. The operator needs to know the apps
|
|
684
|
+
* are not runnable, which is what the error says.
|
|
685
|
+
*/
|
|
686
|
+
/**
|
|
687
|
+
* Let the gateway reach the applications it proxies to.
|
|
688
|
+
*
|
|
689
|
+
* The gateway is a container and the apps are host processes, so every
|
|
690
|
+
* `/api/*` request crosses out of docker and into the host's INPUT chain.
|
|
691
|
+
* On a hardened host that chain drops it, and the gateway answers a
|
|
692
|
+
* correct, unhelpful `503` three seconds later while the upstream it could
|
|
693
|
+
* not reach is listening and healthy on the same machine.
|
|
694
|
+
*
|
|
695
|
+
* Measured on the test node with everything else already right: apps on
|
|
696
|
+
* `0.0.0.0:3001-3007`, answering `404` to the host itself, timing out from
|
|
697
|
+
* the gateway, `ufw` active with `22/tcp` its only rule and INPUT policy
|
|
698
|
+
* DROP.
|
|
699
|
+
*
|
|
700
|
+
* Derived, not configured: the subnet comes from the network the gateway
|
|
701
|
+
* is actually on, so a stack cannot be given a rule for the wrong one, and
|
|
702
|
+
* a subnet that is not a private range is refused outright rather than
|
|
703
|
+
* turned into a firewall rule nobody can explain.
|
|
704
|
+
*
|
|
705
|
+
* Never fatal. A host whose firewall cannot be read is a host that gets a
|
|
706
|
+
* message; stopping a deployment over it would trade a visible 503 for an
|
|
707
|
+
* invisible one.
|
|
708
|
+
*/
|
|
709
|
+
async openGatewayPath(target, project, stack) {
|
|
710
|
+
const network = `${project}-${stack}_default`;
|
|
711
|
+
try {
|
|
712
|
+
// No `2>/dev/null || true`: a network that cannot be inspected is the
|
|
713
|
+
// reason the path stays shut, and discarding it leaves an operator
|
|
714
|
+
// with a 503 and no sentence. The failure is caught below and named.
|
|
715
|
+
const subnet = (await this.sshExec(target, `docker network inspect ${shellEscape(network)} --format '{{range .IPAM.Config}}{{.Subnet}}{{end}}'`)).trim();
|
|
716
|
+
if (!subnet) {
|
|
717
|
+
this.logger.warn({ node: target.host, network }, 'The stack network reports no subnet — the gateway’s path to the applications was left as it was');
|
|
718
|
+
return;
|
|
719
|
+
}
|
|
720
|
+
const rule = reachabilityRule(subnet, project, stack);
|
|
721
|
+
if (!rule) {
|
|
722
|
+
this.logger.warn({ node: target.host, network, subnet }, 'Refusing to open a path from this subnet — it is not a private range');
|
|
723
|
+
return;
|
|
724
|
+
}
|
|
725
|
+
const outcome = (await this.sshExec(target, reachabilityCommand(rule))).trim();
|
|
726
|
+
if (outcome === 'allowed') {
|
|
727
|
+
this.logger.info({ node: target.host, subnet: rule.subnet, ports: `${rule.fromPort}-${rule.toPort}` }, 'Opened the gateway’s path to the applications on this host');
|
|
728
|
+
}
|
|
729
|
+
else if (outcome === 'failed') {
|
|
730
|
+
this.logger.error({ node: target.host, subnet: rule.subnet }, 'Could not open the gateway’s path — every /api/* request will answer 503 while the apps are healthy');
|
|
731
|
+
}
|
|
732
|
+
else if (outcome.startsWith('ufw-unreadable')) {
|
|
733
|
+
// Not `ufw-inactive`: nothing is known about the firewall, and if it
|
|
734
|
+
// is active it is dropping the gateway's traffic right now.
|
|
735
|
+
this.logger.warn({ node: target.host, subnet: rule.subnet, reason: outcome.slice('ufw-unreadable'.length).replace(/^:\s*/, '') }, 'Could not read the node’s firewall — if it is active, the gateway’s path to the applications is closed and /api/* will answer 503');
|
|
736
|
+
}
|
|
737
|
+
}
|
|
738
|
+
catch (err) {
|
|
739
|
+
this.logger.warn({ node: target.host, network, error: err.message.slice(0, 200) }, 'Could not check the gateway’s path to the applications');
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
/**
|
|
743
|
+
* Run each app's migrations, on the node, against the node's database.
|
|
744
|
+
*
|
|
745
|
+
* They had never run there. `ProjectService` migrates by executing
|
|
746
|
+
* `apps/<name>/src/database/migrate.ts` from the PROJECT PATH against
|
|
747
|
+
* `localhost` — both of which name the machine the master is on. A node has
|
|
748
|
+
* no project sources and a different localhost, so a remote stack's
|
|
749
|
+
* databases were created and left empty.
|
|
750
|
+
*
|
|
751
|
+
* Measured on the test node after six apps installed and five started:
|
|
752
|
+
* every one of the six databases existed and held ZERO tables. Five apps
|
|
753
|
+
* came up anyway, because they touch no table at boot; `payments` does, and
|
|
754
|
+
* said so —
|
|
755
|
+
*
|
|
756
|
+
* Failed to call @PostConstruct 'bootstrap' on 'PlatformRevenueService':
|
|
757
|
+
* relation "accounts" does not exist
|
|
758
|
+
*
|
|
759
|
+
* — which is the only reason anyone found out. A platform with no schema
|
|
760
|
+
* that reports five of six apps healthy is a worse state than one that
|
|
761
|
+
* refuses to start.
|
|
762
|
+
*
|
|
763
|
+
* The artifact already carries what is needed: `dist/database/migrate.js`
|
|
764
|
+
* beside `dist/database/migrations/` — 160 compiled files for payments. The
|
|
765
|
+
* same build the app itself runs from, so there is no second copy of the
|
|
766
|
+
* schema to drift.
|
|
767
|
+
*
|
|
768
|
+
* Returns the apps whose migrations failed, with the reason, and the
|
|
769
|
+
* caller does not start their new code.
|
|
770
|
+
*
|
|
771
|
+
* This used to say: «Failure is reported and does not stop the deployment:
|
|
772
|
+
* an app whose migrations fail will fail its own start with a message
|
|
773
|
+
* about the table it wanted, which is more specific than anything this
|
|
774
|
+
* step could say.» True for one of the two ways it goes. An app that
|
|
775
|
+
* fails to START is caught by `verifyHealth` and marked failed — that
|
|
776
|
+
* branch was covered, and the sentence is right about it. An app that
|
|
777
|
+
* starts fine and fails on the first REQUEST that touches the missing
|
|
778
|
+
* table passes `verifyHealth`, the stack reports `started`, and the only
|
|
779
|
+
* trace is one ERROR line in the master's log. The justification covered
|
|
780
|
+
* the branch that was already safe.
|
|
781
|
+
*
|
|
782
|
+
* Idempotence is what makes refusing cheap: the migrator records each
|
|
783
|
+
* migration it applied, in a transaction with the migration itself, so
|
|
784
|
+
* running the batch again after the cause is fixed applies exactly what
|
|
785
|
+
* is missing. That holds because `HardenedMigrationRunner` defaults
|
|
786
|
+
* `useTransactions` to true (`titan-database/src/migration/
|
|
787
|
+
* hardened-runner.ts:171`) and no sample `migrate.ts` overrides it — it is
|
|
788
|
+
* conditional in the runner (`:502`), so a migrator that turned it off
|
|
789
|
+
* would make a refused deployment leave a half-applied migration behind.
|
|
790
|
+
*/
|
|
791
|
+
async migrateNodeApps(target, project, landed, appEnv) {
|
|
792
|
+
const failed = new Map();
|
|
793
|
+
for (const entry of landed) {
|
|
794
|
+
const databaseUrl = appEnv?.[entry.app]?.['DATABASE_URL'];
|
|
795
|
+
if (!databaseUrl)
|
|
796
|
+
continue;
|
|
797
|
+
const dir = `/opt/omnitron/artifacts/${assertRemotePathSegment('project name', project)}` +
|
|
798
|
+
`/${assertRemotePathSegment('app name', entry.app)}` +
|
|
799
|
+
`/${assertRemotePathSegment('version', entry.version)}`;
|
|
800
|
+
const script = `${dir}/dist/database/migrate.js`;
|
|
801
|
+
// A node that could not be asked is not an app without a migrator.
|
|
802
|
+
// This command exits 0 either way, so `sshExec` throws only when the
|
|
803
|
+
// question never arrived — and that was answered `'no'`: migrations
|
|
804
|
+
// skipped, new code started on the old schema. The worst case of the
|
|
805
|
+
// docblock above, reached through a dropped SSH connection instead of
|
|
806
|
+
// a failed migration, and refused the same way.
|
|
807
|
+
let present;
|
|
808
|
+
try {
|
|
809
|
+
present = await this.sshExec(target, `test -f ${shellEscape(script)} && echo yes || echo no`);
|
|
810
|
+
}
|
|
811
|
+
catch (err) {
|
|
812
|
+
const message = `the node could not be asked whether there are any to apply — ${err.message}`;
|
|
813
|
+
this.logger.error({ node: target.host, app: entry.app, error: message.slice(0, 400) }, 'Could not ask the node for the app\'s migrations — its new code will not be started');
|
|
814
|
+
failed.set(entry.app, message.slice(0, 300));
|
|
815
|
+
continue;
|
|
816
|
+
}
|
|
817
|
+
if (present.trim() !== 'yes')
|
|
818
|
+
continue;
|
|
819
|
+
this.emitProgress(`${target.host}:${target.daemonPort ?? 9700}`, entry.app, 'installing', 75, 'Applying database migrations...');
|
|
820
|
+
// Both spellings, because the migrators read the second one.
|
|
821
|
+
//
|
|
822
|
+
// `apps/payments/src/database/migrate.ts` declares
|
|
823
|
+
// `envPrefix: 'PAYMENTS__DATABASE'` and builds its connection from
|
|
824
|
+
// `PAYMENTS__DATABASE__HOST` / `__PORT` / `__USER` / `__PASSWORD` /
|
|
825
|
+
// `__DATABASE`. Given only `DATABASE_URL` it falls back to its own
|
|
826
|
+
// defaults and reports `password authentication failed for user
|
|
827
|
+
// "postgres"` — against a password that is correct and that it never
|
|
828
|
+
// read. All six apps failed that way, which is what a convention
|
|
829
|
+
// nobody wrote down costs when a second caller appears.
|
|
830
|
+
//
|
|
831
|
+
// `ProjectService` passes both, with the comment "App-specific env
|
|
832
|
+
// vars (various naming conventions)". This is the second caller.
|
|
833
|
+
const parsed = parseDatabaseUrl(databaseUrl);
|
|
834
|
+
const prefix = entry.app.toUpperCase().replace(/-/g, '_');
|
|
835
|
+
const dbEnv = parsed
|
|
836
|
+
? {
|
|
837
|
+
[`${prefix}__DATABASE__HOST`]: parsed.host,
|
|
838
|
+
[`${prefix}__DATABASE__PORT`]: parsed.port,
|
|
839
|
+
[`${prefix}__DATABASE__DATABASE`]: parsed.database,
|
|
840
|
+
[`${prefix}__DATABASE__USER`]: parsed.user,
|
|
841
|
+
[`${prefix}__DATABASE__PASSWORD`]: parsed.password,
|
|
842
|
+
}
|
|
843
|
+
: {};
|
|
844
|
+
try {
|
|
845
|
+
// `cd` into the artifact so the script resolves its own imports, and
|
|
846
|
+
// every value in the environment rather than the command line, where
|
|
847
|
+
// it would be in every process listing on the host.
|
|
848
|
+
const assignments = Object.entries({ DATABASE_URL: databaseUrl, ...dbEnv })
|
|
849
|
+
.map(([k, v]) => `${k}=${shellEscape(v)}`)
|
|
850
|
+
.join(' ');
|
|
851
|
+
// What the migrations said, in order, before the verdict (see
|
|
852
|
+
// `migration-output.ts`). The masked channel on purpose: these lines
|
|
853
|
+
// are for reading, not parsing, and a migrator that ever printed a
|
|
854
|
+
// credential must not put it in the master's log.
|
|
855
|
+
const said = await this.sshExec(target, `cd ${shellEscape(dir)} && ${assignments} node ${shellEscape(script)}`, 600_000);
|
|
856
|
+
for (const line of migrationOutput(said)) {
|
|
857
|
+
this.logger.info({ node: target.host, app: entry.app, line }, 'Migration said');
|
|
858
|
+
}
|
|
859
|
+
this.logger.info({ node: target.host, app: entry.app }, 'Database migrations applied on the node');
|
|
860
|
+
}
|
|
861
|
+
catch (err) {
|
|
862
|
+
const message = err.message;
|
|
863
|
+
this.logger.error({ node: target.host, app: entry.app, error: message.slice(0, 400) }, 'Database migrations failed on the node — its new code will not be started');
|
|
864
|
+
failed.set(entry.app, message.slice(0, 300));
|
|
865
|
+
}
|
|
866
|
+
}
|
|
867
|
+
return failed;
|
|
868
|
+
}
|
|
869
|
+
/**
|
|
870
|
+
* Write the node's app definitions, and say whether they changed.
|
|
871
|
+
*
|
|
872
|
+
* The caller decides whether to restart the applications, and "the
|
|
873
|
+
* configuration moved" is half of that decision — identical files with a
|
|
874
|
+
* different environment is a different deployment. Compared by the hash of
|
|
875
|
+
* the rendered body against one written beside it, rather than by reading
|
|
876
|
+
* the file back: the body holds every generated credential on the node,
|
|
877
|
+
* and it should cross the wire once, in the direction it has to.
|
|
878
|
+
*/
|
|
879
|
+
async registerNodeApps(target, project, apps, landed, appEnv, alertSink) {
|
|
880
|
+
if (landed.length === 0) {
|
|
881
|
+
this.logger.warn({ host: target.host, project }, 'No artifact reached this node — nothing to register');
|
|
882
|
+
return { changed: false };
|
|
883
|
+
}
|
|
884
|
+
const { renderNodeAppConfig } = await import('../project/node-app-config.js');
|
|
885
|
+
const dir = `/opt/omnitron/projects/${assertRemotePathSegment('project name', project)}`;
|
|
886
|
+
const body = renderNodeAppConfig({
|
|
887
|
+
project,
|
|
888
|
+
artifactRoot: '/opt/omnitron/artifacts',
|
|
889
|
+
apps,
|
|
890
|
+
artifacts: landed,
|
|
891
|
+
appEnv,
|
|
892
|
+
alertSink,
|
|
893
|
+
});
|
|
894
|
+
const bodyHash = crypto.createHash('sha256').update(body).digest('hex');
|
|
895
|
+
try {
|
|
896
|
+
await this.sshExec(target, `mkdir -p ${shellEscape(dir)}`);
|
|
897
|
+
const previous = parseRecordedChecksum(await this.sshExec(target, readChecksumCommand(shellEscape(dir), NODE_CONFIG_HASH_FILE)));
|
|
898
|
+
const changed = previous !== bodyHash;
|
|
899
|
+
// Through a here-document: the config carries braces, quotes and
|
|
900
|
+
// newlines, and a single-quoted argument would need every quote in it
|
|
901
|
+
// escaped by hand — which is how a generated file acquires a syntax
|
|
902
|
+
// error nobody can see in the source that generated it.
|
|
903
|
+
const configPath = `${dir}/omnitron.config.mjs`;
|
|
904
|
+
// `umask 077` around the write, not a `chmod` after it: the file exists
|
|
905
|
+
// with its contents between the two, and what it contains is the
|
|
906
|
+
// generated password of every service on this node. It was written 0644
|
|
907
|
+
// for as long as it has existed — measured on the test node — which was
|
|
908
|
+
// harmless only because it held no credentials to leak.
|
|
909
|
+
await this.sshExec(target, `umask 077 && cat > ${shellEscape(configPath)} <<'OMNITRON_EOF'\n${body}\nOMNITRON_EOF`);
|
|
910
|
+
await this.sshExec(target, `chmod 600 ${shellEscape(configPath)}`);
|
|
911
|
+
await this.sshExec(target, `printf %s ${shellEscape(bodyHash)} > ${shellEscape(`${dir}/${NODE_CONFIG_HASH_FILE}`)}`);
|
|
912
|
+
const added = await this.sshExec(target, `omnitron project add ${shellEscape(project)} ${shellEscape(dir)} 2>&1`);
|
|
913
|
+
// Registering the project is not starting it. The node reads the
|
|
914
|
+
// definitions and waits: measured, `omnitron status` answered
|
|
915
|
+
// `appsTotal: 0` with all six apps listed in the file it had just been
|
|
916
|
+
// given, and `omnitron restart main` still said `Unknown app: main`.
|
|
917
|
+
//
|
|
918
|
+
// `startStack` is how a project's apps are started, which is why the
|
|
919
|
+
// generated config carries a stack for them to be in.
|
|
920
|
+
const { NODE_STACK } = await import('../project/node-app-config.js');
|
|
921
|
+
// ...and whether it has to be told at all. This command starts
|
|
922
|
+
// everything the node's config lists, so it overruled the per-app
|
|
923
|
+
// decision taken below it — measured 2026-09-22, six «Left running»
|
|
924
|
+
// lines about six applications this same deployment had restarted
|
|
925
|
+
// twenty seconds earlier. See `decideNodeStackStart`.
|
|
926
|
+
const start = decideNodeStackStart({
|
|
927
|
+
configChanged: changed,
|
|
928
|
+
apps: landed.map((l) => l.app),
|
|
929
|
+
online: await this.appsOnline(target, project),
|
|
930
|
+
});
|
|
931
|
+
const out = start.action === 'start'
|
|
932
|
+
? await this.sshExec(target, `omnitron stack start ${shellEscape(project)} ${shellEscape(NODE_STACK)} 2>&1`, 300_000)
|
|
933
|
+
: '';
|
|
934
|
+
void added;
|
|
935
|
+
// The node's CLI prints its refusals and exits zero — `omnitron project
|
|
936
|
+
// add` answered `No omnitron.config.ts found at …` and this logged
|
|
937
|
+
// `The node now knows what to run` over it, twice, while the node knew
|
|
938
|
+
// nothing. An exit code that does not carry the outcome means the
|
|
939
|
+
// OUTPUT is the outcome.
|
|
940
|
+
const refused = /\bno .*found\b|\bfailed\b|\berror\b|not found|cannot/i.test(`${added}\n${out}`);
|
|
941
|
+
if (refused) {
|
|
942
|
+
this.logger.error({ host: target.host, project, dir, detail: `${added}\n${out}`.trim().slice(0, 300) }, 'The node refused the apps it was given — its artifacts are installed and it will run none of them');
|
|
943
|
+
return { changed };
|
|
944
|
+
}
|
|
945
|
+
this.logger.info({
|
|
946
|
+
host: target.host,
|
|
947
|
+
project,
|
|
948
|
+
dir,
|
|
949
|
+
apps: landed.map((l) => l.app),
|
|
950
|
+
changed,
|
|
951
|
+
started: start.action === 'start',
|
|
952
|
+
because: start.because,
|
|
953
|
+
detail: out.trim().slice(0, 200),
|
|
954
|
+
}, start.action === 'start'
|
|
955
|
+
? 'The node now knows what to run'
|
|
956
|
+
: 'The node is already running these apps with this configuration — its stack was not restarted');
|
|
957
|
+
return { changed };
|
|
958
|
+
}
|
|
959
|
+
catch (err) {
|
|
960
|
+
this.logger.error({ host: target.host, project, dir, error: err.message }, 'Could not tell the node what to run — its artifacts are installed and its daemon does not know the apps');
|
|
961
|
+
// Unknown, and unknown must not read as "nothing moved": an app whose
|
|
962
|
+
// config may have changed is an app to restart.
|
|
963
|
+
return { changed: true };
|
|
964
|
+
}
|
|
965
|
+
}
|
|
120
966
|
/**
|
|
121
967
|
* Provision a remote node as a slave daemon.
|
|
122
968
|
*
|
|
@@ -134,191 +980,952 @@ export class RemoteDeployer {
|
|
|
134
980
|
* @param masterHost - The master daemon's reachable address (from slave's perspective)
|
|
135
981
|
* @param masterPort - The master daemon's fleet TCP port
|
|
136
982
|
*/
|
|
137
|
-
async provisionSlaveNode(
|
|
138
|
-
|
|
983
|
+
async provisionSlaveNode(target,
|
|
984
|
+
/**
|
|
985
|
+
* Where this node should dial to reach the master, or null when there is
|
|
986
|
+
* nowhere — a master behind NAT, which is where remote stacks are
|
|
987
|
+
* started from.
|
|
988
|
+
*
|
|
989
|
+
* Null is not a failure. Replication is master-PULL: the master opens
|
|
990
|
+
* the connection, over SSH when the node's daemon port is closed, and
|
|
991
|
+
* drains the node's buffer. `omnitron up --slave` takes the address
|
|
992
|
+
* optionally for exactly this reason.
|
|
993
|
+
*/
|
|
994
|
+
masterHost, masterPort, project, options = {}) {
|
|
995
|
+
const nodeKey = `${target.host}:${target.daemonPort ?? 9700}`;
|
|
139
996
|
try {
|
|
140
997
|
// 1. Verify SSH access
|
|
141
998
|
this.emitProgress(nodeKey, '*', 'pending', 0, 'Connecting via SSH...');
|
|
142
|
-
await this.verifySSH(
|
|
143
|
-
// 2.
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
999
|
+
await this.verifySSH(target);
|
|
1000
|
+
// 2-3. Prepare the host: whatever it is, and whatever it is missing.
|
|
1001
|
+
//
|
|
1002
|
+
// This was two probes and a `||` chain of package-manager installs that
|
|
1003
|
+
// only worked on three Linux families, added a vendor repository to the
|
|
1004
|
+
// host as a side effect, and on failure reported the error from the
|
|
1005
|
+
// LAST alternative — `apk` on a Debian machine that has never had it.
|
|
1006
|
+
//
|
|
1007
|
+
// Now the host is asked what it is in one round trip, a plan is derived
|
|
1008
|
+
// from the answer, and the plan is a value: it can be logged before it
|
|
1009
|
+
// runs and read in a test without a machine. See
|
|
1010
|
+
// `remote-provisioner.ts` for what it decides and why.
|
|
1011
|
+
this.emitProgress(nodeKey, '*', 'extracting', 10, 'Inspecting the host...');
|
|
1012
|
+
const facts = parsePlatformProbe(await this.sshExec(target, PLATFORM_PROBE, 60_000));
|
|
1013
|
+
const plan = planProvisioning(facts, {
|
|
1014
|
+
...(options.nodeVersion ? { nodeVersion: options.nodeVersion } : {}),
|
|
1015
|
+
...(options.installRuntime === false ? { usePackageManager: false } : {}),
|
|
1016
|
+
});
|
|
1017
|
+
this.logger.info({
|
|
1018
|
+
host: target.host,
|
|
1019
|
+
os: facts.os,
|
|
1020
|
+
arch: facts.arch,
|
|
1021
|
+
distro: facts.distro,
|
|
1022
|
+
packageManager: facts.packageManager,
|
|
1023
|
+
node: facts.node,
|
|
1024
|
+
omnitron: facts.omnitron,
|
|
1025
|
+
plan: describePlan(plan),
|
|
1026
|
+
}, 'Host inspected');
|
|
1027
|
+
if (plan.refusal) {
|
|
1028
|
+
this.logger.error({ host: target.host, reason: plan.refusal }, 'Cannot prepare this host');
|
|
1029
|
+
this.emitProgress(nodeKey, '*', 'failed', 0, plan.refusal);
|
|
1030
|
+
return false;
|
|
157
1031
|
}
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
1032
|
+
let progress = 15;
|
|
1033
|
+
const stride = plan.steps.length > 0 ? Math.floor(30 / plan.steps.length) : 0;
|
|
1034
|
+
for (const step of plan.steps) {
|
|
1035
|
+
this.emitProgress(nodeKey, '*', 'extracting', progress, step.what);
|
|
1036
|
+
if (step.touchesPackageManager) {
|
|
1037
|
+
// Said at warning level: this is the one kind of step that changes
|
|
1038
|
+
// how the machine gets its software, and the machines this reaches
|
|
1039
|
+
// are not blank.
|
|
1040
|
+
this.logger.warn({ host: target.host, step: step.what }, 'Using the host package manager');
|
|
1041
|
+
}
|
|
162
1042
|
try {
|
|
163
|
-
await this.sshExec(
|
|
1043
|
+
await this.sshExec(target, step.command, step.timeoutMs);
|
|
164
1044
|
}
|
|
165
1045
|
catch (err) {
|
|
166
|
-
|
|
1046
|
+
// The step that failed, by name, rather than the last alternative's
|
|
1047
|
+
// error message.
|
|
1048
|
+
this.logger.error({ host: target.host, step: step.what, error: err.message }, 'Host preparation step failed');
|
|
1049
|
+
this.emitProgress(nodeKey, '*', 'failed', progress, `${step.what}: ${err.message}`);
|
|
167
1050
|
return false;
|
|
168
1051
|
}
|
|
1052
|
+
progress += stride;
|
|
169
1053
|
}
|
|
170
|
-
// 4.
|
|
1054
|
+
// 4. Configure and start the slave, through omnitron's own setup path.
|
|
1055
|
+
//
|
|
1056
|
+
// This wrote `/etc/omnitron/omnitron.config.ts` containing `role`,
|
|
1057
|
+
// `master` and a `daemon` block, then ran a bare `omnitron up` beside
|
|
1058
|
+
// it. Every one of those keys was read by nothing.
|
|
1059
|
+
//
|
|
1060
|
+
// `IEcosystemConfig` — the schema of that file — has no `role`, no
|
|
1061
|
+
// `master` and no `daemon`. The daemon boots from
|
|
1062
|
+
// `~/.omnitron/config.json`, whose `SavedDaemonConfig` carried role and
|
|
1063
|
+
// master and nothing about transports. So a provisioned slave would
|
|
1064
|
+
// have come up as a MASTER, bound to loopback, with no master address
|
|
1065
|
+
// and no sync: three of the four things this step exists to arrange,
|
|
1066
|
+
// silently not arranged.
|
|
1067
|
+
//
|
|
1068
|
+
// `omnitron up --slave <host>:<port>` is the path that writes what the
|
|
1069
|
+
// daemon reads, and it is the same one an operator uses by hand. One
|
|
1070
|
+
// way to configure a slave instead of two, and the one that is
|
|
1071
|
+
// exercised.
|
|
171
1072
|
this.emitProgress(nodeKey, '*', 'extracting', 50, 'Configuring slave daemon...');
|
|
172
|
-
|
|
173
|
-
const
|
|
174
|
-
|
|
175
|
-
//
|
|
176
|
-
|
|
177
|
-
//
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
1073
|
+
assertRemotePathSegment('project name', project);
|
|
1074
|
+
const masterAddr = masterHost ? `${masterHost}:${masterPort}` : null;
|
|
1075
|
+
// 5. Start the slave daemon — if it is not already the one we want.
|
|
1076
|
+
//
|
|
1077
|
+
// `omnitron down` stops the daemon AND every application under it, so
|
|
1078
|
+
// this step is the most expensive thing a deployment does to a node
|
|
1079
|
+
// that needed nothing. See `decideSlaveDaemonRestart`: it took three
|
|
1080
|
+
// deployments to attribute, because the node-side `stack start` was
|
|
1081
|
+
// restarting them too and either one alone explains the pids.
|
|
1082
|
+
const daemonNow = readNodeStatus((await this.askNodeStatus(target)).raw);
|
|
1083
|
+
const daemonDecision = decideSlaveDaemonRestart({
|
|
1084
|
+
hostChanged: plan.steps.length > 0,
|
|
1085
|
+
daemon: daemonNow
|
|
1086
|
+
? {
|
|
1087
|
+
running: typeof daemonNow.pid === 'number' || typeof daemonNow.uptime === 'number',
|
|
1088
|
+
...(daemonNow.role ? { role: daemonNow.role } : {}),
|
|
1089
|
+
}
|
|
1090
|
+
: null,
|
|
181
1091
|
});
|
|
182
|
-
|
|
1092
|
+
if (daemonDecision.action === 'leave') {
|
|
1093
|
+
this.emitProgress(nodeKey, '*', 'restarting', 70, 'Slave daemon already running');
|
|
1094
|
+
this.logger.info({ host: target.host, because: daemonDecision.because, apps: daemonNow?.apps.length ?? 0 }, 'The node daemon was left alone — this deployment changes nothing under it');
|
|
1095
|
+
}
|
|
1096
|
+
else {
|
|
1097
|
+
// Said as loudly as the other branch. A decision that only speaks
|
|
1098
|
+
// when it declines is a decision an operator cannot audit: this one
|
|
1099
|
+
// took three deployments to attribute and a fourth to explain,
|
|
1100
|
+
// because «restart» left nothing behind but the restart itself.
|
|
1101
|
+
this.logger.info({
|
|
1102
|
+
host: target.host,
|
|
1103
|
+
because: daemonDecision.because,
|
|
1104
|
+
steps: plan.steps.length,
|
|
1105
|
+
role: daemonNow?.role ?? null,
|
|
1106
|
+
pid: daemonNow?.pid ?? null,
|
|
1107
|
+
uptime: daemonNow?.uptime ?? null,
|
|
1108
|
+
}, 'Taking the node daemon down and back up — and everything under it');
|
|
1109
|
+
this.emitProgress(nodeKey, '*', 'restarting', 70, 'Starting slave daemon...');
|
|
1110
|
+
await this.sshExec(target, masterAddr
|
|
1111
|
+
? `omnitron down 2>/dev/null; omnitron up --slave ${shellEscape(masterAddr)} --no-infra`
|
|
1112
|
+
// `--slave` with no address still sets the role; the node buffers
|
|
1113
|
+
// locally and waits to be pulled from, which is what it would do
|
|
1114
|
+
// with an address it cannot reach anyway.
|
|
1115
|
+
: 'omnitron down 2>/dev/null; omnitron up --slave --no-infra', 180_000).catch((err) => {
|
|
1116
|
+
// Reported, not swallowed. The verification below tells us whether the
|
|
1117
|
+
// daemon came up; this tells us what it said on the way.
|
|
1118
|
+
this.logger.warn({ host: target.host, error: err.message }, 'Slave start command did not return cleanly — verifying anyway');
|
|
1119
|
+
});
|
|
1120
|
+
}
|
|
1121
|
+
// 6. Verify the daemon is running, and mean it.
|
|
1122
|
+
//
|
|
1123
|
+
// This slept three seconds, pinged once, and on `unreachable` logged
|
|
1124
|
+
// "may still be starting" — then reported success regardless. Measured
|
|
1125
|
+
// on the first real run: the start command failed with
|
|
1126
|
+
// `omnitron: command not found`, the ping found nothing, and the
|
|
1127
|
+
// function returned `true`. A caller that trusts that goes on to ship
|
|
1128
|
+
// artifacts to a node with no daemon, and the console shows a node
|
|
1129
|
+
// that was "provisioned" and answers nothing.
|
|
1130
|
+
//
|
|
1131
|
+
// A daemon that is starting will answer within the window; one that is
|
|
1132
|
+
// not there will not answer within any window. Polling tells them
|
|
1133
|
+
// apart, which one sample cannot.
|
|
183
1134
|
this.emitProgress(nodeKey, '*', 'verifying', 90, 'Verifying slave daemon...');
|
|
184
|
-
|
|
1135
|
+
const started = await this.awaitDaemon(target, SLAVE_START_TIMEOUT_MS);
|
|
1136
|
+
if (!started.ok) {
|
|
1137
|
+
this.logger.error({ host: target.host, waitedMs: SLAVE_START_TIMEOUT_MS, detail: started.detail }, 'Slave daemon did not come up');
|
|
1138
|
+
this.emitProgress(nodeKey, '*', 'failed', 90, `the slave daemon did not answer within ${Math.round(SLAVE_START_TIMEOUT_MS / 1000)}s: ${started.detail}`);
|
|
1139
|
+
return false;
|
|
1140
|
+
}
|
|
1141
|
+
this.emitProgress(nodeKey, '*', 'success', 100, `Slave provisioned — ${started.detail}`);
|
|
1142
|
+
this.logger.info({ host: target.host, masterHost, masterPort, daemon: started.detail }, 'Slave node provisioned');
|
|
1143
|
+
return true;
|
|
1144
|
+
}
|
|
1145
|
+
catch (err) {
|
|
1146
|
+
this.emitProgress(nodeKey, '*', 'failed', 0, err.message);
|
|
1147
|
+
this.logger.error({ host: target.host, error: err.message }, 'Failed to provision slave node');
|
|
1148
|
+
return false;
|
|
1149
|
+
}
|
|
1150
|
+
}
|
|
1151
|
+
// ===========================================================================
|
|
1152
|
+
// Private — SSH Operations
|
|
1153
|
+
// ===========================================================================
|
|
1154
|
+
/**
|
|
1155
|
+
* Wait for the remote daemon to answer, or report why it did not.
|
|
1156
|
+
*
|
|
1157
|
+
* `omnitron ping` on the far side prints its pid, uptime and version when
|
|
1158
|
+
* the daemon is up, and fails otherwise. The last failure is carried out of
|
|
1159
|
+
* the loop so a caller can say what the host said rather than only that
|
|
1160
|
+
* time ran out.
|
|
1161
|
+
*/
|
|
1162
|
+
async awaitDaemon(target, timeoutMs) {
|
|
1163
|
+
const deadline = Date.now() + timeoutMs;
|
|
1164
|
+
let last = 'no answer';
|
|
1165
|
+
while (Date.now() < deadline) {
|
|
185
1166
|
try {
|
|
186
|
-
const
|
|
187
|
-
|
|
188
|
-
|
|
1167
|
+
const answer = await this.sshExec(target, 'omnitron ping', 15_000);
|
|
1168
|
+
// "It printed something" is not "it is running". `omnitron ping`
|
|
1169
|
+
// used to exit 0 while printing that the daemon is not running —
|
|
1170
|
+
// fixed, but a node runs whatever version it has, and this code
|
|
1171
|
+
// upgrades nodes from older ones. The answer is read, not assumed.
|
|
1172
|
+
if (/daemon is running/i.test(answer)) {
|
|
1173
|
+
return { ok: true, detail: answer.split('\n').filter(Boolean).pop().trim() };
|
|
189
1174
|
}
|
|
1175
|
+
last = answer.split('\n').filter(Boolean).pop()?.trim() || 'the ping printed nothing';
|
|
190
1176
|
}
|
|
191
|
-
catch {
|
|
192
|
-
|
|
1177
|
+
catch (err) {
|
|
1178
|
+
last = err.message;
|
|
193
1179
|
}
|
|
194
|
-
|
|
195
|
-
|
|
1180
|
+
await new Promise((resolve) => setTimeout(resolve, DAEMON_POLL_MS));
|
|
1181
|
+
}
|
|
1182
|
+
return { ok: false, detail: last };
|
|
1183
|
+
}
|
|
1184
|
+
/**
|
|
1185
|
+
* Install a locally built omnitron on a node, beside whatever it is
|
|
1186
|
+
* running.
|
|
1187
|
+
*
|
|
1188
|
+
* The counterpart to installing from the registry, and the only channel
|
|
1189
|
+
* that can put THIS tree on a node: the published package is whatever was
|
|
1190
|
+
* last released, which on 2026-09-14 was five months and 224 commits
|
|
1191
|
+
* behind, under the same version number.
|
|
1192
|
+
*
|
|
1193
|
+
* Nothing is made live here. The archive is transferred, unpacked into its
|
|
1194
|
+
* own version directory, installed, and run — and `activate` is a separate
|
|
1195
|
+
* call, so a caller that stops after this has spent disk and changed
|
|
1196
|
+
* nothing about what the node is serving.
|
|
1197
|
+
*/
|
|
1198
|
+
async installBundle(target, archivePath, version, prefix = '/opt/omnitron') {
|
|
1199
|
+
const nodeKey = `${target.host}:${target.daemonPort ?? 9700}`;
|
|
1200
|
+
const layout = { prefix, version };
|
|
1201
|
+
try {
|
|
1202
|
+
// Reject path traversal before any connection, upload or filesystem mutation.
|
|
1203
|
+
versionDir(layout);
|
|
1204
|
+
const fs = await import('node:fs/promises');
|
|
1205
|
+
const archiveSha256 = crypto.createHash('sha256').update(await fs.readFile(archivePath)).digest('hex');
|
|
1206
|
+
// Content-addressed transfer cannot replace an earlier archive with different bytes.
|
|
1207
|
+
const remoteArchive = `${prefix}/releases/${version}.${archiveSha256}.tar.gz`;
|
|
1208
|
+
this.emitProgress(nodeKey, '*', 'pending', 0, 'Connecting via SSH...');
|
|
1209
|
+
await this.verifySSH(target);
|
|
1210
|
+
this.emitProgress(nodeKey, '*', 'transferring', 10, `Transferring ${version}...`);
|
|
1211
|
+
await this.sshExec(target, installPreflightCommand(layout));
|
|
1212
|
+
await this.scpTransfer(target, archivePath, remoteArchive);
|
|
1213
|
+
let progress = 30;
|
|
1214
|
+
for (const step of installSteps(layout, remoteArchive, archiveSha256)) {
|
|
1215
|
+
this.emitProgress(nodeKey, '*', 'extracting', progress, step.what);
|
|
1216
|
+
const output = await this.sshExec(target, step.command, step.timeoutMs);
|
|
1217
|
+
if (step.what.includes('runs')) {
|
|
1218
|
+
// The version the node reports has to be the version we shipped.
|
|
1219
|
+
// A mismatch means the archive, the directory and the manifest
|
|
1220
|
+
// disagree — and the one thing a fleet upgrade cannot tolerate is
|
|
1221
|
+
// a node that reports a version it is not running.
|
|
1222
|
+
const reported = output.trim().split('\n').pop()?.trim();
|
|
1223
|
+
if (reported !== version) {
|
|
1224
|
+
throw new Error(`The installed copy reports ${JSON.stringify(reported)}, not ${JSON.stringify(version)}.`);
|
|
1225
|
+
}
|
|
1226
|
+
}
|
|
1227
|
+
progress += 20;
|
|
1228
|
+
}
|
|
1229
|
+
this.emitProgress(nodeKey, '*', 'success', 100, `${version} installed (not yet current)`);
|
|
1230
|
+
this.logger.info({ host: target.host, version, prefix }, 'Bundle installed beside the running version');
|
|
196
1231
|
return true;
|
|
197
1232
|
}
|
|
198
1233
|
catch (err) {
|
|
199
|
-
|
|
200
|
-
this.
|
|
1234
|
+
const message = err.message;
|
|
1235
|
+
this.emitProgress(nodeKey, '*', 'failed', 0, message);
|
|
1236
|
+
this.logger.error({ host: target.host, version, error: message }, 'Bundle install failed');
|
|
201
1237
|
return false;
|
|
202
1238
|
}
|
|
203
1239
|
}
|
|
204
1240
|
/**
|
|
205
|
-
*
|
|
1241
|
+
* `activateBundle` with the platform held. `omnitron down; up` stops every
|
|
1242
|
+
* app on the node, so each stack gateway there holds its platform
|
|
1243
|
+
* (`maintenance-lock.ts`) — warned first — until its apps answer through it
|
|
1244
|
+
* again, for up to three minutes after the restart; past that the lock is
|
|
1245
|
+
* left to expire by itself. A node with no gateway activates as before.
|
|
206
1246
|
*/
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
level: 'info',
|
|
242
|
-
directory: '~/.omnitron/logs',
|
|
243
|
-
maxSize: '50m',
|
|
244
|
-
maxFiles: 10,
|
|
245
|
-
compress: false,
|
|
246
|
-
},
|
|
247
|
-
env: 'production',
|
|
248
|
-
};
|
|
249
|
-
`;
|
|
1247
|
+
async activateBundleHeld(target, version, prefix = '/opt/omnitron', keepVersions = 3) {
|
|
1248
|
+
assertControlledDaemonPort(target.daemonPort);
|
|
1249
|
+
return this.underLease(target, `held host upgrade to ${version}`, async () => {
|
|
1250
|
+
await this.assertNoPendingHostOperations(target, prefix);
|
|
1251
|
+
const { runningGatewaySites, MaintenanceLock, DEFAULT_MAINTENANCE_TIMING } = await import('./maintenance-lock.js');
|
|
1252
|
+
const run = (script) => this.sshExecData(target, script, 30_000);
|
|
1253
|
+
const locks = [];
|
|
1254
|
+
for (const site of await runningGatewaySites(run, `omnitron ${version}`)) {
|
|
1255
|
+
const lock = await MaintenanceLock.open(site, this.logger, { ...DEFAULT_MAINTENANCE_TIMING, probeWithinMs: 180_000 });
|
|
1256
|
+
if (lock)
|
|
1257
|
+
locks.push(lock);
|
|
1258
|
+
}
|
|
1259
|
+
try {
|
|
1260
|
+
let activated = await this.activateBundle(target, version, prefix, keepVersions, async () => {
|
|
1261
|
+
for (const lock of locks) {
|
|
1262
|
+
const failed = await lock.probe(['/', '/api/main/health']);
|
|
1263
|
+
if (failed.length)
|
|
1264
|
+
throw new Error(`Gateway verification failed: ${failed.join(', ')}`);
|
|
1265
|
+
}
|
|
1266
|
+
});
|
|
1267
|
+
while (locks.length > 0) {
|
|
1268
|
+
const lifted = await locks.shift().liftWhenAnswering(['/', '/api/main/health']);
|
|
1269
|
+
if (!lifted.lifted) {
|
|
1270
|
+
activated = false;
|
|
1271
|
+
this.logger.error({ host: target.host, version, failed: lifted.failed }, 'After the upgrade the platform does not answer through its gateway — maintenance expires by itself');
|
|
1272
|
+
}
|
|
1273
|
+
}
|
|
1274
|
+
return activated;
|
|
1275
|
+
}
|
|
1276
|
+
finally {
|
|
1277
|
+
for (const lock of locks)
|
|
1278
|
+
await lock.lift('failed').catch(() => undefined);
|
|
1279
|
+
}
|
|
1280
|
+
});
|
|
250
1281
|
}
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
]
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
1282
|
+
/** Check all durable operator workflows; a recovery can allow only its exact active journal. */
|
|
1283
|
+
async assertNoPendingHostOperations(target, prefix = '/opt/omnitron', allowance) {
|
|
1284
|
+
await this.sshExecData(target, hostOperationBarrierCommand(prefix, allowance));
|
|
1285
|
+
}
|
|
1286
|
+
/** Require a settled host and the actual serving immutable daemon build. */
|
|
1287
|
+
async inspectInstalledBundle(target, prefix = '/opt/omnitron', allowance) {
|
|
1288
|
+
assertControlledDaemonPort(target.daemonPort);
|
|
1289
|
+
await this.assertNoPendingHostOperations(target, prefix, allowance);
|
|
1290
|
+
const pending = JSON.parse(await this.sshExecData(target, pendingReceiptsCommand(prefix)));
|
|
1291
|
+
if (pending.length)
|
|
1292
|
+
throw new Error(`Unfinished host upgrade ${pending.map(r => r.id).join(', ')}; recover it before deploying an app`);
|
|
1293
|
+
const bundle = JSON.parse(await this.sshExecData(target, bundleProbeCommand(prefix)));
|
|
1294
|
+
const status = JSON.parse(await this.sshExecData(target, inventoryCommand(bundle, false)));
|
|
1295
|
+
assertServingBundle(bundle, status);
|
|
1296
|
+
await this.sshExecData(target, runtimeProbeCommand(bundle, status.pid));
|
|
1297
|
+
return bundle;
|
|
1298
|
+
}
|
|
1299
|
+
async activateBundle(target, version, prefix = '/opt/omnitron', keepVersions = 3, verifyPlatform, allowance) {
|
|
1300
|
+
assertControlledDaemonPort(target.daemonPort);
|
|
1301
|
+
if (!/^[A-Za-z0-9][A-Za-z0-9._+-]*$/.test(version) || version.includes('..'))
|
|
1302
|
+
throw new Error('Invalid installed bundle version');
|
|
1303
|
+
if (!Number.isSafeInteger(keepVersions) || keepVersions < 1)
|
|
1304
|
+
throw new Error('keepVersions must be a positive integer');
|
|
1305
|
+
return this.underLease(target, `host upgrade to ${version}`, async () => {
|
|
1306
|
+
const nodeKey = `${target.host}:${target.daemonPort ?? 9700}`;
|
|
1307
|
+
try {
|
|
1308
|
+
await this.assertNoPendingHostOperations(target, prefix, allowance);
|
|
1309
|
+
const pending = JSON.parse(await this.sshExecData(target, pendingReceiptsCommand(prefix)));
|
|
1310
|
+
if (pending.length) {
|
|
1311
|
+
if (pending.length !== 1)
|
|
1312
|
+
throw new Error('Multiple unfinished host upgrades; recover an explicit receipt ID first');
|
|
1313
|
+
const recovered = await this.recoverBundleUpgrade(target, pending[0].id, prefix, verifyPlatform, allowance);
|
|
1314
|
+
this.logger.warn({ receipt: pending[0].id, recovered }, 'Recovered the unfinished host upgrade; retry the requested upgrade separately');
|
|
1315
|
+
return false;
|
|
1316
|
+
}
|
|
1317
|
+
const previous = await this.inspectInstalledBundle(target, prefix, allowance);
|
|
1318
|
+
const candidate = JSON.parse(await this.sshExecData(target, bundleProbeCommand(prefix, version)));
|
|
1319
|
+
await this.sshExec(target, `${shellEscape(`${candidate.root}/dist/cli/omnitron.js`)} --version`, 120_000);
|
|
1320
|
+
const inventory = JSON.parse(await this.sshExecData(target, inventoryCommand(previous, true)));
|
|
1321
|
+
inventory.runtime = JSON.parse(await this.sshExecData(target, runtimeProbeCommand(previous, inventory.status.pid)));
|
|
1322
|
+
const plan = planHostUpgrade(previous, candidate, inventory);
|
|
1323
|
+
await this.sshExec(target, contractPreflightCommand(plan.apps));
|
|
1324
|
+
for (const app of plan.apps) {
|
|
1325
|
+
await this.sshExec(target, hookCommand(app, previous, 'verify'), 330_000);
|
|
1326
|
+
if (plan.online.includes(app.name))
|
|
1327
|
+
await this.sshExec(target, appHealthCommand(app, previous), 330_000);
|
|
1328
|
+
}
|
|
1329
|
+
const id = `${new Date().toISOString().replace(/[:.]/g, '-')}-${crypto.randomUUID()}`;
|
|
1330
|
+
const receiptPath = `${prefix}/upgrade-receipts/${id}.json`;
|
|
1331
|
+
const receipt = await executeHostUpgrade(plan, id, this.hostUpgradeActions(target, prefix, keepVersions, plan, verifyPlatform));
|
|
1332
|
+
const ok = receipt.outcome === 'upgraded';
|
|
1333
|
+
this.emitProgress(nodeKey, '*', ok ? 'success' : 'failed', 100, `${receipt.outcome}; recovery receipt: ${receiptPath}`);
|
|
1334
|
+
this.logger[ok ? 'info' : 'error']({ host: target.host, version, receipt: receiptPath, outcome: receipt.outcome }, 'Controlled host upgrade finished');
|
|
1335
|
+
return ok;
|
|
1336
|
+
}
|
|
1337
|
+
catch (error) {
|
|
1338
|
+
const message = error.message;
|
|
1339
|
+
this.emitProgress(nodeKey, '*', 'failed', 0, message);
|
|
1340
|
+
this.logger.error({ host: target.host, version, error: message }, 'Host upgrade refused or preparation failed');
|
|
1341
|
+
return false;
|
|
1342
|
+
}
|
|
1343
|
+
});
|
|
1344
|
+
}
|
|
1345
|
+
/** Recover a recorded interrupted upgrade under the same node lease; never prunes or changes app release versions. */
|
|
1346
|
+
async recoverBundleUpgrade(target, id, prefix = '/opt/omnitron', verifyPlatform, allowance) {
|
|
1347
|
+
assertControlledDaemonPort(target.daemonPort);
|
|
1348
|
+
return this.underLease(target, `recover host upgrade ${id}`, async () => {
|
|
1349
|
+
await this.assertNoPendingHostOperations(target, prefix, allowance);
|
|
1350
|
+
const records = JSON.parse(await this.sshExecData(target, pendingReceiptsCommand(prefix, id)));
|
|
1351
|
+
if (records.length !== 1 || records[0].id !== id)
|
|
1352
|
+
throw new Error('No unfinished host upgrade with that receipt ID');
|
|
1353
|
+
const receipt = records[0];
|
|
1354
|
+
const plan = planHostUpgrade(receipt.plan.previous, receipt.plan.candidate, receipt.plan.inventory);
|
|
1355
|
+
const installed = JSON.parse(await this.sshExecData(target, bundleProbeCommand(prefix, plan.previous.version)));
|
|
1356
|
+
if (JSON.stringify(installed) !== JSON.stringify(plan.previous))
|
|
1357
|
+
throw new Error('Recorded previous bundle changed; recovery refused');
|
|
1358
|
+
await this.sshExec(target, contractPreflightCommand(plan.apps));
|
|
1359
|
+
const result = await recoverHostUpgrade(receipt, this.hostUpgradeActions(target, prefix, 3, plan, verifyPlatform));
|
|
1360
|
+
this.logger[result.outcome === 'rolled-back' ? 'info' : 'error']({ receipt: `${prefix}/upgrade-receipts/${id}.json`, outcome: result.outcome }, 'Host upgrade recovery finished');
|
|
1361
|
+
return result.outcome === 'rolled-back';
|
|
1362
|
+
});
|
|
1363
|
+
}
|
|
1364
|
+
hostUpgradeActions(target, prefix, keepVersions, plan, verifyPlatform) {
|
|
1365
|
+
return {
|
|
1366
|
+
save: async (receipt) => { await this.sshExec(target, receiptCommand(prefix, receipt)); },
|
|
1367
|
+
stop: async (bundle) => {
|
|
1368
|
+
await this.sshExec(target, runtimeStopCommand(plan.inventory.runtime, bundle), 180_000);
|
|
1369
|
+
await this.sshExec(target, stoppedDaemonCommand(bundle), 30_000);
|
|
1370
|
+
},
|
|
1371
|
+
activate: async (bundle) => {
|
|
1372
|
+
for (const step of activateSteps({ prefix, version: bundle.version }))
|
|
1373
|
+
await this.sshExec(target, step.command, step.timeoutMs);
|
|
1374
|
+
},
|
|
1375
|
+
rebind: async (app, bundle) => { await this.sshExec(target, hookCommand(app, bundle, 'rebind'), 330_000); },
|
|
1376
|
+
start: async (bundle) => {
|
|
1377
|
+
await this.sshExec(target, runtimeStartCommand(plan.inventory.runtime, plan.previous, bundle, [plan.previous.root, plan.candidate.root]), 180_000);
|
|
1378
|
+
},
|
|
1379
|
+
verify: async (bundle) => {
|
|
1380
|
+
const deadline = Date.now() + 180_000;
|
|
1381
|
+
let lastError;
|
|
1382
|
+
do {
|
|
1383
|
+
try {
|
|
1384
|
+
const status = JSON.parse(await this.sshExecData(target, inventoryCommand(bundle, false), 30_000));
|
|
1385
|
+
verifyRestoredInventory(plan, bundle, status);
|
|
1386
|
+
const runtime = JSON.parse(await this.sshExecData(target, runtimeProbeCommand(bundle, status.pid)));
|
|
1387
|
+
if (runtime.kind !== plan.inventory.runtime.kind || (runtime.kind === 'systemd' && plan.inventory.runtime.kind === 'systemd' &&
|
|
1388
|
+
(runtime.scope !== plan.inventory.runtime.scope || runtime.enabled !== plan.inventory.runtime.enabled)))
|
|
1389
|
+
throw new Error('Supervisor state was not restored');
|
|
1390
|
+
for (const app of plan.apps) {
|
|
1391
|
+
await this.sshExec(target, hookCommand(app, bundle, 'verify'), 330_000);
|
|
1392
|
+
if (plan.online.includes(app.name))
|
|
1393
|
+
await this.sshExec(target, appHealthCommand(app, bundle), 330_000);
|
|
1394
|
+
}
|
|
1395
|
+
if (verifyPlatform)
|
|
1396
|
+
await verifyPlatform();
|
|
1397
|
+
await this.sshExec(target, updateContractBindingsCommand(plan, bundle));
|
|
1398
|
+
return;
|
|
1399
|
+
}
|
|
1400
|
+
catch (error) {
|
|
1401
|
+
lastError = error;
|
|
1402
|
+
}
|
|
1403
|
+
if (Date.now() < deadline)
|
|
1404
|
+
await new Promise(resolve => setTimeout(resolve, 2_000));
|
|
1405
|
+
} while (Date.now() < deadline);
|
|
1406
|
+
throw new Error(`Host and application verification failed: ${String(lastError)}`);
|
|
1407
|
+
},
|
|
1408
|
+
prune: async (protectedRoots) => {
|
|
1409
|
+
for (const step of pruneSteps({ prefix, version: plan.candidate.version }, keepVersions, protectedRoots))
|
|
1410
|
+
await this.sshExec(target, step.command, step.timeoutMs);
|
|
1411
|
+
},
|
|
1412
|
+
};
|
|
1413
|
+
}
|
|
1414
|
+
async verifySSH(target) {
|
|
1415
|
+
await this.sshExec(target, 'echo ok', 10_000);
|
|
1416
|
+
}
|
|
1417
|
+
/**
|
|
1418
|
+
* Run a command on the node, through the daemon's one SSH implementation.
|
|
1419
|
+
*
|
|
1420
|
+
* Throws on a non-zero exit, which is what the callers above are written
|
|
1421
|
+
* against — they use `|| echo ""` where they want to see a failure as an
|
|
1422
|
+
* answer, and expect a throw everywhere else. `ExecutionService.ssh` reports
|
|
1423
|
+
* failure in `exitCode` rather than raising, so the check has to be here;
|
|
1424
|
+
* without it every `if (!result.trim())` in this file would read a failed
|
|
1425
|
+
* command as an empty answer, and the two probes in `provisionSlaveNode`
|
|
1426
|
+
* would install a runtime onto a host that already has one.
|
|
1427
|
+
*/
|
|
1428
|
+
/**
|
|
1429
|
+
* How a node deploy lease reaches its node: the lease module writes the
|
|
1430
|
+
* scripts, the deployer owns the SSH. See `node-deploy-lease.ts`.
|
|
1431
|
+
*/
|
|
1432
|
+
leaseRunner(target) {
|
|
1433
|
+
// Through the data channel: a lease answer carries the holder's record,
|
|
1434
|
+
// `{"token":…}`, which the masker rewrote into `"token": [REDACTED]` on
|
|
1435
|
+
// every read — a refusal could never say who held the node.
|
|
1436
|
+
return async (script) => {
|
|
1437
|
+
// Confirmation itself must use the raw channel, never recursively ask
|
|
1438
|
+
// for another confirmation of the lease being acquired or renewed.
|
|
1439
|
+
const result = await this.readFromNodeRaw(target, script, 30_000);
|
|
1440
|
+
if (result.code !== 0)
|
|
1441
|
+
throw new Error(`Lease transport failed: ${result.stderr || result.stdout}`);
|
|
1442
|
+
return result.stdout.trim();
|
|
1443
|
+
};
|
|
1444
|
+
}
|
|
1445
|
+
/**
|
|
1446
|
+
* Run `work` holding the machine's deploy lease — for what changes a node
|
|
1447
|
+
* outside a stack deployment. `fleet upgrade`'s install and activate went
|
|
1448
|
+
* to the node with no lease at all, and are exactly a second writer: the
|
|
1449
|
+
* activation restarts the node's daemon under whatever else is deploying.
|
|
1450
|
+
*/
|
|
1451
|
+
async underLease(target, purpose, work) {
|
|
1452
|
+
const machine = `${target.host}:${target.sshPort ?? 22}`;
|
|
1453
|
+
const held = this.deploymentLeases.getStore();
|
|
1454
|
+
if (held?.has(machine))
|
|
1455
|
+
return work();
|
|
1456
|
+
return withNodeLeases([{ node: machine, machine, run: this.leaseRunner(target) }], purpose, this.logger, async (leases) => {
|
|
1457
|
+
if (!leases.has(machine)) {
|
|
1458
|
+
throw new Error(`Could not reach ${machine} to lease it for ${purpose}: ${leases.unreachable.get(machine) ?? 'no answer'}`);
|
|
1459
|
+
}
|
|
1460
|
+
const context = new Map(held);
|
|
1461
|
+
context.set(machine, { confirm: step => leases.confirm(machine, step), guard: (command, timeoutMs) => leases.guard(machine, command, timeoutMs) });
|
|
1462
|
+
return this.deploymentLeases.run(context, work);
|
|
1463
|
+
});
|
|
1464
|
+
}
|
|
1465
|
+
/** Confirm a held lease before the next node operation; outside a lease this is a no-op. */
|
|
1466
|
+
async confirmLease(target, step) {
|
|
1467
|
+
const machine = `${target.host}:${target.sshPort ?? 22}`;
|
|
1468
|
+
await this.deploymentLeases.getStore()?.get(machine)?.confirm(step);
|
|
1469
|
+
}
|
|
1470
|
+
/** The node holds its execution fence for the command lifetime, beyond a lost controller. */
|
|
1471
|
+
guardedCommand(target, command, timeout) {
|
|
1472
|
+
const lease = this.deploymentLeases.getStore()?.get(`${target.host}:${target.sshPort ?? 22}`);
|
|
1473
|
+
return lease ? { command: lease.guard(command, timeout), timeout: timeout + 45_000 } : { command, timeout };
|
|
1474
|
+
}
|
|
1475
|
+
async sshExec(target, command, timeout = 60_000) {
|
|
1476
|
+
await this.confirmLease(target, 'remote command');
|
|
1477
|
+
const guarded = this.guardedCommand(target, command, timeout);
|
|
1478
|
+
const result = await this.execution.ssh(sshTargetOf(target), guarded.command, { timeout: guarded.timeout });
|
|
1479
|
+
if (result.exitCode !== 0) {
|
|
1480
|
+
const detail = result.stderr || result.stdout || `exit ${result.exitCode}`;
|
|
1481
|
+
throw new Error(`ssh ${target.username ?? 'root'}@${target.host}: ${detail}`);
|
|
1482
|
+
}
|
|
1483
|
+
return result.stdout.trim();
|
|
1484
|
+
}
|
|
1485
|
+
/** `sshExec` for a command whose stdout is data — see `readFromNode`. */
|
|
1486
|
+
async sshExecData(target, command, timeout = 60_000) {
|
|
1487
|
+
const result = await this.readFromNode(target, command, timeout);
|
|
1488
|
+
if (result.code !== 0) {
|
|
1489
|
+
throw new Error(`ssh ${target.username ?? 'root'}@${target.host}: ${result.stderr || result.stdout || `exit ${result.code}`}`);
|
|
1490
|
+
}
|
|
1491
|
+
return result.stdout.trim();
|
|
1492
|
+
}
|
|
1493
|
+
/**
|
|
1494
|
+
* Run a command on a node and hand back what it said, exit code included.
|
|
1495
|
+
*
|
|
1496
|
+
* `sshExec` turns every non-zero exit into a throw, which is right for a
|
|
1497
|
+
* deployment step — a step either happened or it did not. A PROBE's exit
|
|
1498
|
+
* code is information: the attestation producer exits 1 when probes failed
|
|
1499
|
+
* (a fact to keep) and 2 when it could not measure at all (nothing to
|
|
1500
|
+
* keep), and a throw would flatten the two into one.
|
|
1501
|
+
*/
|
|
1502
|
+
async runOnNode(target, command, timeoutMs = 600_000) {
|
|
1503
|
+
await this.confirmLease(target, 'remote command');
|
|
1504
|
+
const guarded = this.guardedCommand(target, command, timeoutMs);
|
|
1505
|
+
const result = await this.execution.ssh(sshTargetOf(target), guarded.command, { timeout: guarded.timeout });
|
|
1506
|
+
return { stdout: result.stdout, stderr: result.stderr, code: result.exitCode ?? -1 };
|
|
1507
|
+
}
|
|
1508
|
+
/**
|
|
1509
|
+
* `runOnNode` for a command whose stdout is DATA — a record to parse, a
|
|
1510
|
+
* line to store — carried past the transport's masker
|
|
1511
|
+
* (`execution/data-channel.ts`). Its callers are named there and here: the
|
|
1512
|
+
* lease runner (holder records), the attestation (the producer's line), the
|
|
1513
|
+
* operator tool (its answer line). What it returns is unmasked; none of
|
|
1514
|
+
* them logs it whole.
|
|
1515
|
+
*
|
|
1516
|
+
* A channel that came back as something other than its encoding is a
|
|
1517
|
+
* failure with its own code, -2, and its words on stderr — never data.
|
|
1518
|
+
*/
|
|
1519
|
+
async readFromNode(target, command, timeoutMs = 600_000) {
|
|
1520
|
+
await this.confirmLease(target, 'remote data command');
|
|
1521
|
+
const guarded = this.guardedCommand(target, command, timeoutMs);
|
|
1522
|
+
return this.readFromNodeRaw(target, guarded.command, guarded.timeout);
|
|
1523
|
+
}
|
|
1524
|
+
async readFromNodeRaw(target, command, timeoutMs) {
|
|
1525
|
+
const result = await this.execution.ssh(sshTargetOf(target), throughDataChannel(command), { timeout: timeoutMs });
|
|
1526
|
+
const data = fromDataChannel(result.stdout);
|
|
1527
|
+
if (!data.ok) {
|
|
1528
|
+
return { stdout: '', stderr: [data.because, result.stderr].filter(Boolean).join(' | '), code: result.exitCode === 0 ? -2 : (result.exitCode ?? -1) };
|
|
1529
|
+
}
|
|
1530
|
+
return { stdout: data.text, stderr: result.stderr, code: result.exitCode ?? -1 };
|
|
1531
|
+
}
|
|
1532
|
+
/** Use this inside underLease so uploads share the command confirmation boundary. */
|
|
1533
|
+
async uploadToNode(target, localPath, remotePath) {
|
|
1534
|
+
await this.confirmLease(target, 'file upload');
|
|
1535
|
+
const lease = this.deploymentLeases.getStore()?.get(`${target.host}:${target.sshPort ?? 22}`);
|
|
1536
|
+
if (!lease) {
|
|
1537
|
+
await this.execution.uploadFile(sshTargetOf(target), localPath, remotePath);
|
|
1538
|
+
return;
|
|
1539
|
+
}
|
|
1540
|
+
await this.execution.uploadFile(sshTargetOf(target), localPath, remotePath, {
|
|
1541
|
+
runControl: async (command, timeout) => {
|
|
1542
|
+
const result = await this.runOnNode(target, command, timeout);
|
|
1543
|
+
return { stdout: result.stdout, stderr: result.stderr, exitCode: result.code };
|
|
1544
|
+
},
|
|
1545
|
+
});
|
|
1546
|
+
}
|
|
1547
|
+
async scpTransfer(target, localPath, remotePath) {
|
|
1548
|
+
await this.uploadToNode(target, localPath, remotePath);
|
|
1549
|
+
}
|
|
1550
|
+
/**
|
|
1551
|
+
* Ask the node's daemon to run the app whose artifact just landed.
|
|
1552
|
+
*
|
|
1553
|
+
* This was `omnitron restart <app> 2>/dev/null || true`, and the `|| true`
|
|
1554
|
+
* was hiding the whole of the remote deployment's last mile.
|
|
1555
|
+
*
|
|
1556
|
+
* Measured on the test node, running the command by hand:
|
|
1557
|
+
*
|
|
1558
|
+
* omnitron restart main → Failed: Unknown app: main
|
|
1559
|
+
*
|
|
1560
|
+
* The node's daemon has no app definitions — its own log says `No projects
|
|
1561
|
+
* registered`, and `omnitron status --json` answers `appsTotal: 0` — because
|
|
1562
|
+
* nothing in this deployment ever tells it about the apps. Artifacts land
|
|
1563
|
+
* at `/opt/omnitron/artifacts/<project>/<app>/<version>/` complete with
|
|
1564
|
+
* `config`, `dist` and `package.json`, and a project is "a directory with
|
|
1565
|
+
* omnitron.config.ts", which an artifact is not.
|
|
1566
|
+
*
|
|
1567
|
+
* That gap is not closed here — it is a design question about how a node
|
|
1568
|
+
* learns what to run. What is closed here is its INVISIBILITY: the shell
|
|
1569
|
+
* discarded the only sentence that said so, and the caller reported
|
|
1570
|
+
* `Deployment successful`.
|
|
1571
|
+
*/
|
|
1572
|
+
/**
|
|
1573
|
+
* Ship a built frontend to a node and say where it landed.
|
|
1574
|
+
*
|
|
1575
|
+
* The gateway's config travels inside the provisioning RPC — six files, 65
|
|
1576
|
+
* KB. A frontend build is 32 MB, and an RPC argument is the wrong shape for
|
|
1577
|
+
* that: it is held whole in memory on both sides and blocks the call it
|
|
1578
|
+
* rides on. So it goes the way artifacts already go — tar over SSH, extract
|
|
1579
|
+
* on the far side — which is the mechanism this class exists for.
|
|
1580
|
+
*
|
|
1581
|
+
* Idempotent by content: the archive's hash names the directory, so a build
|
|
1582
|
+
* that has not changed is transferred once and every later provisioning pass
|
|
1583
|
+
* finds it already there. A build that HAS changed lands beside the old one
|
|
1584
|
+
* and the gateway's spec hash changes with the path, which is what makes the
|
|
1585
|
+
* container pick it up.
|
|
1586
|
+
*/
|
|
1587
|
+
async uploadStaticBundle(target, localDir, remoteRoot,
|
|
1588
|
+
/**
|
|
1589
|
+
* The stack's link inside `remoteRoot`, `current-<project>-<stack>`
|
|
1590
|
+
* (`gatewayStaticLinkName`). Given, the delivery points it at the build it
|
|
1591
|
+
* just unpacked — which is what lets the gateway mount the ROOT once and
|
|
1592
|
+
* never be recreated for a new build. Omitted, nothing is linked and the
|
|
1593
|
+
* delivery is exactly what it was.
|
|
1594
|
+
*/
|
|
1595
|
+
link) {
|
|
1596
|
+
const { createHash } = await import('node:crypto');
|
|
1597
|
+
const fsp = await import('node:fs/promises');
|
|
1598
|
+
const os = await import('node:os');
|
|
1599
|
+
const path = await import('node:path');
|
|
1600
|
+
const stat = await fsp.stat(localDir).catch(() => null);
|
|
1601
|
+
if (!stat?.isDirectory())
|
|
1602
|
+
throw new Error(`No such directory to serve: ${localDir}`);
|
|
1603
|
+
const staging = await fsp.mkdtemp(path.join(os.tmpdir(), 'omnitron-static-'));
|
|
1604
|
+
const archive = path.join(staging, 'static.tar.gz');
|
|
1605
|
+
try {
|
|
1606
|
+
// `-C` so the archive holds the directory's CONTENTS, not a path from
|
|
1607
|
+
// this machine: the node mounts what is inside, and a leading
|
|
1608
|
+
// `apps/portal/dist/` would put every file one level too deep.
|
|
1609
|
+
// `COPYFILE_DISABLE=1`: macOS `tar` writes an AppleDouble sidecar for
|
|
1610
|
+
// every file with an extended attribute, and this archive is unpacked
|
|
1611
|
+
// straight into the gateway's web root. Measured there:
|
|
1612
|
+
// `._index.html`, `._assets`, `._docs-static` — 163 bytes each, served
|
|
1613
|
+
// to anyone who asks for them.
|
|
1614
|
+
// `--no-xattrs` for the same reason as `COPYFILE_DISABLE`, one layer
|
|
1615
|
+
// down. macOS stamps `com.apple.provenance` on files it has seen, and
|
|
1616
|
+
// bsdtar carries it as a `LIBARCHIVE.xattr.*` header that GNU tar on
|
|
1617
|
+
// the node does not know. Measured: 29 319 characters of
|
|
1618
|
+
// `tar: Ignoring unknown extended header keyword` in one error, with
|
|
1619
|
+
// the four lines that named the actual failure at the very end. The
|
|
1620
|
+
// warnings were harmless and the noise was not — it is what a reader
|
|
1621
|
+
// sees first, and it says nothing.
|
|
1622
|
+
// `| gzip -n` rather than tar's own `-z`, for the digest below.
|
|
1623
|
+
//
|
|
1624
|
+
// gzip writes the time of compression into its header, so the same
|
|
1625
|
+
// directory packed twice a second apart produces two different sums —
|
|
1626
|
+
// and this sum is what the remote directory is NAMED after. Measured:
|
|
1627
|
+
// two archives of one file, one second apart, gave 2 distinct sha256
|
|
1628
|
+
// with `-czf` and 1 with `| gzip -n`.
|
|
1629
|
+
//
|
|
1630
|
+
// Two consequences, and the second is the load-bearing one. A peer
|
|
1631
|
+
// repacking the bundle to check what the node holds could compare
|
|
1632
|
+
// only its LENGTH, because the sums were guaranteed to differ — an
|
|
1633
|
+
// «almost proof» where a proof was one flag away. And the skip on an
|
|
1634
|
+
// unchanged build never fired: every deployment minted a new digest,
|
|
1635
|
+
// so `test -d <digest>` was always false and the same bytes crossed
|
|
1636
|
+
// the link again. (omni-74, after their size-matched, sum-mismatched
|
|
1637
|
+
// comparison.)
|
|
1638
|
+
const packed = await this.execution.exec(`COPYFILE_DISABLE=1 tar --no-xattrs -cf - -C ${shellEscape(localDir)} . | gzip -n > ${shellEscape(archive)}`);
|
|
1639
|
+
// Read, and judged whole, BEFORE it is summed. `exec` never throws, and
|
|
1640
|
+
// this pipeline's status is gzip's alone, so a `tar` that stopped
|
|
1641
|
+
// halfway left a short archive that was then summed, sent, checked
|
|
1642
|
+
// against its own short sum — and failed only on the node: «gzip:
|
|
1643
|
+
// stdin: unexpected end of file» (sample-202609221949-23dce2f6 to test,
|
|
1644
|
+
// 2026-09-22). The sum proves the file ARRIVED as it left; nothing
|
|
1645
|
+
// proved it left whole.
|
|
1646
|
+
const content = await fsp.readFile(archive).catch(() => null);
|
|
1647
|
+
const verdict = content ? wholeTarGz(content) : 'no archive was written';
|
|
1648
|
+
if (packed.exitCode !== 0 || verdict !== true) {
|
|
1649
|
+
const words = (packed.stderr || '').trim().split('\n').slice(-3).join(' | ');
|
|
1650
|
+
throw new Error(`Packing ${localDir} for the node failed (exit ${packed.exitCode}; ${verdict === true ? 'archive whole' : verdict})` +
|
|
1651
|
+
(words ? `: ${words}` : '') +
|
|
1652
|
+
' — nothing was sent');
|
|
1653
|
+
}
|
|
1654
|
+
const bytes = content.length;
|
|
1655
|
+
const digest = createHash('sha256').update(content).digest('hex').slice(0, 16);
|
|
1656
|
+
const remoteDir = `${remoteRoot}/${digest}`;
|
|
1657
|
+
const remoteFile = `${remoteDir}.tar.gz`;
|
|
1658
|
+
// Already there: the same build was sent before. Transferring it again
|
|
1659
|
+
// costs 32 MB over a link that may be an SSH tunnel, for no change.
|
|
1660
|
+
//
|
|
1661
|
+
// The question is «is the BUNDLE there», and `test -d` answered «is
|
|
1662
|
+
// the DIRECTORY there» — two different statements, and the first
|
|
1663
|
+
// deployment made them disagree. `mkdir -p` had succeeded, the
|
|
1664
|
+
// transfer had landed short, and `tar` had died on the truncated
|
|
1665
|
+
// stream: the directory existed, holding nothing or part of a tree.
|
|
1666
|
+
// The next run would have seen it, answered «the node already has
|
|
1667
|
+
// this build», and skipped the delivery — reporting a saved transfer
|
|
1668
|
+
// where there was a missing portal. (omni-74 saw this coming before
|
|
1669
|
+
// the second attempt reached the step.)
|
|
1670
|
+
//
|
|
1671
|
+
// So readiness is a RECORD, written last, exactly as the app
|
|
1672
|
+
// artifacts do it with `.artifact-sha256`: a marker that cannot
|
|
1673
|
+
// outlive a failed unpack because it is written after one succeeds.
|
|
1674
|
+
//
|
|
1675
|
+
// Not `test -s index.html` either, which was the first idea: that is
|
|
1676
|
+
// a sign the CONTENT is present, and half a tree contains an
|
|
1677
|
+
// index.html too. The marker is a sign the OPERATION completed.
|
|
1678
|
+
// (omni-74 drew that distinction.)
|
|
1679
|
+
// It sits beside the directory rather than inside it — everything
|
|
1680
|
+
// inside is served to anyone who asks.
|
|
1681
|
+
const marker = `${remoteDir}.delivered`;
|
|
1682
|
+
const exists = await this.sshExec(target, `test -s ${shellEscape(marker)} && test -d ${shellEscape(remoteDir)} && echo yes || echo no`).catch(() => 'no');
|
|
1683
|
+
//
|
|
1684
|
+
// What «already there» excuses is the TRANSFER, not the deployment.
|
|
1685
|
+
// This used to `return` here, and under the old mount that was exact:
|
|
1686
|
+
// the gateway mounted the build directory itself, so a container
|
|
1687
|
+
// recreated with this digest as its source served this digest, and
|
|
1688
|
+
// nothing else had to happen. Serving through
|
|
1689
|
+
// `current-<project>-<stack>` moves that fact into the link, and a
|
|
1690
|
+
// deployment that does not point the link has not deployed.
|
|
1691
|
+
//
|
|
1692
|
+
// The case is a ROLLBACK, and only a rollback: forward releases carry
|
|
1693
|
+
// new content, so a new digest, so this is false. Deploy A, deploy B,
|
|
1694
|
+
// then ask for A again — A's bundle and its marker are still on the
|
|
1695
|
+
// node, the early return fired, and the link stayed on B. The gateway
|
|
1696
|
+
// went on serving B while the release said A, with nothing anywhere
|
|
1697
|
+
// saying so. Which is the version you least want wrong.
|
|
1698
|
+
const alreadyThere = exists.trim() === 'yes';
|
|
1699
|
+
// Whatever a previous attempt left half-written is not a starting
|
|
1700
|
+
// point: unpacking onto it would merge two trees.
|
|
1701
|
+
if (!alreadyThere) {
|
|
1702
|
+
await this.sshExec(target, `rm -rf ${shellEscape(remoteDir)} ${shellEscape(marker)}`).catch(() => undefined);
|
|
1703
|
+
await this.sshExec(target, `mkdir -p ${shellEscape(remoteDir)}`);
|
|
1704
|
+
await this.uploadToNode(target, archive, remoteFile);
|
|
1705
|
+
// `uploadFile` already refuses a file that landed SHORT. That catches
|
|
1706
|
+
// the likely failure and not the other one: a transfer that completes
|
|
1707
|
+
// and corrupts in the middle has the right length and the wrong
|
|
1708
|
+
// bytes, and `tar` would report it three steps later as a broken
|
|
1709
|
+
// archive rather than as a bad transfer.
|
|
1710
|
+
//
|
|
1711
|
+
// The sum costs nothing to obtain here, because it is already the
|
|
1712
|
+
// name of the directory this is going into. `sha256sum` on the node,
|
|
1713
|
+
// `shasum -a 256` where that is what exists — asking for both and
|
|
1714
|
+
// taking whichever answers beats assuming the platform.
|
|
1715
|
+
// (omni-74's objection: the size check was one class of failure, and
|
|
1716
|
+
// the stronger check needed no new information.)
|
|
1717
|
+
const sum = await this.sshExec(target, `sha256sum ${shellEscape(remoteFile)} 2>/dev/null || shasum -a 256 ${shellEscape(remoteFile)}`);
|
|
1718
|
+
const landed = sum.trim().split(/\s+/)[0]?.slice(0, 16);
|
|
1719
|
+
if (landed !== digest) {
|
|
1720
|
+
await this.sshExec(target, `rm -f ${shellEscape(remoteFile)}`).catch(() => undefined);
|
|
1721
|
+
throw new Error(`The static bundle arrived corrupt: sent sha256 ${digest}, the node has ${landed ?? 'no readable sum'}. ` +
|
|
1722
|
+
'Nothing was unpacked and the partial file was removed.');
|
|
1723
|
+
}
|
|
1724
|
+
await this.sshExec(target, `tar -xzf ${shellEscape(remoteFile)} -C ${shellEscape(remoteDir)} && rm -f ${shellEscape(remoteFile)}`);
|
|
1725
|
+
}
|
|
1726
|
+
// A tab opened before this deployment asks for chunks of the build it
|
|
1727
|
+
// loaded; they are carried into this one, and builds nobody serves go
|
|
1728
|
+
// (`static-carry-forward.ts`). Before the marker: it is part of what
|
|
1729
|
+
// «delivered» means now.
|
|
1730
|
+
await this.carryPreviousBuildForward(target, remoteRoot, remoteDir);
|
|
1731
|
+
// AFTER the carry-forward, because that reads the link to find the build
|
|
1732
|
+
// open tabs loaded — the one being replaced.
|
|
1733
|
+
if (link)
|
|
1734
|
+
await this.pointStaticLinkAt(target, remoteRoot, link, digest);
|
|
1735
|
+
// Last, and only now: the unpack is what this records.
|
|
1736
|
+
if (!alreadyThere)
|
|
1737
|
+
await this.sshExec(target, `printf %s ${shellEscape(digest)} > ${shellEscape(marker)}`);
|
|
1738
|
+
this.logger.info({ host: target.host, remoteDir, bytes: alreadyThere ? 0 : bytes, transferred: !alreadyThere }, alreadyThere ? 'The node already had this build — pointed at it' : 'Static bundle delivered to the node');
|
|
1739
|
+
return { remoteDir, bytes: alreadyThere ? 0 : bytes };
|
|
1740
|
+
}
|
|
1741
|
+
finally {
|
|
1742
|
+
await fsp.rm(staging, { recursive: true, force: true }).catch(() => undefined);
|
|
1743
|
+
}
|
|
1744
|
+
}
|
|
1745
|
+
/**
|
|
1746
|
+
* Point a stack's `current-<project>-<stack>` at the build just unpacked.
|
|
1747
|
+
*
|
|
1748
|
+
* This is the whole reason the gateway stops being recreated on a
|
|
1749
|
+
* deployment: it mounts the static ROOT, whose path never changes, and
|
|
1750
|
+
* serves `/var/www/portal/<link>`. nginx resolves `root` per request and
|
|
1751
|
+
* this config sets no `open_file_cache`, so the swap is picked up with no
|
|
1752
|
+
* reload and no restart. What it replaces cost **1 s of 000** on the
|
|
1753
|
+
* gateway, measured on the release-4 deployment (81 s total).
|
|
1754
|
+
*
|
|
1755
|
+
* RELATIVE, so it resolves inside the container as well as on the host — the
|
|
1756
|
+
* host path is not the container path.
|
|
1757
|
+
*
|
|
1758
|
+
* And swapped with `mv -T`, never `ln -sfn`: `-f` unlinks before it
|
|
1759
|
+
* symlinks, and a request arriving in that window gets a 404 from a web root
|
|
1760
|
+
* that does not exist. `rename(2)` has no window at all. The temporary name
|
|
1761
|
+
* carries the digest so two deliveries racing on one node cannot collide on
|
|
1762
|
+
* it.
|
|
1763
|
+
*/
|
|
1764
|
+
async pointStaticLinkAt(target, root, link, digest) {
|
|
1765
|
+
const tmp = `${root}/${link}.${digest}.tmp`;
|
|
1766
|
+
await this.sshExec(target, `ln -s ${shellEscape(digest)} ${shellEscape(tmp)} && mv -T ${shellEscape(tmp)} ${shellEscape(`${root}/${link}`)}`);
|
|
1767
|
+
this.logger.info({ host: target.host, link, digest }, 'The gateway now serves this build through its link');
|
|
1768
|
+
}
|
|
1769
|
+
async carryPreviousBuildForward(target, input, fresh) {
|
|
1770
|
+
if (typeof input !== 'string') {
|
|
1771
|
+
const { immutableAssetsRoot, immutableAssetsScript } = await import('./static-carry-forward.js');
|
|
1772
|
+
const expectedRoot = immutableAssetsRoot(input.owner);
|
|
1773
|
+
// The native execution fence serializes every publisher, including reentrant calls.
|
|
1774
|
+
return this.underLease(target, 'retain immutable public assets', async () => {
|
|
1775
|
+
await this.assertNoPendingHostOperations(target);
|
|
1776
|
+
const fs = await import('node:fs/promises');
|
|
1777
|
+
const { tmpdir } = await import('node:os');
|
|
1778
|
+
const { join } = await import('node:path');
|
|
1779
|
+
const { randomUUID } = await import('node:crypto');
|
|
1780
|
+
const temporary = await fs.mkdtemp(join(tmpdir(), 'omnitron-static-assets-'));
|
|
1781
|
+
const local = join(temporary, 'request.json');
|
|
1782
|
+
const remote = '/tmp/omnitron-static-assets-' + randomUUID() + '.json';
|
|
1783
|
+
try {
|
|
1784
|
+
await fs.writeFile(local, JSON.stringify(input), { mode: 0o600, flag: 'wx' });
|
|
1785
|
+
await this.uploadToNode(target, local, remote);
|
|
1786
|
+
const output = await this.sshExec(target, immutableAssetsScript(remote), 300_000);
|
|
1787
|
+
const receipt = JSON.parse(output);
|
|
1788
|
+
if (receipt.assetsRoot !== expectedRoot || !/^[a-f0-9]{64}$/.test(receipt.manifestSha256) ||
|
|
1789
|
+
![receipt.files, receipt.added, receipt.reused, receipt.bytes].every((n) => Number.isSafeInteger(n) && n >= 0)) {
|
|
1790
|
+
throw new Error('Invalid immutable asset publication receipt');
|
|
1791
|
+
}
|
|
1792
|
+
this.logger.info({ host: target.host, owner: input.owner, ...receipt }, 'Immutable public asset bytes retained');
|
|
1793
|
+
return receipt;
|
|
1794
|
+
}
|
|
1795
|
+
finally {
|
|
1796
|
+
await fs.rm(temporary, { recursive: true, force: true });
|
|
1797
|
+
await this.sshExec(target, 'rm -f -- ' + shellEscape(remote)).catch(() => undefined);
|
|
1798
|
+
}
|
|
1799
|
+
});
|
|
1800
|
+
}
|
|
1801
|
+
const root = input;
|
|
1802
|
+
if (!fresh)
|
|
1803
|
+
throw new Error('Missing legacy static build');
|
|
1804
|
+
try {
|
|
1805
|
+
const { GATEWAY_MOUNTS_COMMAND, readGatewayMounts, carryForwardScript } = await import('./static-carry-forward.js');
|
|
1806
|
+
const mounts = readGatewayMounts(await this.sshExec(target, GATEWAY_MOUNTS_COMMAND).catch(() => ''));
|
|
1807
|
+
const script = carryForwardScript({ root, fresh, serving: mounts.serving, mounted: mounts.mounted, keep: 5 });
|
|
1808
|
+
const said = await this.sshExec(target, `bash -c ${shellEscape(script)}`, 300_000);
|
|
1809
|
+
this.logger.info({ host: target.host, fresh, serving: mounts.serving, said: said.trim() }, 'The build open tabs loaded is carried forward; builds nobody serves are removed');
|
|
1810
|
+
}
|
|
1811
|
+
catch (err) {
|
|
1812
|
+
this.logger.warn({ host: target.host, fresh, error: err.message.slice(0, 400) }, 'Could not carry the previous build forward — open tabs will reload onto the new one');
|
|
1813
|
+
}
|
|
1814
|
+
}
|
|
1815
|
+
async signalRemoteDaemon(target, appName) {
|
|
278
1816
|
try {
|
|
279
|
-
//
|
|
280
|
-
await this.sshExec(
|
|
1817
|
+
// No `2>/dev/null`, no `|| true`: the failure IS the information.
|
|
1818
|
+
const out = await this.sshExec(target, `omnitron restart ${shellEscape(appName)} 2>&1`);
|
|
1819
|
+
const failed = /unknown app|failed|not found|no such/i.test(out);
|
|
1820
|
+
if (failed) {
|
|
1821
|
+
this.logger.error({ host: target.host, app: appName, detail: out.trim().slice(0, 300) }, 'The node refused to start this app — its artifact is installed and its daemon does not know the app');
|
|
1822
|
+
return { ok: false, detail: out.trim().slice(0, 300) };
|
|
1823
|
+
}
|
|
1824
|
+
return { ok: true, detail: out.trim().slice(0, 200) };
|
|
281
1825
|
}
|
|
282
|
-
catch {
|
|
283
|
-
|
|
284
|
-
this.logger.
|
|
1826
|
+
catch (err) {
|
|
1827
|
+
const detail = err.message;
|
|
1828
|
+
this.logger.error({ host: target.host, app: appName, error: detail }, 'Could not reach the node to start this app');
|
|
1829
|
+
return { ok: false, detail };
|
|
285
1830
|
}
|
|
286
1831
|
}
|
|
287
|
-
|
|
288
|
-
|
|
1832
|
+
/**
|
|
1833
|
+
* Record that the app now runs this artifact — written only after it came
|
|
1834
|
+
* up healthy on it, never at unpack time, so a deployment interrupted
|
|
1835
|
+
* between installing and starting leaves the old record and the next one
|
|
1836
|
+
* ships and starts again. No checksum, no record: the next deployment then
|
|
1837
|
+
* cannot tell, and «cannot tell» deploys.
|
|
1838
|
+
*
|
|
1839
|
+
* A write that fails is said and not fatal: the app runs; the next
|
|
1840
|
+
* deployment transfers once more than it needed to.
|
|
1841
|
+
*/
|
|
1842
|
+
async recordRunningArtifact(target, remotePath, app, checksum) {
|
|
1843
|
+
if (!checksum)
|
|
1844
|
+
return;
|
|
289
1845
|
try {
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
1846
|
+
await this.sshExec(target, `printf %s ${shellEscape(checksum)} > ${shellEscape(`${remotePath}/${ARTIFACT_CHECKSUM_FILE}`)}`);
|
|
1847
|
+
}
|
|
1848
|
+
catch (err) {
|
|
1849
|
+
this.logger.warn({ node: target.host, app, error: err.message }, 'The app runs its new artifact and the node could not record which — the next deployment ships it again');
|
|
1850
|
+
}
|
|
1851
|
+
}
|
|
1852
|
+
/**
|
|
1853
|
+
* The apps this node reports as online, by their bare names.
|
|
1854
|
+
*
|
|
1855
|
+
* One question for the whole node. The per-app check beside this one asks
|
|
1856
|
+
* the same command, and asking it six times to decide six restarts is six
|
|
1857
|
+
* SSH round trips for one answer.
|
|
1858
|
+
*
|
|
1859
|
+
* An empty set on any failure: not knowing what is running is not a claim
|
|
1860
|
+
* that nothing is, and the caller's fallback — restart it — is the safe
|
|
1861
|
+
* reading of "we could not tell".
|
|
1862
|
+
*/
|
|
1863
|
+
async appsOnline(target, project) {
|
|
1864
|
+
try {
|
|
1865
|
+
const answer = await this.askNodeStatus(target);
|
|
1866
|
+
const status = readNodeStatus(answer.raw);
|
|
1867
|
+
if (!status) {
|
|
1868
|
+
this.logger.warn({ node: target.host, project, said: answer.words }, 'The node did not answer what it is running — every app will be restarted');
|
|
1869
|
+
return new Set();
|
|
296
1870
|
}
|
|
1871
|
+
return new Set(status.apps
|
|
1872
|
+
.filter((app) => app.status === 'online' && typeof app.name === 'string')
|
|
1873
|
+
.map((app) => {
|
|
1874
|
+
const name = app.name;
|
|
1875
|
+
return name.includes('/') ? name.slice(name.lastIndexOf('/') + 1) : name;
|
|
1876
|
+
}));
|
|
297
1877
|
}
|
|
298
|
-
catch {
|
|
299
|
-
|
|
1878
|
+
catch (err) {
|
|
1879
|
+
this.logger.warn({ node: target.host, project, error: err.message }, 'Could not read what this node is running — every app will be restarted');
|
|
1880
|
+
return new Set();
|
|
300
1881
|
}
|
|
301
1882
|
}
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
1883
|
+
/**
|
|
1884
|
+
* Whether the app is actually running on the node.
|
|
1885
|
+
*
|
|
1886
|
+
* This returned `void` on every path — a match, a mismatch, a parse failure,
|
|
1887
|
+
* an unreachable node — under a comment reading "Health check is
|
|
1888
|
+
* best-effort". A verification that cannot fail is not a verification, and
|
|
1889
|
+
* it is worse than none: its presence in the sequence is what persuades a
|
|
1890
|
+
* reader that the deployment was checked.
|
|
1891
|
+
*
|
|
1892
|
+
* The reading itself is in `readNodeHealth`, where the test can reach it.
|
|
1893
|
+
*/
|
|
1894
|
+
async verifyHealth(target, appName, project) {
|
|
1895
|
+
const answer = await this.askNodeStatus(target);
|
|
1896
|
+
if (answer.code === UNREADABLE) {
|
|
1897
|
+
return { online: false, detail: `could not read the node's status: ${answer.words}` };
|
|
1898
|
+
}
|
|
1899
|
+
const health = readNodeHealth(answer.raw, appName, project);
|
|
1900
|
+
return health.online || !answer.words ? health : { online: false, detail: `${health.detail}: ${answer.words}` };
|
|
1901
|
+
}
|
|
1902
|
+
/**
|
|
1903
|
+
* `omnitron status --json` on a node: its stdout is DATA, so it comes
|
|
1904
|
+
* through the data channel — past the transport's masker, which rewrites a
|
|
1905
|
+
* value such as `"token": …` into something that is no longer JSON — and
|
|
1906
|
+
* its stderr stays words, masked, and separate. `2>&1` used to fold the two
|
|
1907
|
+
* together, so an answer that could not be parsed was quoted into the log
|
|
1908
|
+
* whole. Three readers: the daemon check before a restart, `appsOnline`,
|
|
1909
|
+
* `verifyHealth`. None logs `raw`.
|
|
1910
|
+
*
|
|
1911
|
+
* `UNREADABLE` when the node could not be asked at all, with the reason as
|
|
1912
|
+
* the words.
|
|
1913
|
+
*/
|
|
1914
|
+
async askNodeStatus(target) {
|
|
1915
|
+
try {
|
|
1916
|
+
const answer = await this.readFromNode(target, 'omnitron status --json', 15_000);
|
|
1917
|
+
return { raw: answer.stdout, words: answer.stderr.trim().split('\n')[0] ?? '', code: answer.code };
|
|
1918
|
+
}
|
|
1919
|
+
catch (err) {
|
|
1920
|
+
return { raw: '', words: err.message, code: UNREADABLE };
|
|
313
1921
|
}
|
|
314
|
-
args.push(`${user}@${node.host}`);
|
|
315
|
-
return args;
|
|
316
1922
|
}
|
|
317
1923
|
// ===========================================================================
|
|
318
1924
|
// Private — Progress
|
|
319
1925
|
// ===========================================================================
|
|
320
1926
|
emitProgress(node, app, status, progress, message) {
|
|
321
1927
|
const event = { node, app, status, progress, message };
|
|
1928
|
+
this.lastProgress.set(`${node}\u0000${app}`, { ...event, at: new Date().toISOString() });
|
|
322
1929
|
for (const handler of this.progressHandlers) {
|
|
323
1930
|
try {
|
|
324
1931
|
handler(event);
|
|
@@ -328,5 +1935,15 @@ export default {
|
|
|
328
1935
|
}
|
|
329
1936
|
}
|
|
330
1937
|
}
|
|
1938
|
+
/**
|
|
1939
|
+
* What each app on each node is doing, newest first.
|
|
1940
|
+
*
|
|
1941
|
+
* For a console that polls. Returned as a list rather than the map so a
|
|
1942
|
+
* caller cannot hold a reference that keeps changing underneath it while
|
|
1943
|
+
* it renders.
|
|
1944
|
+
*/
|
|
1945
|
+
getProgress() {
|
|
1946
|
+
return [...this.lastProgress.values()].sort((a, b) => (a.at < b.at ? 1 : a.at > b.at ? -1 : 0));
|
|
1947
|
+
}
|
|
331
1948
|
}
|
|
332
1949
|
//# sourceMappingURL=remote-deployer.service.js.map
|