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