diff --git a/api-collection/OpenAgriNet.network-adapter.local-environment.json b/api-collection/OpenAgriNet.network-adapter.local-environment.json new file mode 100644 index 00000000..273aa5c7 --- /dev/null +++ b/api-collection/OpenAgriNet.network-adapter.local-environment.json @@ -0,0 +1,162 @@ +{ + "id": "oan-local-environment-single", + "name": "OpenAgriNet local (single mode)", + "values": [ + { + "key": "consumer-adapter-host", + "value": "http://127.0.0.1:9201", + "enabled": true, + "type": "default", + "description": "Where discover and select enter. Single mode: the one adapter, so all three adapter hosts are the same URL. NETWORK_ADAPTER_PORT in .env." + }, + { + "key": "provider-adapter-host", + "value": "http://127.0.0.1:9201", + "enabled": true, + "type": "default", + "description": "Where publish enters. Single mode: the same adapter as consumer-adapter-host. NETWORK_ADAPTER_PORT in .env." + }, + { + "key": "network-adapter-host", + "value": "http://127.0.0.1:9201", + "enabled": true, + "type": "default", + "description": "Used only by the health check. Single mode: the same adapter; its network tier sits under /_internal/ and is not called from Postman. NETWORK_ADAPTER_PORT in .env." + }, + { + "key": "discovery-host", + "value": "http://127.0.0.1:8090", + "enabled": true, + "type": "default", + "description": "The catalogue index. Reached through an adapter, not directly, so only its health check is here. DISCOVERY_PORT in .env." + }, + { + "key": "registry-host", + "value": "http://127.0.0.1:8081", + "enabled": true, + "type": "default", + "description": "Participants and capability bindings, seeded by bin/setup.py. HOST ONLY: the API base is /api/v1, so a request spells it out. REGISTRY_PORT in .env." + }, + { + "key": "keycloak-host", + "value": "http://127.0.0.1:8080", + "enabled": true, + "type": "default", + "description": "Issues the registry write token, and authenticates nothing else -- it is Sunbird RC's, not the network's. KEYCLOAK_PORT in .env." + }, + { + "key": "keycloak-realm", + "value": "sunbird-rc", + "enabled": true, + "type": "default", + "description": "KEYCLOAK_REALM in .env." + }, + { + "key": "keycloak-client-id", + "value": "registry-frontend", + "enabled": true, + "type": "default", + "description": "The client that logs a user in. admin-api is the one that mints tokens and is not used from here. KEYCLOAK_CLIENT_ID in .env." + }, + { + "key": "registry-user", + "value": "no-user", + "enabled": true, + "type": "default", + "description": "REGISTRY_ADMIN_USER in .env. A shipped default, so it is public until changed." + }, + { + "key": "registry-password", + "value": "no-user-password", + "enabled": true, + "type": "default", + "description": "REGISTRY_ADMIN_PASSWORD in .env. A shipped default, so it is public until changed." + }, + { + "key": "consumer-participant-id", + "value": "consumer.oan.dev", + "enabled": true, + "type": "default", + "description": "The consumer adapter's network identity, and what its signatures verify against. CONSUMER_SUBSCRIBER_ID in .env." + }, + { + "key": "consumer-base-url", + "value": "https://consumer.oan.dev", + "enabled": true, + "type": "default", + "description": "The address PEERS are told to POST to, published in the registry row. Nothing in this stack resolves it, so a wrong value costs nothing locally and everything on a shared network -- and the registry cannot correct it. CONSUMER_BASE_URL in .env." + }, + { + "key": "consumer-participant-key", + "value": "", + "enabled": true, + "type": "default", + "description": "EMPTY ON PURPOSE. The public half lives in quick-start/keys/keys.json, which bin/setup.py generates; paste it in before registering. setup.py has already registered this participant, so it is normally not needed." + }, + { + "key": "mausamgram-participant-id", + "value": "mausamgram", + "enabled": true, + "type": "default", + "description": "IMD Mausamgram, the weather upstream. Half of its binding key. MAUSAMGRAM_PARTICIPANT_ID in .env." + }, + { + "key": "mausamgram-capability", + "value": "openagrinet:WeatherObservation", + "enabled": true, + "type": "default", + "description": "MAUSAMGRAM_CAPABILITY in .env." + }, + { + "key": "mausamgram-binding-key", + "value": "mausamgram|openagrinet:WeatherObservation", + "enabled": true, + "type": "default", + "description": "|. Used to look up the binding's osid. The provider adapter builds this from the incoming payload, so it must match the registry row exactly -- a mismatch is a 404." + }, + { + "key": "mausamgram-base-url", + "value": "https://mausamgram.imd.gov.in", + "enabled": true, + "type": "default", + "description": "MAUSAMGRAM_BASE_URL in .env." + }, + { + "key": "mausamgram-path", + "value": "/nwpapi/get-daily", + "enabled": true, + "type": "default", + "description": "MAUSAMGRAM_PATH in .env." + }, + { + "key": "mausamgram-mapping-url", + "value": "https://raw.githubusercontent.com/OpenAgriNet/helmcharts/0.0.1/quick-start/config/mappings/mausamgram/weather-observation.select.yaml", + "enabled": true, + "type": "default", + "description": "What the binding row publishes and the provider adapter fetches. A TAG ref, so what a running stack reads is fixed at 0.0.1 and a later push does not move it. MAUSAMGRAM_MAPPING_URL in .env." + }, + { + "key": "capability-code", + "value": "openagrinet:WeatherObservation", + "enabled": true, + "type": "default", + "description": "WHICH CAPABILITY THE 4. Capability Schemas FOLDER OPERATES ON -- the one variable to change to work on another, because the schemaUrl and the display name are derived from it rather than written down. The four this deployment serves are openagrinet:WeatherObservation, openagrinet:MandiPrice, openagrinet:KnowledgeAdvisory and openagrinet:AgricultureFacility; network-specs carries four more packs no capability uses yet." + }, + { + "key": "schema-pack-ref", + "value": "main", + "enabled": true, + "type": "default", + "description": "The ref in OpenAgriNet/network-specs that a derived schemaUrl points at. SCHEMA_PACK_REF in .env, which is what bin/setup.py seeds the rows from -- keep the two the same or a create here disagrees with a seeded row. MAIN IS A 404 FOR THESE PATHS TODAY; only schema-packs-v0.1 serves them. The registry checks the URL's shape and never fetches it, so a row stores either way, and main is the ref that becomes correct once the packs merge." + }, + { + "key": "schema-pack-version", + "value": "v0.1", + "enabled": true, + "type": "default", + "description": "The pack version, and the row's version field. All eight packs are v0.1 today. Per capability in setup.py rather than one setting for all four, because a capability can move to v0.2 while the others stay -- so if you point capability-code at one that has, change this too." + } + ], + "_postman_variable_scope": "environment", + "_postman_exported_using": "hand-written, tracked in this repo" +} diff --git a/charts/adapter-service/CHANGELOG.md b/charts/adapter-service/CHANGELOG.md index b8e36b07..7889dcc0 100644 --- a/charts/adapter-service/CHANGELOG.md +++ b/charts/adapter-service/CHANGELOG.md @@ -5,6 +5,85 @@ All notable changes to this chart are documented here. The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this chart adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [0.6.0] - 2026-10-09 + +### Added +- **Two deployment modes, with single as the default.** + `deploymentMode: single-adapter | multi-adapter` (default `single-adapter`), read by + role `network-adapter` only. In single, role `network-adapter` is the Network Adapter Layer: the consumer, network and provider tiers as four + modules of one process (`/` for the consumer, `/publish`, + `/_internal/network/`, `/_internal/provider/`), one release called `network-adapter`. In multi, + `provider`, `network` and `consumer` run one release per tier, unchanged. +- `config/single-adapter/network-adapter-config.yaml`: the layer's adapter config, mounted + like any role's file (an outbound connection pool and log level info on + every module). Each module is its tier's module, so a change to + a tier file that should apply to both deployment modes is made in both. Its routing + is generated at render time from `config/multi-adapter/routing-*.yaml` + (`routing-network-adapter-{consumer,provider}.yaml`), with tier hosts rewritten + to `http://localhost:/_internal/`, so tier hops stay inside the pod. The render fails if `http.port` differs from `service.targetPort`. +- Three identities: each module signs as its own tier from + `adapterKeys.{consumer-adapter,network-adapter,provider-adapter}`, defaulting to + `consumer-adapter-keys`, `network-adapter-keys` and `provider-adapter-keys` + (`adapterKeys.extraSubstitutions` for extra placeholders). A literal + private key in the config, including a `config:` override, fails the render. +- Flat paths: `/discover`, `/publish`, `/` and `/health`, the + same URLs as the multi deployment mode. The network and provider tiers are mounted + under `/_internal/`, which only the process itself calls. +- Ingress for the layer may expose only single-segment `/`, as literal + paths with `pathType` Prefix or Exact and no `use-regex` / `rewrite-target` + annotation; `/`, `/publish` and `/_internal*` are refused. `routing.config` fails the render for this + role. +- `examples/network-adapter.yaml` serves both modes: in single mode the chart adds + `adapterKeys.upstreamSecret` (default `provider-upstream`) to `envFrom`, + so only `--set deploymentMode` changes. `ci/network-adapter-values.yaml` + renders single mode in CI. +- `deploymentMode` is set only in `values.yaml` (default `single-adapter`); + examples and CI values do not set it. `ci/lint-values.yaml` (role `network-adapter`) + therefore renders single mode. +- `SETUP.md`: setup for both modes, including the openbao-secrets changes for multi. + +### Changed +- **Breaking:** `role: network` now defaults to the single deployment mode. + An existing network-tier release must set `deploymentMode: + multi-adapter` (e.g. `--set deploymentMode=multi-adapter`), or the upgrade turns it into the + Network Adapter Layer. That pod then waits on the consumer and provider key + Secrets, which a multi environment does not mirror into `network-adapter`. +- Docs, `values.yaml` and the example headers say single is the default + deployment mode and multi is optional. +- Naming: in single mode the release, Service, app name and namespace are + `network-adapter`, the same as the multi network tier's (telemetry service + `oan-network-adapter`, set in `config/single-adapter/network-adapter-config.yaml`). + Switching modes upgrades that release in place. + +### Fixed +- Liveness and readiness probes now render. Values used `probes.liveness` / + `probes.readiness`, which `common.probes` never read, so no adapter pod had a + readiness probe. Upgrading rolls every adapter pod once. +- The startup probe bounds first boot at 180s (`periodSeconds: 5` x + `failureThreshold: 36`) and holds off liveness until `/health` answers. + Pods that had no probe before now get a liveness probe, and the adapter + fetches `beckn.yaml` at boot, so without this a slow or proxied egress + would get a cold start killed after about 70s. +- NOTES.txt in single mode no longer prints `registry.url` and + `otel.*`: that role mounts `config/single-adapter/network-adapter-config.yaml` as is, so those + values never reached the pod and NOTES reported the opposite of what it ran. + The example and CI values no longer set them. +- The Ingress annotations merge tolerates `ingress.annotations: null`. + `ci/network-adapter-values.yaml` turns the Ingress on, so CI renders the path guard. + +### Removed +- **Breaking:** the `probes.liveness` and `probes.readiness` values keys, renamed to + `livenessProbe` / `readinessProbe`, plus a new `startupProbe`, on by + default. The liveness and readiness timings are unchanged. An environment + that overrides `probes.*` must rename those keys; the old keys never took + effect and are now silently ignored, which is why this is a minor bump + rather than a patch. + +### Unchanged +- Roles `provider`, `network`, `consumer`: same config, routing and manifests + apart from the probes (the chart version label and the config checksum + annotation also change with the version). + ## [0.5.2] - 2026-09-29 ### Changed diff --git a/charts/adapter-service/README.md b/charts/adapter-service/README.md index 7bd5cc81..ad3c2c48 100644 --- a/charts/adapter-service/README.md +++ b/charts/adapter-service/README.md @@ -1,75 +1,63 @@ # adapter-service -One chart for all three Beckn adapters. `provider`, `network` and `consumer` -are the same image and the same config format, so `role` selects the handler -role, the step list and where requests are routed. +One chart for the OAN Beckn adapters. `role` picks the adapter (`provider-adapter`, +`network-adapter` or `consumer-adapter`; the old names `provider`, `network`, `consumer` still work); all three use the same image and config format. -``` - consumer ──unsigned──▶ network ──▶ discovery - │ - provider ──signed─────────┤ - │ └── registry (whose key signed this?) - └──▶ mandi / agmarknet upstreams -``` +Role `network` has two deployment modes, set by `deploymentMode`: -| Role | Handler | Steps | Routes to | -|---|---|---|---| -| `provider` | `bpp` | `validateSign → addRoute → sign` | upstream APIs (several) | -| `network` | `bpp` | `validateSign → addRoute → sign` | discovery | -| `consumer` | `bap` | `addRoute → sign` | network | +- **single (default):** one release runs the consumer, network and provider + tiers as modules of one adapter. Tier-to-tier calls stay inside the pod. +- **multi:** one release per tier (`consumer-adapter`, `network-adapter`, + `provider-adapter`). The network release sets `deploymentMode=multi-adapter`. -`consumer` is the only one that accepts **unsigned** requests: the consumer -app is inside the trust boundary, so there is no network signature to check. -That is why it has no `validateSign`, and why an Ingress on it exposes an -unauthenticated entry point. +`provider-adapter` and `consumer-adapter` releases are always multi-adapter and ignore the value. -Stateless. Each adapter's identity is a keypair in a Secret; everything else it -reads from the registry at request time. +**To install, follow [SETUP.md](SETUP.md).** Why single is the default is in +[ADR-0001](../../docs/adr/0001-single-network-adapter-deployment-mode.md). -## Install +``` + multi: app ──▶ consumer ──▶ network ──▶ discovery + └──────▶ provider ──▶ upstream APIs + + single: app ──▶ network-adapter + / consumer module + /_internal/network/ network module ──▶ discovery + /_internal/provider/ provider module ──▶ upstream APIs + /publish catalogue publishing +``` -Once per role. **Keep the release name per-role** — it is what the Service is -called, which is the DNS name the other adapters address. +## Roles -```sh -helm dependency build charts/adapter-service - -helm upgrade --install provider-adapter charts/adapter-service -n oan \ - -f charts/adapter-service/examples/provider.yaml -helm upgrade --install network-adapter charts/adapter-service -n oan \ - -f charts/adapter-service/examples/network.yaml -helm upgrade --install consumer-adapter charts/adapter-service -n oan \ - -f charts/adapter-service/examples/consumer.yaml -``` +| Role | Handler | Steps | Routes to | +|---|---|---|---| +| `provider-adapter` | `bpp` | `validateSign → addRoute → sign` | upstream APIs | +| `network-adapter` (multi-adapter) | `bpp` | `validateSign → addRoute → sign` | discovery | +| `consumer-adapter` | `bap` | `addRoute → sign` | network | +| `network-adapter` (single-adapter, default) | `bap` + `bpp`, four modules | each module runs its tier's steps | discovery and upstream APIs | -Each example sets `fullnameOverride` to `-adapter`. Without it the Service -would be named `-adapter-service`, and nothing routing by name would -resolve. Rename a release and you must update whatever routes to it — `consumer` -points at the network adapter, `network` points at discovery. +The consumer accepts **unsigned** requests: the app is inside the trust +boundary. An Ingress on it is an unauthenticated entry point, so rate-limit it. -The Secret comes first — the render fails without it. +Callers see the same paths in both modes: `/` (e.g. `/discover`), +`/publish` and `/health`. `/_internal/` is only called by the adapter itself. +The chart refuses an Ingress path other than a single-segment `/`. -## What you must decide +## Required values -Six values have no default, and the chart fails rather than guessing. Each one -is something that produces a *silent* failure if wrong, which is why it is a -render error rather than a default: +The render fails without these: -| Value | Why there is no default | +| Value | Why | |---|---| -| `image.repository` | An empty one renders `ghcr.io/:tag`, which Helm and the API server both accept and which surfaces later as `ImagePullBackOff` | -| `keys.existingSecret.name` | Without an identity the pod starts, serves `/health`, reports Ready, and fails every signature it makes — in a peer's logs | -| `registry.url` | The adapter verifies every caller against the registry, so with none it can verify nobody | -| `role` | It decides the handler role, the step list and the routing target. A default would give one adapter another one's behaviour | -| `routing.rules` | An adapter with no route accepts requests and has nowhere to send them | -| `otel.endpoint` | Only when `otel.enabled` — an enabled exporter with nowhere to send logs a failure every interval | +| `role` | Decides the handler role, the steps and where requests go. | +| `image.repository` | An empty one renders an image name that fails only at pull time. | +| `keys.existingSecret.name` | The adapter's identity (multi roles). Single mode uses `adapterKeys`, which defaults to the Secret names openbao-secrets creates. | ## The identity Secret Six keys, all required: ```sh -kubectl -n oan create secret generic -adapter-keys \ +kubectl -n create secret generic -adapter-keys \ --from-literal=subscriberId=... \ --from-literal=keyId=... \ --from-literal=signingPrivateKey=... \ @@ -78,58 +66,34 @@ kubectl -n oan create secret generic -adapter-keys \ --from-literal=encrPublicKey=... ``` -`keyId` is the **key's osid as the registry assigned it**, not a name you -choose — that is what a verifier looks the key up by. - -In the compose stack `bin/setup.py` generates these and writes them to -`keys/keys.json`. Check the field names there before scripting the command -above; that file's shape belongs to `setup.py`, not to this chart. - -### How the keys reach the process - -The adapter reads one config file and wants the keys inline in it. A plain -ConfigMap would therefore hold private keys, so instead: - -1. the **ConfigMap** holds the config with `__NETWORK_*__` placeholders — the - same shape as the `.tmpl` in the compose stack; -2. the **Secret** is mounted as files, not env vars, so the values are not - readable from `/proc//environ` of anything in the pod; -3. an **init container** substitutes one into the other and writes the result - to an `emptyDir` that dies with the pod. - -It fails loudly if a key is empty or a placeholder survives. That check exists -because the alternative is an adapter that runs, looks healthy, and produces -signatures nobody can verify — a failure that shows up in someone else's logs. +- `keyId` is the key's **osid as the registry assigned it**, not a name you choose. +- The registry needs a `Participant` row for `subscriberId` with the **public** + key. If it is missing or different, peers reject every signature while the + pod still reports Ready. +- The keys never go into the ConfigMap. The ConfigMap holds placeholders, and an + init container fills them from the Secret into an `emptyDir` at pod start. It + fails if a key is empty or a placeholder is left. ## Key rotation needs a restart -The keys Secret is not rendered by this chart, so its contents cannot go into -the config checksum, so changing it restarts nothing: +The chart does not render the keys Secret, so changing it restarts nothing: ```sh -kubectl -n oan rollout restart deploy/-adapter +kubectl -n rollout restart deploy/ ``` -Changing anything else — log level, upstreams, telemetry — rolls the pods on -its own. - -## The registry row - -This chart cannot verify the half of the setup that lives outside the cluster. -The adapter's `subscriberId` needs a `Participant` row in the registry carrying -the **public** half of the keypair. Missing, or carrying a different key, and -every peer rejects what this adapter signs while the pod stays perfectly -healthy. +Any other values change rolls the pods on its own. -## Values +## Values and examples -See `values.yaml` — every field is commented with what it does and what breaks -without it. `examples/{provider,network,consumer}.yaml` are working dev deployments; -`ci/` holds the two files lint renders, one minimal and one with every switch -turned on. +- `values.yaml`: every field, with a comment on what it does. +- `examples/network-adapter.yaml`: role `network-adapter` in either mode (`--set deploymentMode=single-adapter|multi-adapter`). +- `examples/{provider,consumer}-adapter.yaml`: the other two releases in multi mode. +- `ci/*-values.yaml`: what CI lints and renders. ## Related -- `charts/discovery` — where `discover` and `publish` are forwarded -- `charts/registry` — what signatures are verified against -- `docker-deployment/` — the compose stack this chart was modelled on +- `charts/openbao-secrets`: puts the key Secrets in each namespace. +- `charts/discovery`: where `discover` and `publish` are forwarded. +- `charts/registry`: what signatures are verified against. +- `quick-start/`: the same adapters in docker compose. diff --git a/charts/adapter-service/SETUP.md b/charts/adapter-service/SETUP.md new file mode 100644 index 00000000..0684141d --- /dev/null +++ b/charts/adapter-service/SETUP.md @@ -0,0 +1,176 @@ +# Network adapter setup guide + +How to install the network adapter with this chart, in either deployment mode. +Why there are two modes is in +[ADR-0001](../../docs/adr/0001-single-network-adapter-deployment-mode.md). + +## Guidelines + +1. **Use single mode unless you need multi.** Single is the default: one release + runs the consumer, network and provider tiers. Multi runs one release per tier. +2. **Run one mode per environment.** Both modes use release `network-adapter` in + namespace `network-adapter`. +3. **Switch with one value.** The `network-adapter` release always uses the same + command; only `--set deploymentMode=single-adapter|multi-adapter` changes + (default `single-adapter`). `provider` and `consumer` ignore it. +4. **Keep the release and namespace names.** Adapters find each other by + Service name, so `consumer-adapter`, `network-adapter` and `provider-adapter` + must not be renamed. +5. **Keep each tier's keys where they belong.** In single mode `network-adapter` + holds all three tiers' keys. In multi mode it holds only its own. +6. **Never expose `/publish` or `/_internal/` outside the cluster.** Only + `/` paths (for example `/discover`) may go on an Ingress. + +| | single (default) | multi | +|---|---|---| +| Releases | `network-adapter` | `consumer-adapter`, `network-adapter`, `provider-adapter` | +| App calls | `network-adapter.network-adapter.svc:9201/` | `consumer-adapter.consumer-adapter.svc:9202/` | +| Catalogue publisher calls | `network-adapter.network-adapter.svc:9201/publish` | `provider-adapter.provider-adapter.svc:9200/publish` | + +The network release, in either mode: + +```sh +helm upgrade --install network-adapter charts/adapter-service \ + -n network-adapter --create-namespace \ + -f charts/adapter-service/examples/network-adapter.yaml \ + --set deploymentMode=single-adapter # or multi-adapter +``` + +## Single mode (default) + +### 1. Secrets + +These four Secrets must exist in namespace `network-adapter`. +The default openbao-secrets values already mirror all four there. + +| Secret | Holds | +|---|---| +| `consumer-adapter-keys` | consumer tier keys | +| `network-adapter-keys` | network tier keys | +| `provider-adapter-keys` | provider tier keys | +| `provider-upstream` | provider upstream credentials (`adapterKeys.upstreamSecret`) | + +Each keys Secret has the six keys described in [README.md](README.md#the-identity-secret), +and each tier's public key must be in its registry row. + +### 2. Install + +```sh +helm dependency build charts/adapter-service +helm upgrade --install network-adapter charts/adapter-service \ + -n network-adapter --create-namespace \ + -f charts/adapter-service/examples/network-adapter.yaml \ + --set deploymentMode=single-adapter +``` + +If the config needs extra values from a tier's Secret, list them per +environment. Otherwise the init container stops on the unfilled placeholder. + +```yaml +adapterKeys: + extraSubstitutions: + __VISTAAR_TOKEN_URL__: provider/vistaarTokenUrl # / +``` + +### 3. Check + +```sh +kubectl -n network-adapter rollout status deploy/network-adapter +kubectl -n network-adapter port-forward svc/network-adapter 9201:9201 +curl localhost:9201/health # in a second terminal +``` + +## Multi mode + +### 1. Secrets + +Each tier reads its keys from its own namespace, and `network-adapter` must not +keep the other tiers' keys. In the openbao-secrets values for the environment, +remove `network-adapter` from the consumer, provider and upstream entries: + +```yaml + - name: consumer-adapter-keys + namespaces: [consumer-adapter] + - name: network-adapter-keys + namespaces: [network-adapter] + - name: provider-adapter-keys + namespaces: [provider-adapter] + - name: provider-upstream + namespaces: [provider-adapter] +``` + +Edit these entries in place in the values file. Do not pass this block on its +own with `-f`: Helm replaces the whole `secrets` list. Then delete the copies +already in `network-adapter`: + +```sh +kubectl -n network-adapter delete secret consumer-adapter-keys \ + provider-adapter-keys provider-upstream --ignore-not-found +``` + +`examples/provider-adapter.yaml` reads its upstream credentials from a Secret named +`provider-adapter-upstream`. Create it in `provider-adapter`, or change +`envFromSecrets` in your provider values to `provider-upstream`. + +### 2. Install + +```sh +helm upgrade --install provider-adapter charts/adapter-service \ + -n provider-adapter --create-namespace -f charts/adapter-service/examples/provider-adapter.yaml +helm upgrade --install network-adapter charts/adapter-service \ + -n network-adapter --create-namespace -f charts/adapter-service/examples/network-adapter.yaml \ + --set deploymentMode=multi-adapter +helm upgrade --install consumer-adapter charts/adapter-service \ + -n consumer-adapter --create-namespace -f charts/adapter-service/examples/consumer-adapter.yaml +``` + +The network release is the same command as in single mode, with `multi-adapter`. +Without `--set` it would install as single and wait for keys it should not have. + +### 3. Point callers at the new addresses + +- App: `consumer-adapter.consumer-adapter.svc:9202/` +- Catalogue publisher: `provider-adapter.provider-adapter.svc:9200/publish` + +## Switching modes + +A switch upgrades the `network-adapter` release in place. Both modes use the +same keys and registry rows, so the registry does not change. + +**Single to multi:** do the multi steps above, in order. + +**Multi to single:** + +1. Put `network-adapter` back on the consumer, provider and upstream + openbao-secrets entries. +2. Run the network release command with `single`: + ```sh + helm upgrade --install network-adapter charts/adapter-service \ + -n network-adapter -f charts/adapter-service/examples/network-adapter.yaml \ + --set deploymentMode=single-adapter + ``` +3. Point the app and the publisher at `network-adapter.network-adapter.svc:9201`. +4. Remove the other two releases: + ```sh + helm uninstall consumer-adapter -n consumer-adapter + helm uninstall provider-adapter -n provider-adapter + ``` + +## If something is wrong + +| Symptom | Cause | +|---|---| +| Pod stuck in `ContainerCreating` | A Secret it mounts is missing from the namespace. | +| Init container `render-config` exits | A placeholder has no value; add it to `extraSubstitutions`. | +| `401` or `400` on `/_internal/...` from outside the pod | Expected: those paths only accept the adapter's own signed calls. | +| `helm` fails with `deploymentMode must be single-adapter or multi-adapter` | The value is misspelt. | + +## quick-start (docker compose) + +The same two modes run locally with `make`; see `quick-start/README.md`. + +| | single (default) | multi | +|---|---|---| +| Start | `make up` | `NETWORK_ADAPTER_DEPLOYMENT_MODE=multi-adapter make up` | +| App calls | `127.0.0.1:9201/` | `127.0.0.1:9202/` | +| Publisher | `127.0.0.1:9201/publish` | `127.0.0.1:9200/publish` | diff --git a/charts/openbao-secrets/CHANGELOG.md b/charts/openbao-secrets/CHANGELOG.md index 9e3e163e..5d1257f0 100644 --- a/charts/openbao-secrets/CHANGELOG.md +++ b/charts/openbao-secrets/CHANGELOG.md @@ -5,6 +5,14 @@ All notable changes to the `openbao-secrets` chart are documented here. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [0.1.1] - 2026-10-09 + +### Added +- `network-adapter` as a target namespace of `consumer-adapter-keys`, + `provider-adapter-keys` and `provider-upstream`: in the single deployment + mode (the default) `adapter-service` role `network-adapter` runs all three tiers + there. In multi mode, remove `network-adapter` from those three entries. + ## [0.1.0] - 2026-09-27 ### Added diff --git a/docs/adr/0000-template.md b/docs/adr/0000-template.md new file mode 100644 index 00000000..8751ab3f --- /dev/null +++ b/docs/adr/0000-template.md @@ -0,0 +1,24 @@ +# ADR-0000 — Title + +**Status:** Proposed | Accepted | Superseded by ADR-NNNN +**Date:** YYYY-MM-DD +**Issues:** owner/repo#N + +## Context + +What forced a decision. The constraint, not the preamble. + +## Decision + +What was chosen, stated so that a reader can tell whether a given change +complies with it. + +## Alternatives considered + +What was rejected, and the specific property that disqualified it. "It was +worse" is not a reason; "it exposes an unsigned endpoint" is. + +## Consequences + +What this costs, including the cases it makes harder. An ADR that lists only +benefits is advocacy, not a record. diff --git a/docs/adr/0001-single-network-adapter-deployment-mode.md b/docs/adr/0001-single-network-adapter-deployment-mode.md new file mode 100644 index 00000000..d0054158 --- /dev/null +++ b/docs/adr/0001-single-network-adapter-deployment-mode.md @@ -0,0 +1,119 @@ +# ADR-0001 — One Beckn OAN network adapter process for all three tiers, by default + +**Status:** Proposed +**Date:** 2026-10-15 +**Issues:** OpenAgriNet/helmcharts#55, OpenAgriNet/helmcharts#66, OpenAgriNet/network-adapter#55 (parent OpenAgriNet/engineering-tracker#171) + +## Context + +The Beckn OAN network adapter runs in three tiers: consumer (`bap`, 9202), network (`bpp`, 9201) and provider (`bpp`, 9200). Each was its own container in `quick-start/` and its own Helm release in `charts/adapter-service`, installed once per role. The release name is the Service name the other tiers address. + +So every request crossed two containers or pods, with a network hop and a DNS lookup: +- consumer to network for `discover`; +- consumer to provider for `select`; +- provider to network for `publish`. + +Every environment also ran three of every object: Deployment, Service, ConfigMap, and the PDB or HPA where enabled. + +OAN is single-sided today: our app calls out, and our own discovery and upstream APIs answer. The separate scaling and ownership the split allows is not needed. + +Constraints: +- **The registry cannot update a row.** The identities and participant ids already written must keep working. +- **The published Beckn OAN network adapter image must work unchanged.** + +## Decision + +Two deployment modes from the same chart, compose file, keys and registry rows. **`single` is the default; `multi` (the three adapters, unchanged) is optional.** + +In `single`, one Beckn OAN network adapter process listens on **9201** (the multi network tier's port) and mounts four modules. Each module is its tier's module from the tier configs, with the same steps and its own identity and Secret: + +| Module | Path | Steps | Routes to | +|---|---|---|---| +| consumer-adapter | `/` (app calls `/`) | validateSchema, addRoute, sign | `localhost:9201/_internal/network` (discover), `localhost:9201/_internal/provider` (select, init, confirm, status) | +| network-adapter | `/_internal/network/` | validateSign, addRoute | discovery | +| oanProvider | `/_internal/provider/` | validateSign, validateSchema, capability steps | answers itself | +| oanProviderPublish | `/publish` | validateSchema, addRoute, sign | `localhost:9201/_internal/network/publish` | + +A change complies with this decision if: +- **Paths:** the paths callers see stay `/discover`, `/publish`, `/` and `/health`, the same as multi. Tier-to-tier paths stay under `/_internal/`, called only by the process itself. +- **Routing:** each routing module keeps its own routing file (`routing-network-adapter-consumer.yaml`, `routing-network-adapter-provider.yaml`; network reuses `routing-network.yaml`). A module's routing file is its list of allowed actions: `addRoute` runs before `sign`, so an action with no rule errors instead of being forwarded. +- **Helm:** no new role. Role `network-adapter` (old name `network` still accepted) with `deploymentMode: single-adapter` (the default) is the single adapter; with `multi-adapter` it is the network tier. `provider-adapter` and `consumer-adapter` ignore the value. Configs live in `config/single-adapter/` and `config/multi-adapter/`. Both modes release `network-adapter` into namespace `network-adapter`. Routing is generated at render time from the existing `config/multi-adapter/routing-*.yaml`, with hosts rewritten to `localhost`. The render fails on: + - a literal private key; + - `http.port` ≠ `service.targetPort`; + - an unset tier Secret name; + - an Ingress path other than single-segment `/`. +- **Secrets:** `openbao-secrets` reflects the consumer and provider key Secrets and `provider-upstream` into `network-adapter` for single mode. A multi environment removes those targets and deletes the copies, so the network tier holds only its own key. +- **Choosing the mode:** the chart value `deploymentMode: single-adapter | multi-adapter` on the `network-adapter` release; multi also installs `consumer-adapter` and `provider-adapter`. In quick-start, `NETWORK_ADAPTER_DEPLOYMENT_MODE` picks `network-adapter`'s config, and profile `multi-adapter` adds the other two adapters, via `make up` and `NETWORK_ADAPTER_DEPLOYMENT_MODE=multi-adapter make up`. + +## Alternatives considered + +- **Keep three adapters only.** It keeps three of every object and a cross-pod hop on every request, for a separation a single-sided deployment does not use. +- **In-process transport (`inproc://`) between modules.** It needs an adapter image that includes the transport, so it fails the "image unchanged" constraint. The gain does not pay for that dependency: the `localhost` hop was already sub-millisecond on Docker, the prototype saved about 0.1–0.3 ms per hop, and an end-to-end benchmark showed no measurable gain. +- **Tier prefixes in the public paths** (`/network/`, `/provider/`, `/provider/publish`). This was the first version. It put the tiers back into the URLs callers use, gave single and multi different URLs, and needed a per-path allow-list at the edge instead of one `/_internal` deny. Replaced after review. +- **A separate Helm role for the single adapter.** Rejected in favour of one role with a mode switch: a new role duplicates the network tier's install and naming. The cost of the chosen design is that an existing `role: network` release becomes single-adapter on upgrade unless it sets `deploymentMode: multi-adapter` (CHANGELOG, Breaking). +- **One shared routing file for the consumer and publish modules.** The publish module's rule is for the empty action `""`. Shared, it would also apply to the consumer, so `POST /` on the public consumer would publish into the catalogue. +- **Port 9200.** That is the multi provider's port. 9201 is the multi network tier's port and already the chart's default `service.port`. + +## Consequences + +**Gains:** +- One Deployment, Service and ConfigMap per environment. +- No hop between pods. +- No registry, identity or image change. +- Reversible: install the multi releases next to the single one, repoint the app, remove the single release. + +**Costs:** +- **Scaling is per adapter, not per tier.** An environment that needs a tier scaled on its own uses `multi`. +- **One crash takes all three tiers down.** Raise `replicaCount` and enable the optional PDB to limit the blast radius. +- **Tier configs exist twice.** Each module in `network-adapter-config.yaml` mirrors its tier file, so a change meant for both modes is made in both. +- **The catalogue publisher's port differs by mode:** `:9201/publish` in single, `:9200/publish` in multi. + +**Security:** +- `/publish` has no `validateSign`, and `/_internal/` is internal. Their protection is path-level: + - **Helm:** the Ingress guard; + - **quick-start:** the `server_proxy.conf` denies. +- Inside the cluster, anything that reaches port 9201 can call `/publish`. A NetworkPolicy cannot isolate it, because it has no notion of paths. Port-level isolation would need a second listener, which is not built. + +**Outside this repository:** `OpenAgriNet/network-adapter`'s own `install/` and `config/` still describe the per-role layout. Retiring them is the remaining part of network-adapter#55. + +**Verification (2026-10-22, flat paths, port 9201):** quick-start single (11/11 assertions) and multi (10/10), and k3d single (12/12), all passing: publish, discover and select 200, tier hops on `localhost:9201/_internal/`, and on k3d an unsigned call to `/_internal/network/discover` refused with 401. + +## Implementation notes + +Code comments stay at two lines or fewer; the reasoning behind them lives here. + +**Helm (`charts/adapter-service`, `charts/openbao-secrets`)** +- **Mode:** `deploymentMode` is set once, in `values.yaml` (default `single-adapter`), and read only by role `network-adapter`; multi installs pass `--set deploymentMode=multi-adapter`. A `role: network` release without `multi` renders single; in a multi environment its pod then waits on the consumer and provider key Secrets (`adapterKeys`), which are not mirrored there. Setup steps are in `charts/adapter-service/SETUP.md`. +- **Consumer at `/`:** the consumer module is the one catch-all mount, on purpose. `/publish` is an exact pattern and `/_internal//` are longer subtrees, so both win over it. Its routing file is in effect an action allow-list: `addRoute` runs before `sign`, so an action with no rule errors instead of being forwarded. +- **Generated routing:** + - `routing-network-adapter-{consumer,provider}.yaml` are generated from `routing-{consumer,provider}.yaml`. Every `http://-adapter[.][:port][/path]` target becomes `http://localhost:/_internal/[/path]`. Other targets, and rules with no target, are kept as they are. `routing-network.yaml` is reused unchanged. + - Tier hops stay real HTTP calls, not in-process calls, so both modes share one routing source: splitting the modules across pods again changes only the hosts. + - `http.port` in the config must equal `service.targetPort`; the render checks this. +- **Keys:** + - Every `signingPrivateKey` / `encrPrivateKey` in the single-mode config, including a `config:` override, must be a `___*_PRIVATE__` placeholder. This is checked on the parsed config, not with grep. + - The init container fills each tier's placeholders from that tier's Secret, mounted at `/keys/`. + - A missing tier Secret fails the render and names the tier. Otherwise the pod would report Ready and fail every signature that tier makes. +- **Overrides:** `config:` replaces `network-adapter-config.yaml` whole, and must point its routers at `/app/config/routing-network-adapter-*.yaml` and `routing-network.yaml`. `routing.config` and `handler.*` are unused in single mode: setting `routing.config` fails the render. +- **Ingress:** + - Only literal one-segment `/` paths are accepted, with `pathType` Prefix or Exact. + - `/` is refused because a Prefix `/` covers `/publish` and `/_internal`. + - `ImplementationSpecific` is a plain string prefix on ingress-nginx, and `use-regex` / `rewrite-target` turn paths into patterns. Either could reach `/_internal` or `/publish`, so all three are refused. +- **Probes:** the adapter fetches `beckn.yaml` at boot, which on slow or proxied egress can outlast liveness's ~70s budget (10s + 3 × 20s). The startup probe holds liveness off for up to 180s (5s × 36). +- **Probe keys renamed (bug fix, every role):** `values.yaml` had `probes.liveness` and `probes.readiness`, but the shared `common.probes` helper (`charts/common/templates/_helpers.tpl`) reads only `livenessProbe`, `readinessProbe` and `startupProbe`. So on `development` no adapter pod rendered any probe. The keys now match the helper. Existing releases get probes for the first time and roll their pods once, and an override of the old keys must be renamed (it never took effect). +- **Adapter config:** + - Go keeps 2 idle connections per host by default, so every module sets an outbound pool (`httpClientConfig`). + - On the consumer → provider hop, `responseHeaderTimeout` includes the upstream API call. + - The extended schema layer fetches each resource's `@context` per payload. + - With telemetry on and no collector, the exporter logs a failure every few seconds. +- **Secrets:** `openbao-secrets` mirrors `consumer-adapter-keys`, `provider-adapter-keys` and `provider-upstream` into `network-adapter` for single mode. A multi environment removes those targets and deletes the copies, so the network tier holds only its own key. + +**quick-start** +- **Template:** each module in `single-adapter/network-adapter.yaml.tmpl` is an exact copy of its tier template's module. The only differences are the module `name`, the `path` (`/_internal//` for network and provider) and `routingConfig` (`routing-network-adapter-*.yaml`). There is no extra connection pool, and the log level matches the tiers (`debug`). +- **Mode:** + - The mode is `NETWORK_ADAPTER_DEPLOYMENT_MODE` in `.env` (default `single-adapter`; the shell's `MODE` overrides it for `make`). The value is also the config folder, so compose mounts `config/adapters//network-adapter.yaml`, and `COMPOSE_PROFILES` follows it, so `multi-adapter` also starts `provider-adapter` and `consumer-adapter`. Only that one config is mounted, so in multi the network container never holds the consumer or provider keys. + - `up` uses the requested mode. Every other command exports the running mode, read from the config `network-adapter` has mounted. A command that reaches `network-adapter` only through `depends_on` (reverse-proxy, observability) therefore never recreates it with the other config. +- **Upgrade:** a pre-merge stack's `network-adapter` container belongs to the old service `network-adapter-layer`, which compose no longer knows and would not remove. `up` removes it first, because the container name would clash. `down` names every profile, so profiled services stop too. +- **Signing:** signing is off in every quick-start template, so `/_internal/` and `/publish` accept unsigned requests from anyone who can reach `:9201`. Keep the port on `127.0.0.1`. `server_proxy.conf` denies `/publish` (served by the single adapter on 9201) and `^~ /_internal` on every proxy host; `^~` stops regex locations from overriding the deny. +- **Publish:** `/publish` goes through the network module rather than straight to discovery, so one place decides what reaches discovery. The publish mount strips `/publish`, so its rule keys on `""` with `excludeAction`. +- **Base URLs:** in single mode only `CONSUMER_BASE_URL` is a public host. The network and provider rows are read only for key lookup. They stay per tier because the registry cannot update a row, so a later split to multi needs no new participant ids. +- **Known gap:** in multi mode `network-adapter` still receives the provider upstream credentials as environment variables, which its config does not read. diff --git a/docs/adr/README.md b/docs/adr/README.md new file mode 100644 index 00000000..e4ffadcc --- /dev/null +++ b/docs/adr/README.md @@ -0,0 +1,12 @@ +# Architecture Decision Records + +One file per decision, numbered, never edited after acceptance. A decision +that turns out wrong is **superseded** by a new record, not rewritten. The +point of the log is to answer *why does this look the way it does*, and a +rewritten record cannot answer it. + +Start from [`0000-template.md`](0000-template.md). + +| # | Decision | Issues | +|---|---|---| +| [0001](0001-single-network-adapter-deployment-mode.md) | One Beckn OAN network adapter process for all three tiers, by default (`single`); three adapters stay optional (`multi`) | helmcharts#55, #66 | diff --git a/quick-start/README.md b/quick-start/README.md index 1165f014..e2120356 100644 --- a/quick-start/README.md +++ b/quick-start/README.md @@ -1,7 +1,12 @@ # OAN quick-start -The whole OAN stack in Docker Compose: a registry, a discovery service, three -adapters, and two mock upstreams standing in for real provider APIs. +The whole OAN stack in Docker Compose: a registry, a discovery service, the +network adapter, and two mock upstreams standing in for real provider APIs. + +The adapter runs in one of two modes, from the same compose file, keys and +registry. **Single is the default**: one container, `network-adapter`, runs the +consumer, network and provider tiers on port 9201. **Multi is optional**: the +three tiers as three containers. → [Two modes](#two-modes). Nothing is built here — the images are pulled. Locally that is about ten minutes, most of it waiting for Keycloak. Discovery is the one exception, and @@ -30,8 +35,11 @@ capability's provider lives. A `select` answers in the same HTTP round trip — there is no callback. -That is the request order. **Startup order is the reverse**: the consumer -adapter depends on the other two, so Compose brings them up first. The steps +The picture shows the three tiers. In single mode (the default) they are +modules of one container, and the arrows between them are calls inside it. + +That is the request order. **Startup order is the reverse**: in multi mode the +consumer adapter depends on the other two, so Compose brings them up first. The steps below follow startup order, so they work top to bottom. ## Which path are you on? @@ -43,6 +51,7 @@ below follow startup order, so they work top to bottom. | Observability | not started | optional, wants 2–4 GB | | Credentials | shipped defaults are ok | **must all be changed** | | Command | `make up-core` | `make up` | +| Multi mode instead | `NETWORK_ADAPTER_DEPLOYMENT_MODE=multi-adapter make up-core` | `NETWORK_ADAPTER_DEPLOYMENT_MODE=multi-adapter make up` | **Local** is Part 1. **VM** is Part 1, then Part 2 for the differences. @@ -53,14 +62,14 @@ below follow startup order, so they work top to bottom. ## Step 1 — Prerequisites - `git` -- Docker with **Compose v2** — `docker compose version` must work, not `docker-compose` +- Docker with **Compose v2.20 or later** — `docker compose version` must work, not `docker-compose`; the compose file uses `depends_on: required: false`, which older v2 releases reject - `python3` with `cryptography` — `pip install cryptography` - `curl` ## Step 2 — Get the repo ```sh -git clone -b feat/4-docker-compose https://github.com/OpenAgriNet/helmcharts.git +git clone -b development https://github.com/OpenAgriNet/helmcharts.git cd helmcharts/quick-start ``` @@ -76,14 +85,14 @@ cp .env.example .env Locally, nothing in it needs changing. It publishes these on `localhost`: ``` -8081 registry 9200 provider adapter 9100 mock-imd -8080 keycloak 9201 network adapter 9101 mock-agmarknet -9990 keycloak admin 9202 consumer adapter +8081 registry 9201 network adapter (single; network tier in multi) 9100 mock-imd +8080 keycloak 9200 provider adapter (multi only) 9101 mock-agmarknet +9990 keycloak admin 9202 consumer adapter (multi only) 8090 discovery ``` If one is already taken, change it here — that is the only edit a local run -needs. Adapters reach each other by Compose service name, so it only changes +needs. `NETWORK_ADAPTER_PORT` is the network adapter's port in both modes. Adapters reach each other by Compose service name, so it only changes what you type into Postman. ## Step 4 — Start it @@ -93,11 +102,13 @@ make up-core ``` Three tiers, in the only order that works: registry and discovery, then -`bin/setup.py`, then the mocks and the three adapters. Allow up to five minutes +`bin/setup.py`, then the adapter (`network-adapter` on 9201; `NETWORK_ADAPTER_DEPLOYMENT_MODE=multi-adapter make up-core` +starts the three adapters instead). Allow up to five minutes the first time — Keycloak on a cold volume. `setup.py` generates a keypair per adapter, registers seven participants and four -capability bindings, and renders the three adapter configs. Nothing needs +capability bindings, and renders the adapter configs (the three tier configs and +the single adapter's `network-adapter.yaml`). Nothing needs creating by hand. → Appendix C for why the order matters, Appendix D for what it wrote. @@ -107,6 +118,9 @@ make ps ## Step 5 — Provider layer +> Steps 5–7 describe the three tiers. In single mode (the default) all three run +> in `network-adapter`, so use `docker compose logs network-adapter` for each check. + Answers `select`, and the only layer that calls an upstream. Serves all four capabilities from one adapter. @@ -146,7 +160,7 @@ docker compose logs network-adapter | grep 'Server listening' ## Step 7 — Consumer layer The consumer's edge. Sends `discover` to the network layer and `select` -straight to the provider layer, per `config/adapters/routing-consumer.yaml`. +straight to the provider layer, per `config/adapters/multi-adapter/routing-consumer.yaml`. `.env` keys: `CONSUMER_SUBSCRIBER_ID`. @@ -154,19 +168,21 @@ straight to the provider layer, per `config/adapters/routing-consumer.yaml`. docker compose logs consumer-adapter | grep 'Server listening' ``` -`setup.py` renders all three configs from these keys. **Do not edit the +`setup.py` renders all the adapter configs from these keys. **Do not edit the rendered `config/adapters/*.yaml`** — they are regenerated and hold private keys. Change `.env`, re-run `make up`. ## Step 8 — Verify end to end Import both files from `../api-collection/` into Postman — -`OpenAgriNet.api-collection.json` and `OpenAgriNet.local-environment.json`. -Select the environment, or every `{{...-host}}` resolves to nothing. Or: +`OpenAgriNet.api-collection.json` and the environment for your mode: +`OpenAgriNet.network-adapter.local-environment.json` (single, every adapter +host on 9201) or `OpenAgriNet.local-environment.json` (multi). Select the +environment, or every `{{...-host}}` resolves to nothing. Or: ```sh newman run ../api-collection/OpenAgriNet.api-collection.json \ - -e ../api-collection/OpenAgriNet.local-environment.json + -e ../api-collection/OpenAgriNet.network-adapter.local-environment.json # multi: OpenAgriNet.local-environment.json ``` **18 requests, 32 assertions**, one folder per capability plus a health folder. @@ -187,6 +203,48 @@ OAuth2 client credentials that only the adapter reads, at call time. To point it at another deployment, edit the **environment** file, not the collection. → Appendix N. +## Two modes + +The network adapter runs in one of two modes. **Single is the default.** + +| | single (default) | multi | +|---|---|---| +| Start | `make up` | `NETWORK_ADAPTER_DEPLOYMENT_MODE=multi-adapter make up` | +| Containers | `network-adapter` | `consumer-adapter`, `network-adapter`, `provider-adapter` | +| App calls | `127.0.0.1:9201/` | `127.0.0.1:9202/` | +| Catalogue publishes to | `127.0.0.1:9201/publish` | `127.0.0.1:9200/publish` | +| Postman environment | `OpenAgriNet.network-adapter.local-environment.json` | `OpenAgriNet.local-environment.json` | + +Guidelines: + +1. **Set the mode once in `.env`.** `NETWORK_ADAPTER_DEPLOYMENT_MODE` (default + `single-adapter`) picks the config and, through `COMPOSE_PROFILES`, which adapters + start, for `make up` and plain `docker compose up -d` alike. `NETWORK_ADAPTER_DEPLOYMENT_MODE=multi-adapter make up` + overrides it once. +2. **To switch, run the other target.** `make up` after multi removes the + provider and consumer containers; `NETWORK_ADAPTER_DEPLOYMENT_MODE=multi-adapter make up` after single starts them. + `make restart`, `make down`, `make ps` and `make logs` follow the running mode. +3. **Keep the adapter ports on `127.0.0.1`.** Signing is off in every quick-start + config, so `/_internal/` and `/publish` accept unsigned calls. + `server_proxy.conf` denies both on every proxy host. +4. **After switching, re-point the proxy hosts** (Appendix B): single serves + everything from `network-adapter:9201`. + +In single mode, `network-adapter` runs four modules from +`config/adapters/single-adapter/network-adapter.yaml.tmpl`. Each is an exact copy of its tier +template's module; only the module name, path and routing file differ: + +| Module | Path | Routes to | +|---|---|---| +| `consumer-adapter` | `/` | discover to `/_internal/network`, transactions to `/_internal/provider` | +| `network-adapter` | `/_internal/network/` | discovery | +| `oanProvider` | `/_internal/provider/` | answers itself from the upstream APIs | +| `oanProviderPublish` | `/publish` | `/_internal/network/publish` | + +**Upgrading an older stack:** the `network-adapter-layer` service and +`COMPOSE_PROFILES=network` are gone. `make up` cleans up the old container +itself; `NETWORK_ADAPTER_DEPLOYMENT_MODE=single-adapter` in `.env` is optional. + --- # Part 2 — Run it on a VM @@ -198,7 +256,7 @@ Four differences. **V1 comes before Part 1 Step 2**, because it installs `git`. Nothing is cloned yet, so fetch the script rather than running it from the repo: ```sh -curl -fsSL https://raw.githubusercontent.com/OpenAgriNet/helmcharts/feat/4-docker-compose/quick-start/bin/bootstrap-ubuntu.sh | bash +curl -fsSL https://raw.githubusercontent.com/OpenAgriNet/helmcharts/development/quick-start/bin/bootstrap-ubuntu.sh | bash ``` Installs `git`, `make`, `python3-cryptography` and Docker from Docker's own apt @@ -230,7 +288,10 @@ make up `make up`, not `make up-core`: two more tiers on top of Part 1's three — nginx-proxy-manager on 80 and 443, and HyperDX. **This is the step that makes -the VM reachable from the internet.** +the VM reachable from the internet.** `NETWORK_ADAPTER_DEPLOYMENT_MODE=multi-adapter make up` runs the three adapters +instead; the mode decides what the proxy hosts in Appendix B forward to. +Switching mode on a running VM restarts nginx-proxy-manager, so the public +hosts drop connections for a few seconds. ## Step V4 — Decide what is exposed @@ -249,7 +310,7 @@ ssh -L 9202:127.0.0.1:9202 -L 9200:127.0.0.1:9200 \ ``` The collection's defaults then work unchanged, since they already point at -loopback. +loopback. Single mode needs only 9201 for the adapter; 9200 and 9202 are for multi. ## Step V6 — Observability (optional) @@ -312,7 +373,8 @@ Reference, read on demand. Nothing here is a step. ## Appendix A — What is here, and what is not Running: **registry** (SunbirdRC + Postgres + Keycloak), **discovery** -(catalogue search + Postgres), **three adapters** (same image, three configs), +(catalogue search + Postgres), **the adapters** (one Network Adapter by +default, or three tier adapters with `NETWORK_ADAPTER_DEPLOYMENT_MODE=multi-adapter make up`; same image), **two mock upstreams** standing in for Mausamgram and Agmarknet. Behind profiles: **nginx-proxy-manager** (`reverse-proxy`, the only container on a routable interface) and **hyperdx** (`observability`, ClickStack — the @@ -361,6 +423,16 @@ Three hosts rather than one with path prefixes, so a rate limit or a block attaches to a hostname instead of a regex in a textarea, and each gets its own certificate. An unknown `Host` gets NPM's default page, not an adapter. +**Single mode (`make up`, the default).** The table above is for +`NETWORK_ADAPTER_DEPLOYMENT_MODE=multi-adapter make up`. With the single adapter there is no `consumer-adapter`, +`network-adapter` or `provider-adapter` to forward to: point the consumer host +at `network-adapter`, port 9201, with the same Advanced paste. The +app's paths (`/discover`, `/select`, …) do not change, and neither does +`/publish`: both modes present the same URLs. `server_proxy.conf` denies +`/publish` and `/_internal` on every host: `/_internal/` holds the network and +provider tiers, `/publish` publishes as the provider without checking the +caller, and signing is off in quick-start. + **A fourth host for HyperDX**, if the `observability` profile is up — this is what replaces `ssh -L 8085` for reading telemetry: @@ -439,21 +511,24 @@ request. It is also the only option for a wildcard. Renewal reuses whichever method you chose, so a temporarily-correct DNS record or SG rule fails silently in sixty days. -### `POST /publish` returns 403 on all three hosts +### `POST /publish` and `/_internal` return 403 on every host Not optional hardening. The provider adapter's module at `/` verifies the -sender's signature; `oanProviderPublish`, on the exact path `/publish`, has +sender's signature; the publish module (`oanProviderPublish` in multi mode, +`oanProviderPublish` in single mode), on the exact path `/publish`, has **no signature check at all**, because its intended caller is the provider's own catalogue system inside the trust boundary. A proxy host pointed at -`provider-adapter:9200` therefore exposes `/publish` to anyone. +`provider-adapter:9200` (multi) or `network-adapter:9201` (single) therefore +exposes `/publish` to anyone. In single mode the network and provider +tiers also sit under `/_internal/`, which only the adapter itself calls. NPM's UI cannot route a host while withholding one path, so the block lives in `config/reverse-proxy/npm-custom/server_proxy.conf`, which NPM includes in **every** proxy host's server block — a mounted file, not a click, so not -something to remember on one host out of three. +something to remember on one host. To let a real catalogue system publish, give it a tunnel or put it in the VPC -and let it reach `provider-adapter:9200` directly. Do not turn that `deny` into +and let it reach the adapter directly (9200 in multi, 9201 in single). Do not turn that `deny` into an `allow`. ### Which nginx config loads itself, and which is a paste job @@ -461,7 +536,7 @@ an `allow`. | File | How it applies | |---|---| | `npm-custom/http_top.conf` | **Automatic**, top of the `http` block. The `consumer` rate-limit zone and `limit_req_status 429`. | -| `npm-custom/server_proxy.conf` | **Automatic**, every server block. The `/publish` deny. | +| `npm-custom/server_proxy.conf` | **Automatic**, every server block. The `/publish` and `/_internal` denies. | | `npm-advanced/consumer.conf` | **Manual** — paste into the consumer host's Advanced tab. `limit_req` for that host only; a 10 r/s ceiling on signed peer traffic would throttle for no gain. | | `npm-advanced/hyperdx.conf` | **Manual** — paste into the HyperDX host's Advanced tab. 300s timeouts for ClickHouse scans, buffering off for live tail. Carries no credential; the Access List does. | @@ -513,8 +588,8 @@ loopback. A registry route would depend on that silently. ``` 1. registry and discovery (also sunbird-registry-postgres, sunbird-registry-keycloak, discovery-postgres) -2. bin/setup.py keys, seven participants, four bindings, three configs -3. mock upstreams, then the three adapters +2. bin/setup.py keys, seven participants, four bindings, four configs +3. the adapters network-adapter (default), or the three with NETWORK_ADAPTER_DEPLOYMENT_MODE=multi-adapter 4. nginx-proxy-manager the public edge — 80 and 443, all interfaces 5. hyperdx ClickStack ``` @@ -583,7 +658,7 @@ unedited. Those four strings are the hinge. The provider adapter builds the same key from each payload — the provider id and the capability `@type` it carries — and a step answers only when the key matches its own. `setup.py` renders those keys -into `provider.yaml` from the same `.env` it seeds the registry from, which is +into `provider.yaml` and `network-adapter.yaml` from the same `.env` it seeds the registry from, which is what stops the two drifting. **Looking at it** — from the VM or a tunnel. Search takes no token; writes do, @@ -634,11 +709,12 @@ the routing config. The schema validator is the exception: it reads `context.action` from the body and ignores the path. Nothing reconciles the two, though a mismatch usually fails validation anyway. -Publishing enters at the **provider** adapter, which signs and forwards: +Publishing enters at the **provider** (port 9201 in single mode, 9200 in multi), +which signs and forwards: ```sh -curl -s -X POST http://127.0.0.1:9200/publish \ - -H 'Content-Type: application/json' -d @your-catalog.json +curl -s -X POST http://127.0.0.1:9201/publish \ + -H 'Content-Type: application/json' -d @your-catalog.json # multi: :9200 ``` Three things follow from that: @@ -730,7 +806,7 @@ an id is a `.so` basename. If `ADAPTER_IMAGE` names a **new tag**, set it before nothing in `stack.sh` pulls, so the stack quietly comes back on the old image: ```sh -docker compose pull provider-adapter network-adapter consumer-adapter +docker compose --profile multi pull provider-adapter network-adapter consumer-adapter ``` Build from the adapter repo at the commit the config expects, and check the @@ -751,6 +827,10 @@ publish reseeds first; `updateMode: MERGE` updates in place. **Then check, cheapest first:** ```sh +# single mode (the default) +docker logs network-adapter 2>&1 | grep 'Processor steps initialized' +docker logs network-adapter 2>&1 | grep -iE '"level":"(error|fatal)"' +# multi mode docker compose logs provider-adapter | grep 'Processor steps initialized' docker compose logs provider-adapter | grep -iE '"level":"(error|fatal)"' make ps @@ -1003,7 +1083,7 @@ config/ reverse-proxy/ npm-custom/ mounted to /data/nginx/custom; NPM includes these http_top.conf on its own -- rate-limit zone, and the /publish - server_proxy.conf deny every proxy host gets + server_proxy.conf and /_internal denies every proxy host gets npm-advanced/ NOT loaded. Paste into a host's Advanced tab; kept consumer.conf here because a textarea in a database is not hyperdx.conf reviewable. consumer = rate limit; hyperdx = ClickHouse @@ -1013,12 +1093,14 @@ config/ remote config. Reads every container's json log and names the service from the compose label adapters/ - consumer.yaml.tmpl templates. setup.py renders these to .yaml, - network.yaml.tmpl filling in the keys it generated. The rendered - provider.yaml.tmpl files hold private keys and are gitignored - routing-consumer.yaml which action goes where: consumer sends discover - routing-network.yaml to the network layer and select to the provider; - routing-provider.yaml provider sends publish to the network layer + multi-adapter/ multi mode: consumer, network and provider + *.yaml.tmpl templates. setup.py renders each to .yaml beside + it; rendered files hold private keys, gitignored + routing-*.yaml which action goes where (consumer, network, provider) + single-adapter/ single mode: all three tiers in one adapter + network-adapter.yaml.tmpl + routing-network-adapter-*.yaml tier hops to localhost:9201/_internal/... + (the network module reuses multi-adapter/routing-network.yaml) registry/ schemas/ Participant, ProviderSchema, SchemaRegistry. Read at startup -- a change needs a restart @@ -1059,8 +1141,8 @@ that volume — and equally does not survive if you skip it. ```sh docker compose down -v -rm -rf keys config/adapters/consumer.yaml config/adapters/network.yaml \ - config/adapters/provider.yaml +rm -rf keys config/adapters/multi-adapter/{consumer,network,provider}-adapter.yaml \ + config/adapters/single-adapter/network-adapter.yaml ``` New keys mean new identities, so the provider rows must be created again and