Skip to content

Deployment

This repository publishes container images and, for each service, the environment contract that configures it. The KELT repository renders the Kubernetes manifests and pins the image versions that run on the testbed. This page covers what an image expects from its deployment.

Images

Image Port Published
camara-gateway 8080 yes
positioning-engine 8080 yes
wifi-adapter 8080 yes
vendor-adapter 8080 yes
synthetic-adapter 8080 yes
placement-editor 8080 yes
location-app 80 yes
mock-vendor 8080 no, built from source by make demo

A published image is ghcr.io/jacobbista/5g-northbound/<image>:<version>. The Python images run as the non-root user app, uid 1001. location-app is an nginx image serving the built bundle.

Everything in an image has a fixed version. Each FROM names its base image by digest. A Python image installs with services/<image>/constraints.txt, which pins every package it adds to the base, and the tests in CI install with the same file. The JavaScript bundles install from package-lock.json with npm ci. make lock moves the base images to the digests their tags point at now and regenerates the constraints. It is run on purpose, after a dependency changes or to take upstream fixes, and followed by make test.

An adapter that carries vendor SDKs or material under NDA is built in a private repository and implements the same adapter contract. The cluster runs it like a published adapter, with an imagePullSecret.

Releases

A tag v* triggers build.yml. It builds the images whose services/<image> directory changed since the previous tag, pushes each as <version> and latest, and creates a GitHub Release that lists them. Image versions therefore differ between services. The GitHub Release names what a tag published. The KELT pins name what runs.

git tag v0.19.0
git push origin v0.19.0

GHCR creates a new package as private. It is made public once, by hand, in the package settings.

Every push runs test.yml (the Python and JavaScript suites) and checks.yml (environment contracts and sensitivity manifest, positioning fabric, compose file, profiled specification freshness, leak scan).

Configuration

A service is configured only through environment variables. Each service declares them in env.contract.yaml beside its code, in the format described in deploy/contracts, and a running pod serves the same declaration on GET /contract. The contract is the reference for names, defaults and meaning.

A variable with sensitive: true goes into a Secret, every other one into a ConfigMap, and both reach the pod through envFrom. deploy/contracts/sensitivity-manifest.json, generated by make contracts, lists every variable with its routing for KELT.

location-app and placement-editor run in the browser. At start their entrypoint.sh writes the variables marked runtime_layer: window.__ENV__ into env-config.js, so these too are set as pod environment. A changed Secret or ConfigMap takes effect after kubectl rollout restart.

make env-check                                        # compose environment against the contracts
python3 deploy/tools/contracts.py render-k8s <image>  # ConfigMap and Secret skeleton for one service

Wiring

Service Variable Points to
camara-gateway POSITIONING_ENGINE_URL positioning-engine
camara-gateway WIFI_ADAPTER_URL wifi-adapter, for GET /anchors/calibration. Empty disables that route
camara-gateway KEYCLOAK_URL, KEYCLOAK_REALM the identity provider that signs the tokens
each adapter POSITIONING_ENGINE_URL positioning-engine, to register and to read the blueprint
each adapter ADAPTER_BASE_URL the adapter's own Service, which the engine polls
placement-editor POSITIONING_ENGINE_URL, WIFI_ADAPTER_URL, VENDOR_ADAPTER_URL the services its proxy routes reach
location-app VITE_CAMARA_API_BASE, VITE_KEYCLOAK_URL the gateway and the identity provider, as the browser reaches them

The engine needs no adapter list: each adapter registers itself and sends a heartbeat (adapter registry). ADAPTER_URLS on the engine only seeds an empty registry.

Every service runs behind a ClusterIP Service. Only camara-gateway, location-app and placement-editor are reached from outside, and each of their contracts names the variable that holds its public origin (external_origin). The edge wifi-scanner posts to wifi-adapter from outside the cluster.

Storage

Service Variable Content Volume
positioning-engine BLUEPRINT_PATH, ADAPTER_REGISTRY_PATH the blueprint, the seeded adapter registrations persistent volume at /app/data
camara-gateway ASSET_STORE_FILE the asset map persistent volume at /app/data
wifi-adapter WIFI_CONFIG_PATH the bindings and the calibration survey persistent volume
vendor-adapter SCHEMA_FILE the vendor schema ConfigMap, read-only

Each persistent volume is written by one pod, so ReadWriteOnce suffices. The pod needs fsGroup: 1001 to write it. A volume starts empty. BLUEPRINT_SEED_PATH and ASSET_SEED_FILE name a read-only file that is copied in once when the store is absent. The bindings are seeded with PUT /bindings, through the editor's ⇪ import bindings.

The real blueprint, bindings and asset map, and the vendor credentials, never enter this repository. The committed files under dev/ and services/location-app/public/layout.example.json are placeholders (blueprint and bindings, asset registry).

Probes

Service Liveness Readiness
wifi-adapter /health /ready: 503 until the blueprint is loaded
vendor-adapter /health /ready: 503 without a schema, or with one that contradicts ADAPTER_CAPABILITIES
synthetic-adapter /health /ready
camara-gateway, positioning-engine, placement-editor /health /health
location-app / /

A pod that is not ready still answers /health and /contract, so its configuration can be read and corrected.

Adding an adapter

  1. Deploy the adapter with a ClusterIP Service in the engine's namespace.
  2. Set POSITIONING_ENGINE_URL, ADAPTER_NAME, ADAPTER_BASE_URL and ADAPTER_CAPABILITIES (what an adapter declares).
  3. The adapter registers at start and sends a heartbeat. The engine drops it after ADAPTER_TTL_S (45 s) without one.
  4. Assets reach it through capabilities whose source is its ADAPTER_NAME.

The engine and the gateway are not restarted.