feat(deploy): register managed platform runtime components
This commit is contained in:
@@ -33,6 +33,24 @@ TASK_INTERNAL_LOGOUT_URL=https://ops.nodedc.ru/api/internal/nodedc/logout/
|
||||
NODEDC_ENGINE_DOCKER_NETWORK=nodedc-demo_default
|
||||
NODEDC_ENGINE_INTERNAL_URL=http://nodedc-demo-nodedc-backend-1:3001
|
||||
|
||||
# Platform Map Gateway — private service and one shared persistent TileCache.
|
||||
# Optional one-time legacy bootstrap only. Preferred operation: an authorized
|
||||
# Foundry admin saves the token through Page Library → Platform settings; it is
|
||||
# then stored in the private NAS cache volume and never exposed to Foundry UI,
|
||||
# deploy artifact or browser env. Do not paste a real token into this example.
|
||||
CESIUM_ION_TOKEN=
|
||||
CESIUM_ION_ASSET_ALLOWLIST=1,2,96188
|
||||
MAP_CACHE_MODE=readwrite
|
||||
MAP_CACHE_MAX_MB=20480
|
||||
MAP_GATEWAY_HOST_BIND=127.0.0.1:18103
|
||||
# Created by the root-owned deploy runner on first Map Gateway rollout.
|
||||
# These are NAS paths, visible through SMB as nodedc-platform/map-gateway/.
|
||||
MAP_LIVE_CACHE_HOST_DIR=/volume1/docker/nodedc-platform/map-gateway/live-tile-cache
|
||||
MAP_OFFLINE_SNAPSHOT_HOST_DIR=/volume1/docker/nodedc-platform/map-gateway/offline-snapshot
|
||||
MAP_GATEWAY_UPSTREAM_ALLOWLIST=api.cesium.com,assets.ion.cesium.com,tile.openstreetmap.org,dev.virtualearth.net,ecn.t0.tiles.virtualearth.net,ecn.t1.tiles.virtualearth.net,ecn.t2.tiles.virtualearth.net,ecn.t3.tiles.virtualearth.net
|
||||
MAP_GATEWAY_LEGACY_CACHE_HOSTS=
|
||||
MAP_GATEWAY_OFFLINE_PROVIDER_ALLOWLIST=
|
||||
|
||||
LAUNCHER_OIDC_ISSUER=https://id.nodedc.ru/application/o/launcher/
|
||||
LAUNCHER_OIDC_CLIENT_ID=nodedc-launcher
|
||||
LAUNCHER_OIDC_CLIENT_SECRET=replace-with-random-synology-secret
|
||||
@@ -55,6 +73,34 @@ SESSION_SECRET=replace-with-random-synology-secret
|
||||
COOKIE_DOMAIN=.nas.nodedc
|
||||
COOKIE_SECURE=false
|
||||
|
||||
# External Data Plane — provider-neutral storage. Generate a distinct database
|
||||
# password; it must never reuse NODEDC_INTERNAL_ACCESS_TOKEN or a provider key.
|
||||
EXTERNAL_DATA_PLANE_TIMESCALE_IMAGE=timescale/timescaledb-ha:pg16.14-ts2.28.2-all
|
||||
EXTERNAL_DATA_PLANE_PG_DB=nodedc_data_plane
|
||||
EXTERNAL_DATA_PLANE_PG_USER=nodedc_data_plane
|
||||
EXTERNAL_DATA_PLANE_PG_PASS=replace-with-random-synology-secret
|
||||
EXTERNAL_DATA_PLANE_HOST_BIND=127.0.0.1:18106
|
||||
EXTERNAL_DATA_PLANE_DATABASE_POOL_SIZE=10
|
||||
EXTERNAL_DATA_PLANE_RAW_RETENTION_DAYS=14
|
||||
EXTERNAL_DATA_PLANE_MAX_BATCH_BYTES=5242880
|
||||
EXTERNAL_DATA_PLANE_MAX_FACTS_PER_PUBLISH=5000
|
||||
EXTERNAL_DATA_PLANE_MAX_ATTRIBUTES_BYTES_PER_FACT=65536
|
||||
EXTERNAL_DATA_PLANE_MAX_PATCH_OPERATIONS=500
|
||||
EXTERNAL_DATA_PLANE_MAX_PATCH_BYTES=262144
|
||||
EXTERNAL_DATA_PLANE_PATCH_RETENTION_MS=3600000
|
||||
EXTERNAL_DATA_PLANE_RECEIPT_RETENTION_MS=604800000
|
||||
EXTERNAL_DATA_PLANE_RETENTION_DELETE_LIMIT=10000
|
||||
EXTERNAL_DATA_PLANE_STREAM_HEARTBEAT_MS=20000
|
||||
EXTERNAL_DATA_PLANE_STREAM_POLL_MS=1000
|
||||
EXTERNAL_DATA_PLANE_MAX_READER_STREAMS=10
|
||||
EXTERNAL_DATA_PLANE_WRITER_BINDING_MAX_TTL_DAYS=90
|
||||
EXTERNAL_DATA_PLANE_MAX_FUTURE_SKEW_SECONDS=300
|
||||
EXTERNAL_DATA_PLANE_RETENTION_SWEEP_MS=3600000
|
||||
EXTERNAL_DATA_PLANE_LEGACY_INTAKE_ENABLED=false
|
||||
# The writer-provisioner secret is a root-owned file under
|
||||
# /volume1/docker/nodedc-platform/secrets/external-data-plane-provisioner/,
|
||||
# never a shared .env value.
|
||||
|
||||
NOTIFICATION_PG_DB=nodedc_notifications
|
||||
NOTIFICATION_PG_USER=nodedc_notifications
|
||||
NOTIFICATION_PG_PASS=replace-with-random-synology-secret
|
||||
@@ -68,9 +114,41 @@ NODEDC_AI_WORKSPACE_ASSISTANT_URL=http://ai-workspace-assistant:18082
|
||||
AI_WORKSPACE_OPS_ENTITLEMENT_URL=http://172.22.0.222:18190/api/internal/v1/ai-workspace/entitlements
|
||||
AI_WORKSPACE_OPS_ENTITLEMENT_TOKEN=replace-with-ops-agent-gateway-internal-token
|
||||
AI_WORKSPACE_OPS_ENTITLEMENT_REQUIRED=false
|
||||
# Set only after Foundry passes Launcher handoff and Authentik access checks.
|
||||
# Keep every existing entitlement adapter in this same JSON object. The adapter
|
||||
# uses the existing NODE.DC internal service credential; no Foundry browser or
|
||||
# worker secret is required.
|
||||
# {"module-foundry":{"url":"https://foundry.nodedc.ru/api/ai-workspace/entitlements","required":false}}
|
||||
AI_WORKSPACE_ENTITLEMENT_ADAPTERS_JSON=
|
||||
|
||||
AI_WORKSPACE_HUB_TOKEN=replace-with-random-synology-secret
|
||||
AI_WORKSPACE_HUB_HOST_BIND=0.0.0.0:18081
|
||||
AI_WORKSPACE_HUB_PUBLIC_URL=wss://ai-hub.nodedc.ru/api/ai-workspace/hub
|
||||
AI_WORKSPACE_HUB_INTERNAL_URL=https://ai-hub.nodedc.ru
|
||||
AI_WORKSPACE_HUB_FALLBACK_URLS=
|
||||
AI_WORKSPACE_ONTOLOGY_MCP_ENABLED=true
|
||||
# Optional public HTTP origin for the dynamic worker MCP URL. Normally it is
|
||||
# derived from AI_WORKSPACE_HUB_PUBLIC_URL, so leave this empty.
|
||||
AI_WORKSPACE_ONTOLOGY_MCP_PUBLIC_URL=
|
||||
ONTOLOGY_CORE_HOST_BIND=127.0.0.1:18104
|
||||
|
||||
# Gelios Gateway — storage/read service. Provider credentials stay in the
|
||||
# protected Engine Collector. Intake remains disabled until scope is approved.
|
||||
GELIOS_TIMESCALE_IMAGE=timescale/timescaledb-ha:pg16.14-ts2.28.2-all
|
||||
GELIOS_PG_DB=nodedc_gelios
|
||||
GELIOS_PG_USER=nodedc_gelios
|
||||
GELIOS_PG_PASS=replace-with-random-synology-secret
|
||||
# URL-encode reserved characters in GELIOS_PG_PASS when forming this URL.
|
||||
GELIOS_DATABASE_URL=postgresql://nodedc_gelios:replace-with-url-encoded-synology-secret@gelios-postgres:5432/nodedc_gelios
|
||||
GELIOS_GATEWAY_HOST_BIND=127.0.0.1:18105
|
||||
# Explicit tenant and connection identifiers are deployment configuration;
|
||||
# do not encode a customer or pilot name in source defaults.
|
||||
GELIOS_TENANT_ID=replace-with-tenant-id
|
||||
GELIOS_CONNECTION_ID=gelios-connection-id
|
||||
# `all` accepts every unit returned by this approved tenant + connection.
|
||||
GELIOS_UNIT_SCOPE=allowlist
|
||||
GELIOS_ALLOWED_UNIT_IDS=
|
||||
GELIOS_INTAKE_ENABLED=false
|
||||
GELIOS_RAW_RETENTION_DAYS=14
|
||||
# Presentation state only; it does not change provider collection cadence.
|
||||
GELIOS_POSITION_STALE_AFTER_MS=300000
|
||||
|
||||
@@ -202,6 +202,26 @@ TASKER_SYNC_SOURCE=1 ./infra/synology/deploy-current.sh
|
||||
|
||||
Если emergency-fix был сделан прямо на Synology в этих env-файлах, перенести sanitized-значение в `.env.synology.example`/docs, а секрет оставить только в live env.
|
||||
|
||||
### Gelios: сбор всех units доверенного подключения
|
||||
|
||||
`GELIOS_UNIT_SCOPE=all` означает **все текущие и будущие units только одной уже
|
||||
настроенной пары `GELIOS_TENANT_ID` + `GELIOS_CONNECTION_ID`**. Это не wildcard
|
||||
на другие tenants или providers. Provider credential остаётся только в Engine.
|
||||
Пропавший из очередного ответа unit не удаляется: его stable provider ID и
|
||||
`last_seen_at` сохраняются; видимость на карте — отдельная логика витрины.
|
||||
|
||||
Перед каноническим `nodedc-deploy apply` включить политику на Synology (скрипт
|
||||
создаёт backup и не выводит секреты):
|
||||
|
||||
```bash
|
||||
sudo bash /volume1/docker/nodedc-deploy/inbox/prepare-gelios-all-units-env.sh
|
||||
```
|
||||
|
||||
Затем применить узкий Platform-артефакт, собранный с `--gateway-only`, через
|
||||
`nodedc-deploy`. В его plan должны быть только `gelios-postgres` и
|
||||
`gelios-gateway`; общий `docker-compose` в такой архив не входит. После apply
|
||||
Gateway начинает принимать весь состав units этого подключения.
|
||||
|
||||
## AI Hub relay-only deploy
|
||||
|
||||
Для локального тестирования с внешней Codex-машиной нельзя деплоить Launcher, Engine, Ops, Authentik или Tasker. Единственная допустимая удалённая точка в этой схеме — `ai-workspace-hub`, потому что удалённый worker должен иметь публичный WebSocket/HTTP relay.
|
||||
|
||||
@@ -34,12 +34,28 @@ echo "== ai workspace assistant image build =="
|
||||
cd "${PLATFORM_DIR}/ai-workspace-assistant"
|
||||
"${DOCKER_BIN}" build --no-cache -t nodedc/ai-workspace-assistant:local .
|
||||
|
||||
echo "== ontology core image build =="
|
||||
cd "${PLATFORM_DIR}/ontology-core"
|
||||
"${DOCKER_BIN}" build --no-cache -t nodedc/ontology-core:local .
|
||||
|
||||
echo "== gelios gateway image build =="
|
||||
cd "${PLATFORM_DIR}/gelios-gateway"
|
||||
"${DOCKER_BIN}" build --no-cache -t nodedc/gelios-gateway:local .
|
||||
|
||||
echo "== platform services recreate =="
|
||||
cd "${PLATFORM_DIR}"
|
||||
"${DOCKER_BIN}" compose \
|
||||
--env-file "${ENV_FILE}" \
|
||||
-f "${COMPOSE_FILE}" \
|
||||
up -d --force-recreate reverse-proxy authentik-server authentik-worker notification-postgres notification-core ai-workspace-postgres ai-workspace-assistant ai-workspace-hub launcher
|
||||
up -d --force-recreate reverse-proxy authentik-server authentik-worker notification-postgres notification-core ai-workspace-postgres ontology-core ai-workspace-assistant ai-workspace-hub gelios-postgres gelios-gateway launcher
|
||||
|
||||
echo "== ontology core health check =="
|
||||
"${DOCKER_BIN}" exec nodedc-platform-ontology-core-1 sh -lc \
|
||||
'node -e '"'"'fetch("http://127.0.0.1:18104/healthz").then(async (response) => { console.log(await response.text()); process.exit(response.ok ? 0 : 1); }).catch((error) => { console.error(error); process.exit(1); })'"'"''
|
||||
|
||||
echo "== gelios gateway health check =="
|
||||
"${DOCKER_BIN}" exec nodedc-platform-gelios-gateway-1 sh -lc \
|
||||
'node -e '"'"'fetch("http://127.0.0.1:18105/healthz").then(async (response) => { console.log(await response.text()); process.exit(response.ok ? 0 : 1); }).catch((error) => { console.error(error); process.exit(1); })'"'"''
|
||||
|
||||
echo "== notification core health check =="
|
||||
"${DOCKER_BIN}" exec nodedc-platform-notification-core-1 sh -lc \
|
||||
|
||||
@@ -48,6 +48,7 @@ rsync_dir "${NAS_ROOT}/platform/authentik/" "${BACKUP_DIR}/files/platform/authen
|
||||
rsync_dir "${NAS_ROOT}/platform/notification-core/" "${BACKUP_DIR}/files/platform/notification-core/"
|
||||
rsync_dir "${NAS_ROOT}/platform/ai-workspace-hub/" "${BACKUP_DIR}/files/platform/ai-workspace-hub/"
|
||||
rsync_dir "${NAS_ROOT}/platform/ai-workspace-assistant/" "${BACKUP_DIR}/files/platform/ai-workspace-assistant/"
|
||||
rsync_dir "${NAS_ROOT}/platform/ontology-core/" "${BACKUP_DIR}/files/platform/ontology-core/"
|
||||
rsync_dir "${NAS_ROOT}/bim-viewer/source/server/data/" "${BACKUP_DIR}/files/bim-viewer/server-data/"
|
||||
|
||||
rsync_file "${NAS_ROOT}/platform/.env.synology" "${BACKUP_DIR}/files/platform/"
|
||||
@@ -81,6 +82,7 @@ Contains:
|
||||
- Notification Core source/config: platform/notification-core, platform compose/env
|
||||
- AI Workspace Hub source/config: platform/ai-workspace-hub, platform compose/env
|
||||
- AI Workspace Assistant source/config: platform/ai-workspace-assistant, platform compose/env
|
||||
- Ontology Core source/catalog: platform/ontology-core, platform compose/env
|
||||
- Tasker runtime config: tasker/plane-app/.env.synology, compose, Synology override
|
||||
- Ops Agents Gateway runtime config: ops-agents/.env, compose
|
||||
- BIM Viewer runtime data/config: bim-viewer/source/server/data, .env, compose
|
||||
|
||||
@@ -84,6 +84,22 @@ rsync -av "${RSYNC_METADATA_ARGS[@]}" --delete \
|
||||
"${PLATFORM_REPO}/services/ontology-core/" \
|
||||
"${NAS_ROOT}/platform/ai-workspace-assistant/ontology-core/"
|
||||
|
||||
mkdir -p "${NAS_ROOT}/platform/ontology-core"
|
||||
rsync -av "${RSYNC_METADATA_ARGS[@]}" --delete \
|
||||
--exclude='node_modules/' \
|
||||
--exclude='.env' \
|
||||
--exclude='.env.*' \
|
||||
"${PLATFORM_REPO}/services/ontology-core/" \
|
||||
"${NAS_ROOT}/platform/ontology-core/"
|
||||
|
||||
mkdir -p "${NAS_ROOT}/platform/gelios-gateway"
|
||||
rsync -av "${RSYNC_METADATA_ARGS[@]}" --delete \
|
||||
--exclude='node_modules/' \
|
||||
--exclude='.env' \
|
||||
--exclude='.env.*' \
|
||||
"${PLATFORM_REPO}/services/gelios-gateway/" \
|
||||
"${NAS_ROOT}/platform/gelios-gateway/"
|
||||
|
||||
if [[ "${SYNC_AUTHENTIK_TEMPLATES}" == "1" ]]; then
|
||||
mkdir -p "${NAS_ROOT}/authentik/custom-templates"
|
||||
rsync -av "${RSYNC_METADATA_ARGS[@]}" --delete \
|
||||
@@ -240,7 +256,20 @@ fi
|
||||
|
||||
cat <<'EOF'
|
||||
|
||||
Synced files to NAS mount. Run on Synology to apply runtime changes:
|
||||
Synced source to the NAS mount and staged the root-owned runner candidate.
|
||||
|
||||
For a canonical deploy, do not use the legacy direct runtime scripts below.
|
||||
An administrator first installs/verifies the staged runner, then plans and
|
||||
applies a data-only artifact from /volume1/docker/nodedc-deploy/inbox:
|
||||
|
||||
sudo install -o root -g root -m 0755 \
|
||||
/volume1/docker/nodedc-deploy/runner-install/nodedc-deploy \
|
||||
/usr/local/sbin/nodedc-deploy
|
||||
sudo /usr/local/sbin/nodedc-deploy verify-install
|
||||
sudo /usr/local/sbin/nodedc-deploy plan /volume1/docker/nodedc-deploy/inbox/<artifact>.tgz
|
||||
sudo /usr/local/sbin/nodedc-deploy apply /volume1/docker/nodedc-deploy/inbox/<artifact>.tgz
|
||||
|
||||
Legacy direct runtime scripts (emergency/manual recovery only):
|
||||
|
||||
cd /volume1/docker/nodedc-platform/platform
|
||||
sudo bash apply-current-runtime.sh
|
||||
@@ -276,14 +305,14 @@ cd /volume1/docker/nodedc-platform/platform
|
||||
sudo /usr/local/bin/docker compose \
|
||||
--env-file /volume1/docker/nodedc-platform/platform/.env.synology \
|
||||
-f /volume1/docker/nodedc-platform/platform/docker-compose.platform-http.yml \
|
||||
up -d --build --force-recreate --no-deps ai-workspace-hub ai-workspace-assistant notification-core launcher
|
||||
up -d --build --force-recreate --no-deps ontology-core ai-workspace-hub ai-workspace-assistant notification-core launcher
|
||||
|
||||
Optional Platform/Auth infra apply, only after deliberate compose/proxy/Auth templates changes:
|
||||
|
||||
sudo /usr/local/bin/docker compose \
|
||||
--env-file /volume1/docker/nodedc-platform/platform/.env.synology \
|
||||
-f /volume1/docker/nodedc-platform/platform/docker-compose.platform-http.yml \
|
||||
up -d --build --force-recreate --no-deps reverse-proxy authentik-server authentik-worker ai-workspace-hub ai-workspace-assistant notification-core launcher
|
||||
up -d --build --force-recreate --no-deps reverse-proxy authentik-server authentik-worker ontology-core ai-workspace-hub ai-workspace-assistant notification-core launcher
|
||||
|
||||
After Authentik template rollout, clear global Brand custom CSS from the live DB.
|
||||
NODE.DC login CSS must be template-scoped, not stored in Brand.branding_custom_css:
|
||||
|
||||
@@ -0,0 +1,96 @@
|
||||
name: nodedc-platform
|
||||
|
||||
# This is intentionally an overlay, not a provider service. It is composed
|
||||
# with docker-compose.platform-http.yml by the canonical deploy runner.
|
||||
services:
|
||||
external-data-plane-postgres:
|
||||
image: ${EXTERNAL_DATA_PLANE_TIMESCALE_IMAGE:-timescale/timescaledb-ha:pg16.14-ts2.28.2-all}
|
||||
restart: unless-stopped
|
||||
env_file:
|
||||
- ${NODEDC_SYNOLOGY_ENV_FILE:-.env.synology}
|
||||
environment:
|
||||
POSTGRES_DB: ${EXTERNAL_DATA_PLANE_PG_DB:-nodedc_data_plane}
|
||||
POSTGRES_USER: ${EXTERNAL_DATA_PLANE_PG_USER:-nodedc_data_plane}
|
||||
POSTGRES_PASSWORD: ${EXTERNAL_DATA_PLANE_PG_PASS:?external data plane database password required}
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -d $${POSTGRES_DB} -U $${POSTGRES_USER}"]
|
||||
interval: 30s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
start_period: 30s
|
||||
volumes:
|
||||
- external-data-plane-database:/home/postgres/pgdata/data
|
||||
networks:
|
||||
- external-data-plane
|
||||
|
||||
external-data-plane:
|
||||
image: nodedc/external-data-plane:local
|
||||
restart: unless-stopped
|
||||
env_file:
|
||||
- ${NODEDC_SYNOLOGY_ENV_FILE:-.env.synology}
|
||||
environment:
|
||||
NODE_ENV: production
|
||||
PORT: 18106
|
||||
EXTERNAL_DATA_PLANE_DATABASE_URL: postgresql://${EXTERNAL_DATA_PLANE_PG_USER:-nodedc_data_plane}:${EXTERNAL_DATA_PLANE_PG_PASS:?external data plane database password required}@external-data-plane-postgres:5432/${EXTERNAL_DATA_PLANE_PG_DB:-nodedc_data_plane}
|
||||
EXTERNAL_DATA_PLANE_DATABASE_POOL_SIZE: ${EXTERNAL_DATA_PLANE_DATABASE_POOL_SIZE:-10}
|
||||
EXTERNAL_DATA_PLANE_RAW_RETENTION_DAYS: ${EXTERNAL_DATA_PLANE_RAW_RETENTION_DAYS:-14}
|
||||
EXTERNAL_DATA_PLANE_MAX_BATCH_BYTES: ${EXTERNAL_DATA_PLANE_MAX_BATCH_BYTES:-5242880}
|
||||
EXTERNAL_DATA_PLANE_MAX_FACTS_PER_PUBLISH: ${EXTERNAL_DATA_PLANE_MAX_FACTS_PER_PUBLISH:-5000}
|
||||
EXTERNAL_DATA_PLANE_MAX_ATTRIBUTES_BYTES_PER_FACT: ${EXTERNAL_DATA_PLANE_MAX_ATTRIBUTES_BYTES_PER_FACT:-65536}
|
||||
EXTERNAL_DATA_PLANE_MAX_PATCH_OPERATIONS: ${EXTERNAL_DATA_PLANE_MAX_PATCH_OPERATIONS:-500}
|
||||
EXTERNAL_DATA_PLANE_MAX_PATCH_BYTES: ${EXTERNAL_DATA_PLANE_MAX_PATCH_BYTES:-262144}
|
||||
EXTERNAL_DATA_PLANE_PATCH_RETENTION_MS: ${EXTERNAL_DATA_PLANE_PATCH_RETENTION_MS:-3600000}
|
||||
EXTERNAL_DATA_PLANE_RECEIPT_RETENTION_MS: ${EXTERNAL_DATA_PLANE_RECEIPT_RETENTION_MS:-604800000}
|
||||
EXTERNAL_DATA_PLANE_RETENTION_DELETE_LIMIT: ${EXTERNAL_DATA_PLANE_RETENTION_DELETE_LIMIT:-10000}
|
||||
EXTERNAL_DATA_PLANE_STREAM_HEARTBEAT_MS: ${EXTERNAL_DATA_PLANE_STREAM_HEARTBEAT_MS:-20000}
|
||||
EXTERNAL_DATA_PLANE_STREAM_POLL_MS: ${EXTERNAL_DATA_PLANE_STREAM_POLL_MS:-1000}
|
||||
EXTERNAL_DATA_PLANE_MAX_READER_STREAMS: ${EXTERNAL_DATA_PLANE_MAX_READER_STREAMS:-10}
|
||||
EXTERNAL_DATA_PLANE_WRITER_BINDING_MAX_TTL_DAYS: ${EXTERNAL_DATA_PLANE_WRITER_BINDING_MAX_TTL_DAYS:-90}
|
||||
EXTERNAL_DATA_PLANE_MAX_FUTURE_SKEW_SECONDS: ${EXTERNAL_DATA_PLANE_MAX_FUTURE_SKEW_SECONDS:-300}
|
||||
EXTERNAL_DATA_PLANE_RETENTION_SWEEP_MS: ${EXTERNAL_DATA_PLANE_RETENTION_SWEEP_MS:-3600000}
|
||||
# Migration-only shared-token routes stay closed in a canonical install.
|
||||
EXTERNAL_DATA_PLANE_LEGACY_INTAKE_ENABLED: ${EXTERNAL_DATA_PLANE_LEGACY_INTAKE_ENABLED:-false}
|
||||
NODEDC_INTERNAL_ACCESS_TOKEN: ${NODEDC_INTERNAL_ACCESS_TOKEN:-}
|
||||
# Runner-owned secret file. This credential must never live in the
|
||||
# shared .env.synology that is inherited by unrelated services.
|
||||
EXTERNAL_DATA_PLANE_PROVISIONER_TOKEN_FILE: /run/nodedc-secrets/external-data-plane-provisioner-token
|
||||
# This stays false until the dedicated Engine provisioner is deployed and
|
||||
# receives the same read-only secret mount.
|
||||
EXTERNAL_DATA_PLANE_PROVISIONING_ENABLED: "false"
|
||||
volumes:
|
||||
- type: bind
|
||||
source: /volume1/docker/nodedc-platform/secrets/external-data-plane-provisioner/token
|
||||
target: /run/nodedc-secrets/external-data-plane-provisioner-token
|
||||
read_only: true
|
||||
bind:
|
||||
create_host_path: false
|
||||
expose:
|
||||
- "18106"
|
||||
ports:
|
||||
- "${EXTERNAL_DATA_PLANE_HOST_BIND:-127.0.0.1:18106}:18106"
|
||||
depends_on:
|
||||
external-data-plane-postgres:
|
||||
condition: service_healthy
|
||||
healthcheck:
|
||||
test:
|
||||
- CMD
|
||||
- node
|
||||
- -e
|
||||
- "fetch('http://127.0.0.1:18106/healthz').then((response) => process.exit(response.ok ? 0 : 1)).catch(() => process.exit(1))"
|
||||
interval: 30s
|
||||
timeout: 5s
|
||||
retries: 3
|
||||
start_period: 15s
|
||||
networks:
|
||||
- engine
|
||||
- external-data-plane
|
||||
|
||||
networks:
|
||||
engine:
|
||||
external: true
|
||||
name: ${NODEDC_ENGINE_DOCKER_NETWORK:-nodedc-demo_default}
|
||||
external-data-plane:
|
||||
internal: true
|
||||
|
||||
volumes:
|
||||
external-data-plane-database:
|
||||
@@ -142,6 +142,8 @@ services:
|
||||
AI_WORKSPACE_HUB_INTERNAL_URL: ${AI_WORKSPACE_HUB_INTERNAL_URL:-https://ai-hub.nodedc.ru}
|
||||
AI_WORKSPACE_HUB_TOKEN: ${AI_WORKSPACE_HUB_TOKEN:?ai workspace hub token required}
|
||||
AI_WORKSPACE_HUB_FALLBACK_URLS: ${AI_WORKSPACE_HUB_FALLBACK_URLS:-}
|
||||
AI_WORKSPACE_ONTOLOGY_MCP_ENABLED: ${AI_WORKSPACE_ONTOLOGY_MCP_ENABLED:-true}
|
||||
AI_WORKSPACE_ENTITLEMENT_ADAPTERS_JSON: ${AI_WORKSPACE_ENTITLEMENT_ADAPTERS_JSON:-}
|
||||
NDC_ONTOLOGY_LAUNCHER_BASE_URL: http://launcher:5173
|
||||
expose:
|
||||
- "18082"
|
||||
@@ -154,6 +156,160 @@ services:
|
||||
- identity
|
||||
- engine
|
||||
|
||||
ontology-core:
|
||||
image: nodedc/ontology-core:local
|
||||
build:
|
||||
context: ./ontology-core
|
||||
restart: unless-stopped
|
||||
env_file:
|
||||
- ${NODEDC_SYNOLOGY_ENV_FILE:-.env.synology}
|
||||
environment:
|
||||
NODE_ENV: production
|
||||
PORT: 18104
|
||||
NODEDC_INTERNAL_ACCESS_TOKEN: ${NODEDC_INTERNAL_ACCESS_TOKEN:-}
|
||||
expose:
|
||||
- "18104"
|
||||
ports:
|
||||
- "${ONTOLOGY_CORE_HOST_BIND:-127.0.0.1:18104}:18104"
|
||||
healthcheck:
|
||||
test:
|
||||
- CMD
|
||||
- node
|
||||
- -e
|
||||
- "fetch('http://127.0.0.1:18104/healthz').then((response) => process.exit(response.ok ? 0 : 1)).catch(() => process.exit(1))"
|
||||
interval: 30s
|
||||
timeout: 5s
|
||||
retries: 3
|
||||
start_period: 10s
|
||||
networks:
|
||||
- engine
|
||||
|
||||
# The provider database is private. Gateway is the only component that can
|
||||
# reach it; the host bind is loopback-only for audited operator diagnostics.
|
||||
gelios-postgres:
|
||||
image: ${GELIOS_TIMESCALE_IMAGE:-timescale/timescaledb-ha:pg16.14-ts2.28.2-all}
|
||||
restart: unless-stopped
|
||||
env_file:
|
||||
- ${NODEDC_SYNOLOGY_ENV_FILE:-.env.synology}
|
||||
environment:
|
||||
POSTGRES_DB: ${GELIOS_PG_DB:-nodedc_gelios}
|
||||
POSTGRES_USER: ${GELIOS_PG_USER:-nodedc_gelios}
|
||||
POSTGRES_PASSWORD: ${GELIOS_PG_PASS:?gelios database password required}
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -d $${POSTGRES_DB} -U $${POSTGRES_USER}"]
|
||||
interval: 30s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
start_period: 30s
|
||||
volumes:
|
||||
- gelios-database:/home/postgres/pgdata/data
|
||||
networks:
|
||||
- gelios-data
|
||||
|
||||
gelios-gateway:
|
||||
image: nodedc/gelios-gateway:local
|
||||
build:
|
||||
context: ./gelios-gateway
|
||||
restart: unless-stopped
|
||||
env_file:
|
||||
- ${NODEDC_SYNOLOGY_ENV_FILE:-.env.synology}
|
||||
environment:
|
||||
NODE_ENV: production
|
||||
PORT: 18105
|
||||
DATABASE_URL: ${GELIOS_DATABASE_URL:?gelios database URL required}
|
||||
NODEDC_INTERNAL_ACCESS_TOKEN: ${NODEDC_INTERNAL_ACCESS_TOKEN:-}
|
||||
GELIOS_TENANT_ID: ${GELIOS_TENANT_ID:?gelios tenant id required}
|
||||
GELIOS_CONNECTION_ID: ${GELIOS_CONNECTION_ID:?gelios connection id required}
|
||||
GELIOS_UNIT_SCOPE: ${GELIOS_UNIT_SCOPE:-allowlist}
|
||||
GELIOS_ALLOWED_UNIT_IDS: ${GELIOS_ALLOWED_UNIT_IDS:-}
|
||||
GELIOS_INTAKE_ENABLED: ${GELIOS_INTAKE_ENABLED:-false}
|
||||
GELIOS_RAW_RETENTION_DAYS: ${GELIOS_RAW_RETENTION_DAYS:-14}
|
||||
GELIOS_POSITION_STALE_AFTER_MS: ${GELIOS_POSITION_STALE_AFTER_MS:-300000}
|
||||
expose:
|
||||
- "18105"
|
||||
ports:
|
||||
- "${GELIOS_GATEWAY_HOST_BIND:-127.0.0.1:18105}:18105"
|
||||
depends_on:
|
||||
gelios-postgres:
|
||||
condition: service_healthy
|
||||
healthcheck:
|
||||
test:
|
||||
- CMD
|
||||
- node
|
||||
- -e
|
||||
- "fetch('http://127.0.0.1:18105/healthz').then((response) => process.exit(response.ok ? 0 : 1)).catch(() => process.exit(1))"
|
||||
interval: 30s
|
||||
timeout: 5s
|
||||
retries: 3
|
||||
start_period: 15s
|
||||
networks:
|
||||
- engine
|
||||
- gelios-data
|
||||
|
||||
# One private Platform gateway and one NAS-resident persistent cache serve
|
||||
# every Foundry Application/Map Page instance. It is not published to a browser.
|
||||
map-gateway:
|
||||
image: nodedc/map-gateway:local
|
||||
build:
|
||||
context: ../../services/map-gateway
|
||||
restart: unless-stopped
|
||||
env_file:
|
||||
- ${NODEDC_SYNOLOGY_ENV_FILE:-.env.synology}
|
||||
environment:
|
||||
NODE_ENV: production
|
||||
PORT: 18103
|
||||
# Optional legacy bootstrap. The canonical mutable value, when an
|
||||
# authorized Foundry admin saves it, is the private NAS secret file
|
||||
# inside MAP_CACHE_DIR and never a deploy artifact or browser env.
|
||||
CESIUM_ION_TOKEN: ${CESIUM_ION_TOKEN:-}
|
||||
# Runner-owned file mount; the secret itself is never an env value.
|
||||
NODEDC_MAP_GATEWAY_ADMIN_SECRET_FILE: /run/nodedc-secrets/map-gateway-admin-secret
|
||||
# DC AMD Proxy preserves the AMD workstation's VPN-only Cesium route.
|
||||
# The fixed NAS LAN listener is authenticated by the same runner-managed
|
||||
# file token; neither token is a browser or Foundry env.
|
||||
MAP_GATEWAY_EGRESS_URL: http://172.22.0.222:8790
|
||||
MAP_GATEWAY_EGRESS_PROXY_TOKEN_FILE: /run/nodedc-secrets/map-egress-proxy-token
|
||||
CESIUM_ION_ASSET_ALLOWLIST: ${CESIUM_ION_ASSET_ALLOWLIST:-1,2,96188}
|
||||
MAP_CACHE_DIR: /var/lib/nodedc-map-live-cache
|
||||
MAP_OFFLINE_SNAPSHOT_DIR: /var/lib/nodedc-map-offline-snapshot
|
||||
MAP_CACHE_MODE: ${MAP_CACHE_MODE:-readwrite}
|
||||
MAP_CACHE_MAX_MB: ${MAP_CACHE_MAX_MB:-20480}
|
||||
MAP_GATEWAY_ALLOW_ANONYMOUS: "false"
|
||||
MAP_GATEWAY_TRUSTED_SUBJECT_HEADER: x-nodedc-user-id
|
||||
MAP_GATEWAY_UPSTREAM_ALLOWLIST: ${MAP_GATEWAY_UPSTREAM_ALLOWLIST:-api.cesium.com,assets.ion.cesium.com,tile.openstreetmap.org,dev.virtualearth.net,ecn.t0.tiles.virtualearth.net,ecn.t1.tiles.virtualearth.net,ecn.t2.tiles.virtualearth.net,ecn.t3.tiles.virtualearth.net}
|
||||
MAP_GATEWAY_LEGACY_CACHE_HOSTS: ${MAP_GATEWAY_LEGACY_CACHE_HOSTS:-}
|
||||
MAP_GATEWAY_OFFLINE_PROVIDER_ALLOWLIST: ${MAP_GATEWAY_OFFLINE_PROVIDER_ALLOWLIST:-}
|
||||
volumes:
|
||||
- type: bind
|
||||
source: ${MAP_LIVE_CACHE_HOST_DIR:-/volume1/docker/nodedc-platform/map-gateway/live-tile-cache}
|
||||
target: /var/lib/nodedc-map-live-cache
|
||||
- type: bind
|
||||
source: ${MAP_OFFLINE_SNAPSHOT_HOST_DIR:-/volume1/docker/nodedc-platform/map-gateway/offline-snapshot}
|
||||
target: /var/lib/nodedc-map-offline-snapshot
|
||||
read_only: true
|
||||
- type: bind
|
||||
source: /volume1/docker/nodedc-platform/secrets/map-gateway-admin-secret
|
||||
target: /run/nodedc-secrets/map-gateway-admin-secret
|
||||
read_only: true
|
||||
- type: bind
|
||||
source: /volume1/docker/nodedc-platform/secrets/map-egress-proxy-token
|
||||
target: /run/nodedc-secrets/map-egress-proxy-token
|
||||
read_only: true
|
||||
# Operator-only loopback binding for the root-owned deploy healthcheck.
|
||||
# Foundry reaches the service over the private Docker network; browsers
|
||||
# never receive this address.
|
||||
ports:
|
||||
- "${MAP_GATEWAY_HOST_BIND:-127.0.0.1:18103}:18103"
|
||||
healthcheck:
|
||||
test: ["CMD", "node", "-e", "fetch('http://127.0.0.1:18103/healthz',{headers:{'x-nodedc-user-id':'healthcheck'}}).then((response)=>process.exit(response.ok?0:1)).catch(()=>process.exit(1))"]
|
||||
interval: 30s
|
||||
timeout: 5s
|
||||
retries: 3
|
||||
start_period: 15s
|
||||
networks:
|
||||
- engine
|
||||
- map-egress
|
||||
|
||||
ai-workspace-hub:
|
||||
image: nodedc/ai-workspace-hub:local
|
||||
build:
|
||||
@@ -167,11 +323,15 @@ services:
|
||||
AI_WORKSPACE_HUB_TOKEN: ${AI_WORKSPACE_HUB_TOKEN:?ai workspace hub token required}
|
||||
NODEDC_INTERNAL_ACCESS_TOKEN: ${NODEDC_INTERNAL_ACCESS_TOKEN:-}
|
||||
NODEDC_AI_WORKSPACE_ASSISTANT_URL: http://ai-workspace-assistant:18082
|
||||
NODEDC_ONTOLOGY_CORE_URL: http://ontology-core:18104
|
||||
AI_WORKSPACE_HUB_WS_PATH: /api/ai-workspace/hub
|
||||
expose:
|
||||
- "18081"
|
||||
ports:
|
||||
- "${AI_WORKSPACE_HUB_HOST_BIND:-0.0.0.0:18081}:18081"
|
||||
depends_on:
|
||||
ontology-core:
|
||||
condition: service_healthy
|
||||
networks:
|
||||
- edge
|
||||
- engine
|
||||
@@ -256,6 +416,11 @@ networks:
|
||||
name: ${NODEDC_ENGINE_DOCKER_NETWORK:-nodedc-demo_default}
|
||||
identity:
|
||||
internal: true
|
||||
gelios-data:
|
||||
internal: true
|
||||
map-egress:
|
||||
external: true
|
||||
name: nodedc-map-egress
|
||||
|
||||
volumes:
|
||||
authentik-database:
|
||||
@@ -263,5 +428,6 @@ volumes:
|
||||
authentik-certs:
|
||||
notification-database:
|
||||
ai-workspace-database:
|
||||
gelios-database:
|
||||
caddy-data:
|
||||
caddy-config:
|
||||
|
||||
+39
@@ -0,0 +1,39 @@
|
||||
#!/bin/sh
|
||||
set -eu
|
||||
|
||||
ENV_FILE=${1:-/volume1/docker/nodedc-platform/platform/.env.synology}
|
||||
|
||||
if [ ! -f "$ENV_FILE" ]; then
|
||||
echo "ERROR: env file not found: $ENV_FILE" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if grep -q '^EXTERNAL_DATA_PLANE_PG_PASS=' "$ENV_FILE"; then
|
||||
echo "External Data Plane database credential already exists; no change made."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if ! command -v openssl >/dev/null 2>&1; then
|
||||
echo "ERROR: openssl is required to generate the database password." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
umask 077
|
||||
TMP_FILE=$(mktemp "${ENV_FILE}.external-data-plane.XXXXXX")
|
||||
trap 'rm -f "$TMP_FILE"' EXIT HUP INT TERM
|
||||
PASSWORD=$(openssl rand -hex 32)
|
||||
|
||||
cp "$ENV_FILE" "$TMP_FILE"
|
||||
printf '\n# External Data Plane — generated locally; do not reuse an internal API token.\n' >> "$TMP_FILE"
|
||||
printf 'EXTERNAL_DATA_PLANE_TIMESCALE_IMAGE=timescale/timescaledb-ha:pg16.14-ts2.28.2-all\n' >> "$TMP_FILE"
|
||||
printf 'EXTERNAL_DATA_PLANE_PG_DB=nodedc_data_plane\n' >> "$TMP_FILE"
|
||||
printf 'EXTERNAL_DATA_PLANE_PG_USER=nodedc_data_plane\n' >> "$TMP_FILE"
|
||||
printf 'EXTERNAL_DATA_PLANE_PG_PASS=%s\n' "$PASSWORD" >> "$TMP_FILE"
|
||||
printf 'EXTERNAL_DATA_PLANE_HOST_BIND=127.0.0.1:18106\n' >> "$TMP_FILE"
|
||||
printf 'EXTERNAL_DATA_PLANE_DATABASE_POOL_SIZE=10\n' >> "$TMP_FILE"
|
||||
printf 'EXTERNAL_DATA_PLANE_RAW_RETENTION_DAYS=14\n' >> "$TMP_FILE"
|
||||
printf 'EXTERNAL_DATA_PLANE_MAX_BATCH_BYTES=5242880\n' >> "$TMP_FILE"
|
||||
|
||||
mv "$TMP_FILE" "$ENV_FILE"
|
||||
trap - EXIT HUP INT TERM
|
||||
echo "External Data Plane database configuration was added. The generated password was not printed."
|
||||
+81
@@ -0,0 +1,81 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
# This script is intentionally run on the Synology host, before the canonical
|
||||
# platform artifact apply. It changes collection policy only; provider
|
||||
# credentials remain in the protected Engine credential store.
|
||||
NAS_ROOT="${NAS_ROOT:-/volume1/docker/nodedc-platform}"
|
||||
PLATFORM_ENV="${PLATFORM_ENV:-${NAS_ROOT}/platform/.env.synology}"
|
||||
BACKUP_ROOT="${BACKUP_ROOT:-/volume1/docker/nodedc-deploy/backups/gelios-policy}"
|
||||
TIMESTAMP="${TIMESTAMP:-$(date +%Y%m%d-%H%M%S)}"
|
||||
BACKUP_DIR="${BACKUP_DIR:-${BACKUP_ROOT}/${TIMESTAMP}}"
|
||||
|
||||
read_env() {
|
||||
local file="$1"
|
||||
local key="$2"
|
||||
awk -F= -v key="${key}" '
|
||||
$0 ~ "^[[:space:]]*#" { next }
|
||||
$1 == key {
|
||||
value = substr($0, index($0, "=") + 1)
|
||||
gsub(/^[[:space:]]+|[[:space:]]+$/, "", value)
|
||||
gsub(/^"|"$/, "", value)
|
||||
print value
|
||||
exit
|
||||
}
|
||||
' "${file}"
|
||||
}
|
||||
|
||||
set_env_value() {
|
||||
local file="$1"
|
||||
local key="$2"
|
||||
local value="$3"
|
||||
local tmp
|
||||
tmp="$(mktemp "${file}.tmp.XXXXXX")"
|
||||
awk -v key="${key}" -v value="${value}" '
|
||||
BEGIN { replaced = 0 }
|
||||
$0 ~ "^[[:space:]]*#" { print; next }
|
||||
index($0, key "=") == 1 {
|
||||
print key "=" value
|
||||
replaced = 1
|
||||
next
|
||||
}
|
||||
{ print }
|
||||
END {
|
||||
if (!replaced) print key "=" value
|
||||
}
|
||||
' "${file}" > "${tmp}"
|
||||
chmod 600 "${tmp}"
|
||||
mv "${tmp}" "${file}"
|
||||
}
|
||||
|
||||
if [[ ! -f "${PLATFORM_ENV}" ]]; then
|
||||
echo "missing Platform env: ${PLATFORM_ENV}" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
tenant_id="$(read_env "${PLATFORM_ENV}" GELIOS_TENANT_ID || true)"
|
||||
connection_id="$(read_env "${PLATFORM_ENV}" GELIOS_CONNECTION_ID || true)"
|
||||
if [[ -z "${tenant_id}" || -z "${connection_id}" || "${tenant_id}" == replace-with-* || "${connection_id}" == replace-with-* ]]; then
|
||||
echo "GELIOS_TENANT_ID and GELIOS_CONNECTION_ID must be configured before enabling all-unit collection" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
mkdir -p "${BACKUP_DIR}"
|
||||
backup_path="${BACKUP_DIR}/platform.env.synology.before-gelios-all-units"
|
||||
cp "${PLATFORM_ENV}" "${backup_path}"
|
||||
chmod 600 "${backup_path}"
|
||||
|
||||
# `all` is constrained to the one tenant + connection validated above; it is
|
||||
# not a wildcard across Gelios tenants or other providers. Existing stable
|
||||
# unit rows are never deleted by this policy.
|
||||
set_env_value "${PLATFORM_ENV}" GELIOS_UNIT_SCOPE "all"
|
||||
set_env_value "${PLATFORM_ENV}" GELIOS_INTAKE_ENABLED "true"
|
||||
|
||||
cat <<EOF
|
||||
gelios-all-units-env-prep-ok
|
||||
backup_path=${backup_path}
|
||||
updated_platform_env=${PLATFORM_ENV}
|
||||
unit_scope=all
|
||||
intake_enabled=true
|
||||
secrets_printed=false
|
||||
EOF
|
||||
@@ -63,6 +63,14 @@ echo "== ai workspace assistant health check =="
|
||||
"${DOCKER_BIN}" exec nodedc-platform-ai-workspace-assistant-1 sh -lc \
|
||||
'node -e '"'"'fetch("http://127.0.0.1:18082/healthz").then(async (response) => { console.log(await response.text()); process.exit(response.ok ? 0 : 1); }).catch((error) => { console.error(error); process.exit(1); })'"'"''
|
||||
|
||||
echo "== ontology core health check =="
|
||||
"${DOCKER_BIN}" exec nodedc-platform-ontology-core-1 sh -lc \
|
||||
'node -e '"'"'fetch("http://127.0.0.1:18104/healthz").then(async (response) => { console.log(await response.text()); process.exit(response.ok ? 0 : 1); }).catch((error) => { console.error(error); process.exit(1); })'"'"''
|
||||
|
||||
echo "== gelios gateway health check =="
|
||||
"${DOCKER_BIN}" exec nodedc-platform-gelios-gateway-1 sh -lc \
|
||||
'node -e '"'"'fetch("http://127.0.0.1:18105/healthz").then(async (response) => { console.log(await response.text()); process.exit(response.ok ? 0 : 1); }).catch((error) => { console.error(error); process.exit(1); })'"'"''
|
||||
|
||||
echo "== ai workspace assistant hub target check =="
|
||||
"${DOCKER_BIN}" exec nodedc-platform-ai-workspace-assistant-1 sh -lc \
|
||||
'test "$AI_WORKSPACE_HUB_PUBLIC_URL" = "wss://ai-hub.nodedc.ru/api/ai-workspace/hub" && test -z "$AI_WORKSPACE_HUB_FALLBACK_URLS" && echo ai-workspace-prod-hub-target-ok'
|
||||
|
||||
Reference in New Issue
Block a user