feat: add the quick-start docker compose deployment - #5
Merged
Conversation
…/network-adapter#4] One compose file brings up the registry, the discovery service and the three adapters, with every config under config/. Intended for trying the network end to end on one machine, not for production: the adapter keys live in a config file and nothing runs TLS. Two things are deliberately absent, because they are the user's own: the provider API run it locally and expose it with ngrok. No config here names it -- the provider adapter reads its base URL from the registry at request time, so moving to a new tunnel is a registry edit and nothing more. its registry rows the Participant and ProviderSchema are created by hand, since the base URL is that tunnel. bin/setup.py does only what cannot be done by hand: it generates the three adapter keypairs, registers the three adapter identities, and renders the adapter configs with the key osid the registry assigned. Those three rows have to exist before any adapter can sign or verify anything, and the osid is only knowable after the write. The images build from remote git contexts, so no sibling checkout is needed. ADAPTER_SRC points at a feature branch because the three OAN plugins are not on the adapter's default branch yet. config/mappings/ carries the published mapping to read and to fork, and is not served from here. The registry holds a full URL and the adapter fetches it verbatim, so a mapping has to be published before it can be tested -- serving the local copy would prove the file works and prove nothing about the file anyone else reads. No Makefile: the four commands are in the README.
…ork-adapter#4] The folder read as a laptop sandbox. It is a shared dev environment on a VM, and the difference matters in three places. Ports now bind to 127.0.0.1 rather than every interface, overridable with BIND_ADDR. Behind them sit a Keycloak whose admin password ships as "admin" and a registry whose write token any reader of .env.example can mint, so the old default published the whole network's identity records, writable, to whatever the VM was reachable from. The README documents the ssh -L tunnel and says what has to be true before BIND_ADDR is widened. .env.example no longer claims nothing in it is secret. Every credential in it is a shipped default, which is the same as public, and it says so. The three adapter identities are now *.oan.dev rather than *.oan.local, and the discovery network id oan-dev rather than local-network. The registry is append-only and an id can never be reused, so a misleading name is permanent. Also drops the four identity fields from the response context in the shipped mapping, matching the adapter's own copy: bapId, bapUri, bppId and bppUri. A mapping transforms a payload and has no business asserting network identity, and the two Uri fields it copied were whatever the caller sent -- which in a deployed stack is a container-internal address that means nothing outside it. Identity on an answer is the signature the adapter puts on it.
…iNet/network-adapter#4] Registry schema, four changes: role consumer, provider and network replace BAP, BPP and NETWORK. A role now says what a party does rather than which Beckn acronym it is. Nothing in the adapter compared the old values, so this costs a re-seed and no code. keyId gone. Nothing could look one up: the registry assigns an osid on write, and that osid is what a sender names in the Authorization header, so the friendly id was decoration that read like an identifier. use gone. alg already carried the purpose -- ed25519 signs, x25519 encrypts -- and the adapter treats a missing use as "may sign". The two conditionals that derived alg from use went with it: with use absent each `if` matched vacuously, so both `then` branches applied and alg had to be two curves at once, which refused every key. Found by seeding against the new schema. key bare base64, no "base64:" label. What a verifier hands to a decoder is now the value as published. Auth is removed entirely, along with the paramName, secret and materialRef definitions it used and the privateFields entry that redacted it. The adapter never read it -- it presents credentials from its own config, naming environment variables -- so it described a mechanism that did not exist. Its plaintext-http guard goes too, having guarded a field nothing consumed; if that rule is wanted it belongs where the credential actually lives. Compose no longer builds anything. ADAPTER_IMAGE and DISCOVERY_IMAGE name tags to pull and have no working default, so an unset value fails on pull naming the tag it tried rather than quietly running something else. Verified against a registry running this schema: the three adapter rows seed with the new roles and bare keys, and role "BAP", a prefixed key, a key carrying keyId or use, and an upstream carrying auth are each refused.
…er [OpenAgriNet/network-adapter#4] publish now enters at the provider adapter and reaches the discovery service by way of the network layer, which is what fronts discovery for discover already. select is untouched and still answered at the provider adapter without going near it. It needed more than one routing file, and the reason is worth recording. The routing step fails any request whose action is absent from its config, so it cannot be added to the provider adapter's existing /beckn/ module: that module would then have to list select as well, and listing select would proxy it to the network layer instead of answering it locally, which is the only thing that module does. So publishing gets its own module and its own routing config, mounted at /catalog/ and reached as POST /catalog/publish. The catalogue body carries no bapId or bppId, and needs none. The provider adapter signs the forwarded request as itself and the identity travels in the Authorization header's keyId, taken from its keyManager config; the network layer verifies that signature, and its identity check skips a body that declares no caller rather than demanding one. Verified against the local stack: POST /catalog/publish answers ACCEPTED with the catalogue's stats, and select and discover both still work -- select answered locally with its per-day resources, discover returning catalogs.
…nAgriNet/network-adapter#4] The mount is /oan/ rather than /beckn/, on all three adapters, and the /catalog/ module added for publishing is gone. One subtree per adapter, and the payload's action says which action it is. Dropping that module moved where publishing enters. It cannot enter at the provider adapter any more: the routing step fails an action missing from its config, so routing publish from the module that answers select would mean listing select too, and listing select would proxy it away instead of answering it there. A second mount was what bought both, and a second mount is what has been removed. So publish is a routing entry on the experience adapter instead, which already exists to take a call and forward it. Two entry points now: POST :9202/oan/publish through the experience adapter, which takes unsigned calls from inside the trust boundary and signs on the way out -- so a plain curl works POST :9201/oan/publish straight at the network adapter, for a publisher that signs for itself. Unsigned it answers 401 AUT_SIGNATURE_MISSING. Either way the network adapter verifies the signature and forwards to the discovery service, and the catalogue body still needs no bapId or bppId -- identity travels in the Authorization header's keyId. Config only; no adapter code was touched. Verified on the local stack, all on /oan: select answers on_select with its per-day resources, discover returns catalogs, publish answers ACCEPTED, and an unsigned publish direct to the network adapter is refused 401.
…AgriNet/network-adapter#4] The provider's own catalogue system posts POST /publish to the provider adapter. That adapter signs the request as itself and forwards it to the network layer, which verifies the signature and hands it to the discovery service. The caller signs nothing. It is a second module on that adapter, and it has to be: the routing step fails any action missing from its config, so routing publish from the module that answers select would mean listing select too -- and listing select would proxy it to the network layer instead of answering it there. The new module sits at the root rather than under /oan/, and that split is the point. /oan/ is the adapter's Beckn surface -- the path the registry publishes as its baseUrl -- so a peer calling <baseUrl>/select has to reach the module that answers select. Publishing is not part of that surface; it arrives from inside the provider's own deployment. Go's mux takes the longest matching pattern, so /oan/... still reaches the capability module and only /publish falls through to the root one. No prefix is invented for either. publish is no longer a routing entry on the experience adapter: it enters at the provider now, so exp is back to discover and the transactional actions. Config only; no adapter code was touched. Verified on the local stack: POST /publish answers ACCEPTED with the catalogue's stats, select still answers on_select with its per-day resources, discover still returns catalogs, and an unsigned POST /oan/select at the provider adapter is refused 401 -- which is what proves the two mounts are resolving to the right modules rather than the root one swallowing both.
OpenAgriNet/network-adapter#4] bapId, bppId, bapUri and bppUri are gone from every payload -- the example select and discover bodies, the catalogue body, and the prose that described them as what goes on the wire. Identity travels in the Authorization header's keyId, which names the signer and the key the registry published for it, and a body that declares no caller skips the declared-identity comparison rather than failing it. The two *Uri fields were container-internal addresses that meant nothing outside the compose network in any case. Catalogue-level bppId and bppUri go too. The discovery service never names them -- they ride through in the stored document -- so publishing without them is accepted and discover still returns the catalogue. ADAPTER_IMAGE is ghcr.io/nisargabd/oan-adapter:latest. Verified pullable anonymously, and verified to carry the three plugins these configs name: oanregistry.so, jsonmapper.so and weather.so, alongside router, signer, signvalidator and simplekeymanager. DISCOVERY_IMAGE is still unset -- no tag has been shared for it yet. Verified on the local stack with no party named anywhere: publish answers ACCEPTED, select answers on_select with its per-day resources and a context carrying only correlation ids, and discover returns the catalogue.
…/network-adapter#4] Every adapter config declares schemav2validator, pointed at the LTS spec tag, with the extended layer off: it fetches each resource's own @context and validates against that, which is a network call per payload and a second failure mode, and nothing here needs it. The extendedSchema_* settings are present but inert until it is switched on. The validateSchema step is wired only where every action the module handles is defined in that spec: exp /oan/ discover, select, init, confirm, status -- validated provider /oan/ select -- validated network /oan/ carries publish -- declared, not wired provider / carries publish -- declared, not wired publish is absent from the spec, and the validator refuses an action it cannot find rather than passing it through, so wiring the step on either of those modules would reject every catalogue with "unsupported action: publish". Validating it needs an auxiliary spec that defines the action -- auxiliaryTypes and auxiliaryLocations, which are additive and must not overlap the primary spec. Turning validation on surfaced a defect in the spec itself, and the example payloads now carry a quantity because of it: Commitment.resources requires ["id", "quantity"] while Resource defines no quantity property and the spec has no Quantity schema anywhere. additionalProperties is unset, so any value satisfies it. Without one every select is refused SCH_REQUIRED_FIELD_MISSING. That is upstream, not a choice made here, and the README says so where someone writing a payload will read it. Verified on the local stack: select, discover and publish all answer 200 with validation on; a select missing quantity is refused 400 SCH_REQUIRED_FIELD_MISSING naming the JSON path, an unknown action is refused 400 "unsupported action", and extendedSchema_enabled reads "false" inside the running container.
…AgriNet/network-adapter#4] DISCOVERY_IMAGE is ghcr.io/nisargabd/discovery-service:${TAG:-latest}, with TAG pinning a known build and unset meaning latest. Compose interpolates a .env value, so both resolve: verified :latest with TAG unset and :v0.3.1 with TAG set. With both images published the stack was runnable for the first time, and running it end to end found two things. THE DOCUMENTED BRING-UP WAS BROKEN. Step one was a bare `docker compose up -d`, which starts the adapters too -- and an adapter config is a bind-mounted FILE that step two has not written yet, so Docker created a DIRECTORY at each of those paths. The adapters then died on "adapter.yaml: is a directory", the later `up` did not recreate them so they stayed dead, and the directories sat where setup.py needed to write files. Step one now names registry and discovery explicitly, the compose header and README say why, and setup.py refuses with an explanation and the commands to recover if it finds a directory where a config belongs. THE PARTICIPANT SCHEMA HAD BEEN REVERTED. Porting a description reword in 20ccc9d copied oan-local's copy over this one, which still carries the old contract, undoing 37f02c6: the role enum was back to BAP/BPP/NETWORK, keyId and use were required again, keys wanted the base64: label and auth was back. Re-applied, with the reworded descriptions kept. oan-local is deliberately still on the old contract, so it is not a source to copy this file from. Verified from an empty volume, following the README exactly, on the published images: the three adapter rows seed, all eight services come up, the two provider rows register, and then select answers on_select with three per-day resources and no dangling offer references, publish answers ACCEPTED, and discover returns the catalogue.
…Net/network-adapter#4] The README, .env.example and setup.py all said a mismatch comes back as a bare {"status":"ACK"} with no on_select, looking as though nothing happened. That is wrong, and it sent a reader looking for a silent failure that does not occur. Both cases were run against the stack to see what they really answer: the payload names a provider the adapter is not configured for 404 NET_ENTITY_NOT_FOUND, "this module serves no capability matching the request". The step passes through and nothing behind it answers, so the refusal is explicit and names the cause. the adapter is configured for the key but the registry has no matching ProviderSchema row 502 with an EMPTY body. This is the one worth documenting: nothing in the response says why, and "no call plan for <key>" appears only in the provider adapter's log. Both are now described with the command that surfaces the second, and the consequence of a single configured key is stated where it will be read: while the adapter carries one, onboarding a second provider is an edit to .env, a re-run of setup.py and a restart -- a registry entry alone is not enough. Whether the adapter should instead match on capability, and serve any provider the registry lists for it, is a separate question and deliberately still open.
…iNet/network-adapter#4] The troubleshooting entry it names was renamed in the previous commit, so the pointer described a symptom that no longer appears anywhere.
Establishes the repo-level scaffolding the charts depend on: naming and structure conventions, a lint script that rebuilds file:// dependencies before linting, and a CI workflow that runs it. .gitignore excludes Helm dependency artifacts, which are regenerated rather than committed.
oan-common holds the shared helpers every OAN chart renders through - names, labels, image references, resources, and security contexts - so the service charts stay declarative. Resources are deliberately required rather than defaulted: an unset value fails the render instead of shipping an unbounded pod. oan-template is the copy-from starting point for a new service chart.
postgresql-cnpg renders one CloudNativePG Cluster per release, with optional ScheduledBackup and Barman Cloud ObjectStore. Neither chart renders a password: every credential is a reference to a Secret, and the render fails when one is unset rather than defaulting. Applications connect as the owner of their own database, never as postgres. Extensions that require superuser are created once through bootstrap.postInitApplicationSQL, which the operator runs during bootstrap, so no long-lived role holds the privilege. postgresql-migration runs Flyway as a Job across the databases the cluster chart created. Both target directories carry no SQL yet - Sunbird RC and Keycloak each manage their own schema - so the Job currently skips them and the targets exist as a versioned home.
Imports the sunbird-rc realm from a ConfigMap on first start, with the realm content checksummed into the pod annotations so a realm change rolls the pod. An init container waits for the database, standing in for compose's depends_on: condition: service_healthy. Keycloak connects as the owner of its own keycloak database rather than sharing the registry's database as postgres, which is how the compose stack runs it. The realm JSON is Sunbird RC's export and still carries its upstream defaults: a placeholder admin-api client secret, an enabled placeholder user with a known password, and a wildcard redirect URI on the public frontend client. All three need hardening before this reaches any environment that is not local. See the chart README for the client secret step; the other two are tracked as follow-up.
registry runs Sunbird RC with its Participant schema mounted from a ConfigMap, and connects as the owner of the registry database rather than as postgres. discovery renders the Beckn discovery service, taking its whole DSN from the CNPG-generated Secret so no password is assembled or escaped in the chart. It enables readOnlyRootFilesystem, which the other service charts cannot yet. The Beckn spec is fetched by URL with a cache fallback, and the render fails when neither a URL nor an existing ConfigMap is set, since the service refuses to boot without the document. Both charts fail the render on an unset credential rather than defaulting one.
Records the chart inventory and the order releases have to go out in, since the database cluster has to exist before the migration Job and the services that connect to it.
…n upstream [OpenAgriNet/network-adapter#4] MAPPING_URL pointed at a personal fork. It now points at this repo's own copy of the file, the one already sitting in config/mappings/, served over the raw CDN -- so the file a reviewer reads and the file the adapter fetches are one file and cannot drift. Verified fetchable anonymously, and it is in fact the current mapping: the fork still serves the older one that echoed bapId and bppId back in the response context. The URL carries a branch, which is noted in both .env.example and the README: repoint it at the default branch on merge, or pin a tag, so a deployment is not following a moving file. The Participant schema no longer forbids role and keys on a type "upstream". Nothing reads them there -- a signature is verified against the node identity that signed it, never against an upstream -- but forbidding them refused a record a deployment might legitimately want to keep, and the rule read as though an upstream were prevented from ever signing. The conditional is deleted rather than emptied: an if/then with an empty then reads like a rule and enforces nothing. The node conditional is untouched and still requires role and keys, which is the half that matters. Verified against a stack on the published images: an upstream with role and keys is now accepted, one without still is, and a node without them is still refused. The full Postman flow passes 20 assertions with the adapter fetching the mapping from this repo -- confirmed in its log alongside the signature it verified, exp.oan.dev's, resolved from the registry.
…enAgriNet/network-adapter#4] The mapping this deployment serves produced an answer the pinned LTS spec refuses, which matters more here than anywhere: MAPPING_URL points at this copy, so this is the file the adapter fetches. status.descriptor.code was QUOTED; the spec's enum is DRAFT, ACTIVE and CLOSED. DRAFT is also the honest value -- a quote is a draft commitment, since nothing is committed until init and confirm. each resource lacked quantity, which Commitment.resources requires while the spec defines no quantity property and no Quantity schema. The defect is upstream; the consequence was ours, since an answer without it fails validation for any consumer who validates. Carried over verbatim from the adapter's reference copy, where the same fix is committed with its test.
…ter#4] Anyone cloning this branch got the stack but not a way to exercise it beyond copying curl out of the README. The collection under postman-collection/ runs the whole thing: a write token, the provider's two registry rows, both registry searches, publish, discover and select. Every value is prefilled in the collection's own variables rather than in a separate environment file, so the file is useful to whoever receives it with no second import. That includes providerId and capability, set to the values .env.example uses -- which matters, because those two form the binding key the provider adapter answers to, and a row naming anything else is refused 404 with the response naming the module rather than the cause. Typing the id by hand is how that happens. One variable is left to set: upstreamBaseUrl, which the deployment cannot know. The requests carry assertions, so a run reports whether the stack is healthy rather than merely returning 200s: that the three adapter identities exist with signing keys, that the provider is registered as an upstream with no role or keys, that publish is ACCEPTED, that select answers a resource per forecast day whose status is in the spec's enum and whose every resource carries a quantity, and that no party is named in the answer. Every payload in it validates against the pinned Beckn v2 LTS spec and, for resourceAttributes, against openagrinet:WeatherObservation v0.1. It carries no credential: the token variable ships empty and is filled by request 1.
Splits the stack across two networks. Nginx Proxy Manager sits only on oan-edge and is the one container publishing on a routable interface; everything else binds 127.0.0.1 and is reached over an SSH tunnel. NPM's Forward Hostname is a free-text field, so the network split is what bounds the blast radius of a wrong entry -- names on oan-internal do not resolve from there. Adds HyperDX behind an observability profile, and the version-controlled nginx that NPM includes on its own: a rate-limit zone for the unsigned experience layer, and a global deny on /publish, which the provider adapter mounts with no signature validation. The npm-custom mount is deliberately not :ro. NPM's s6 init rewrites and chowns everything under /data/nginx, so a read-only mount makes the prepare service exit 1 and nginx never starts -- while the container still reports Up, because s6 itself is alive. Also ignores .env.* so a `cp .env .env.bak` before an edit cannot be committed; the existing pattern was an exact match on .env only.
`make up` runs the five steps in the order they have to run in. The ordering is load-bearing rather than tidy: an adapter's config is a bind-mounted FILE that setup.py renders, and Docker creates a DIRECTORY at any bind-mount source that does not exist yet -- so starting an adapter first both wedges that container and leaves a directory where setup.py needs to write a file. Preflight checks .env, compose v2, python3 and the cryptography import before anything starts, because keycloak's healthcheck allows five minutes and a missing import would otherwise surface after that wait. down keeps volumes; destroy deletes them and requires the word typed, since npm-data is the only copy of every proxy host and certificate. up-core skips the gateway and hyperdx for a box that has neither a public port nor 16 GB.
Installs docker from docker's own apt repository rather than the docker.io package or the snap -- the snap runs confined, and bind-mounting a file out of a home directory, which every adapter config does, fails under it in ways that read as file-not-found. Takes cryptography from apt rather than pip: Ubuntu 24.04 marks the system python externally-managed, so pip refuses without --break-system-packages, and setup.py needs that import. Checks RAM and disk up front. An undersized box fails by having the OOM killer take out a Postgres mid-write, which surfaces as corruption rather than as an out-of-memory error.
Two mechanisms, split by whether the name is reachable from the internet. Let's Encrypt cannot certify a loopback name -- 127.0.0.1.sslip.io resolves correctly, to 127.0.0.1, which is exactly why their validators cannot reach it -- so local work needs mkcert and a private CA. Includes the failure table, because every symptom in this area points away from the cause: a CA present in the keychain but carrying no trust setting verifies fine under `curl --cacert` and fails in every browser, and a host with no certificate attached fails as an SNI alert rather than as anything mentioning certificates.
…etwork-adapter#4] If the provider answered without its location echo, JSONata dropped the undefined values inside the array and this emitted "coordinates": [] -- an invalid GeoJSON Point, which the adapter then signed and delivered. Nothing downstream could detect it: the payload was well formed and spec-shaped, just describing a point that does not exist. Absent is honest; empty is a lie in the shape of an answer. This deployment serves its own copy of the mapping, deliberately, so the file a reviewer reads is the file the adapter fetches -- which also means it does not pick the fix up from anywhere else. Brought level with the adapter's reference copy, where it was fixed in review of network-adapter PR #2. Mapping only. The other findings from that review are in Go and reach this deployment when the adapter image is next published.
busybox resolves localhost to ::1 first and this image binds v4 only, so the check failed with "Connection refused" against [::1]:8080 while the UI on 8085 served 200 throughout. A container reporting unhealthy while working sends you to ClickHouse, which is the one place the problem was not. Also widens the window. ClickHouse creates its system tables on a cold volume, which takes minutes rather than the 80 seconds the previous retries/start_period pair allowed, and Docker does not re-evaluate once a container has failed past retries -- so a window that is merely tight leaves a label that is permanently wrong.
…er#4] A select had nothing to answer it out of the box. Whoever brought the stack up had to run a provider API themselves and expose it through a tunnel, which made the first end-to-end run depend on the one piece that was not here. Both mocks are in the compose file now, on oan-internal and published on loopback only for looking at directly while debugging. The provider adapter reaches them by service name, so the registry rows point at http://mockimd:9100 and http://mockagmarknet:9101 rather than at anything that has to exist outside. Sources are in mocks/, byte-identical to the ones the oan-local stack has been tested against, and they are pulled as published images like everything else here -- nothing in this stack builds. mocks/README.md carries the build commands and, more usefully, what each one deliberately gets wrong: mockagmarknet answers 401 without its token, sends prices as strings with Title-Case spaced keys, and leaves the minimum and maximum off its last record. Those are the cases the mappings exist to handle, so a mock that answered cleanly would test nothing. MANDI_TOKEN reaches the provider adapter as an environment variable and the mock as a flag, from the same .env value. The adapter config names the variable, never the value.
…network-adapter#4] setup.py registered the three adapter identities and left the provider's two rows to be created by hand, on the reasoning that the upstream's base URL belonged to whoever ran it. That no longer holds twice over: the upstreams are in this stack now, and the registry is not reachable from outside it, so "by hand" had no way to happen. It seeds all of it -- five participants and two capability bindings. Three node identities with the keypairs it generates, two upstreams addressed by compose service name, and a ProviderSchema row per capability carrying the method, path, timeouts and mapping URL. Both new helpers search before they write, so a re-run converges rather than failing, and keys/keys.json is reused rather than regenerated. The binding keys are the part worth being careful about. A provider step answers only when the key it was configured with matches the one built from the incoming payload, and the two are now rendered and seeded from the same .env values in the same run -- which is what keeps them from disagreeing. Changing a provider is an .env edit and a re-run. Adds the mandi capability to the provider template alongside weather, lifted from the config the oan-local stack has been tested against. Both ids are in providerSteps and both are in steps:, because a step that is declared but missing from steps: never runs and the request falls through to a 404 that does not say why. Also drops BIND_ADDR from .env.example. Nothing read it -- the loopback binds are written literally in the compose file, deliberately, since one variable that moves every port onto a public interface at once is a footgun.
…adapter#4] The registry was on oan-edge so NPM could resolve it, with a documented route, an allowlist to paste into the host's Advanced tab and an Access List to attach. The decision is the other way now: it is on oan-internal only, so a proxy host pointed at it fails to resolve rather than quietly working. The argument against the route is that it cannot be reduced to reads. SunbirdRC uses POST for both -- /Participant/search reads, /Participant creates -- so no method rule separates them and a host forwards the whole API. What kept writes out was never the route: it was that nothing outside the VM can mint a Keycloak token, because Keycloak publishes on 127.0.0.1. That is a decision made elsewhere in this file, and a registry route would have depended on it silently -- publish Keycloak later for an unrelated reason and the write surface opens with it, with nothing in the route changing to say so. So the allowlist goes with it. It was a real defence, but a defence for a host that should not exist is worse than no host: it makes the route look considered. Reaching the registry is now an SSH tunnel, and everything that has to write to it -- which is all of setup.py -- runs on the VM.
…OpenAgriNet/network-adapter#4] The README described a flow that no longer exists: compose up, then create the provider's rows by hand, with an ngrok URL for an API you brought yourself. It is `cp .env.example .env && make up` now, and nothing has to be created afterwards. "Register the provider" becomes "What is in the registry, and why you did not create it" -- what the five rows are, what a binding key is for, and how to repoint a capability at a real API. Routing the registry is replaced by why it is the one service not to route, with discovery as the worked example instead. The troubleshooting section leads with the two binding-key failures and which 404 means which side is wrong. The Postman collection loses all five registry requests. Not a trim: the registry has no route, so none of them could work from outside the VM, and setup.py does what they did. Six requests remain -- publish, discover and select for each capability -- 32 assertions, and nothing left to fill in, since the two provider ids are prefilled with what the stack deploys. The mandi select's networkId was still oan-local's value; it is a variable now. Two corrections while checking claims rather than copying them: - The old text said publish is not validated because the spec does not define it. The spec does define /catalog/publish, the validator indexes it under the action these payloads send, and both publish bodies validate against it with no errors. It is off because validateSchema is absent from those two modules' steps: -- a choice pending a test, not a limitation. - A binding key with no matching ProviderSchema row was documented as a 502 with an empty body. It has been a 404 naming the binding since the adapter fix; the 502 text predated it.
OpenAgriNet/network-adapter#4] The previous commit said the migration was in README under "Renaming this directory". It was not. It is now. Compose derives its project name from the directory holding the compose file, so renaming to quick-start renames all five volumes with it and Docker moves no data. A plain `make up` after the pull starts on empty ones: empty registry, empty catalogue, and an NPM with no proxy hosts or certificates. npm-letsencrypt is the one that hurts. Re-issuing runs into Let's Encrypt's five-per-week duplicate limit, so losing it can mean days without certificates. The section gives the copy-across, tested here on scratch volumes including a nested path before writing it down, and says to verify the registry and the proxy hosts before deleting the old copies. Also notes Keycloak's realm lives in registry-data, so it travels with that one volume and equally does not survive skipping it. Two stale references fixed while here: the collection is a sibling now rather than a subdirectory, and it was still described as six requests and 32 assertions -- it is nineteen and 50.
…penAgriNet/network-adapter#4] registry/ is local scratch -- a standalone registry someone ran by hand -- and its .env holds live credentials. It was never part of this repo. It got in because the rename commit used `git add -A` instead of naming paths, which swept up an untracked directory. The Permission denied warning on registry/db-data/ in that commit's output was the tell. Removed from tracking and added to a root .gitignore so the same command cannot do it again. REMOVING IT HERE DOES NOT UNDO IT. The file is still in 3d87de3, which is pushed, so POSTGRES_PASSWORD, KEYCLOAK_SECRET and REGISTRY_DEFAULT_USER_PASSWORD should be treated as disclosed and rotated. Two of the fields (KEYCLOAK_ADMIN_USER, KEYCLOAK_ADMIN_PASSWORD) match defaults already published in quick-start/.env.example, so those are no change. Purging it from history needs a rewrite and a force-push on a shared branch, which is not mine to do unilaterally -- rotation is the reliable fix regardless.
…AgriNet/network-adapter#4] Back to what this collection is for: the flows a consumer drives. Two folders, Weather and Mandi, six requests, 32 assertions. The registry requests never belonged in a shared collection. The registry has no route through the gateway and publishes on loopback only, so they only ever worked from the VM or through a tunnel -- and bin/setup.py already seeds every row from the same .env the adapter configs are rendered from, which is what keeps the two from disagreeing. Anyone who does need to poke a row has the curl in quick-start's README. Removing them orphaned 29 variables -- the Keycloak and registry URLs and credentials, the node ids and keys, the binding keys, paths and mapping URLs. All dropped, from the collection and the environment file, so neither ships fields nothing reads. Six remain: the three adapter URLs, the two provider ids and networkId. networkAdapterUrl is kept although no request uses it. It is the other adapter a deployment exposes, and both its /publish and /discover verify signatures, so a network peer calls it directly -- Postman does not sign, so that call is not scripted, but the address is worth having in one place. The signing-key section moved to quick-start's README rather than being deleted with the requests it described. It is about what a registry row may contain -- that an upstream may hold keys, and why that lets a provider publish straight to the network adapter -- which outlives any particular Postman request. Reframed so it reads as a property of the row, not of a request that no longer exists. Verified against a live stack via an environment override: 6 requests, 32 assertions, no failures.
…nt [OpenAgriNet/network-adapter#4] Discover asked only for text. It now sends all three parts of an Intent the Beckn spec defines -- textSearch, a jsonpath filter and an S_DWITHIN spatial constraint -- and the catalogues carry the geometry that makes the spatial part mean something. WHERE THE GEOMETRY GOES, because I had this wrong first. coverageAreas items are a oneOf -- an administrative area reference OR a GeoJSON geometry -- and coverageAreas is inherited from the AgricultureResource base pack, which neither WeatherObservation nor MandiPrice forbids in OnDemand mode. So a catalogue can carry the drawn area beside the code reference, exactly as the field's own description invites, with no provider-level field added. I had first concluded the geometry had to live at provider.availableAt[].geo, having read only the AdministrativeAreaReference branch of that oneOf and seen additionalProperties: false. coverageAreas is also the better target: it is per-resource, so two capabilities from one provider can cover different areas, where availableAt cannot distinguish them. resourceAttributes.location stays out of it -- the OnDemand branch explicitly forbids it, along with every other reading-bearing field. A catalogue advertises capability; a Direct select response carries the reading and its point. distanceMeters is 100000, not the 250000 the service's own docs example uses. The service caps it at 200000 and answers SCH_INVALID_FORMAT above that, which the test run found. VERIFIED THAT BOTH FILTERS ACTUALLY BITE, since one silently ignored looks identical to one that works: as written, 1 catalogue; probe point moved to London, 0; @type changed to a value we never publish, 0. Full run 51 assertions, no failures. Also reverts @context to the identifier the packs declare in their own x-jsonld block. Pointing it at the raw CDN was only ever to make extended validation fetch something, that stays off, and in JSON-LD @context is a name rather than a fetch target -- so the raw URL put a branch name on the wire for no gain.
…work-adapter#4] Both are Context fields, and both were absent, which made discover quieter than it looked. networkId absent does not mean "this network" -- the service reads its absence differently on each path: publish absent means APP_NETWORK_ID, used to fill an empty visibleTo discover absent means NO network predicate: every network's catalogs match So discover was searching across every network and getting the right answer by having only one. It now sends {{networkId}}, matching what publish stores. schemaContext goes in context, not message.intent. The reference implementation put it in intent, which Intent's additionalProperties:false forbids outright. Each entry is a context URI with an optional #fragment naming the type, and the predicate is exact string equality against what publish indexed off each resource: SchemaContext = resourceAttributes.@context SchemaType = resourceAttributes.@type So publish needs no schemaContext of its own -- which is also why reverting @context to the packs' declared identifier mattered: the filter has to spell the same string the resource carries, and a raw-CDN URL on one side would have drifted from the other. VERIFIED EACH PREDICATE ACTUALLY BITES, because a filter that matches everything is indistinguishable from one that works: as written 1 catalogue networkId -> another network 0 #fragment -> a type we never publish 0 context URI -> example.com 0 Full run 51 assertions, no failures.
…network-adapter#4] One directive per catalogue, in message.publishDirectives -- a sibling of catalogs, and permitted by the spec, which matters now that publish is schema-validated. catalogId matches the catalogue's own id; that is how a directive binds catalogType REGULAR. The spec requires this field, and MASTER means canonical definitions reusable across the network -- not this updateMode MERGE, stated rather than defaulted. FULL DELETES the resources a payload omits, so a republish would silently wipe anything unlisted. Worth spelling out so nobody flips it casually visibleTo ["{{networkId}}"], the same variable discover filters on visibleTo is the substantive one. Without it the network came from the receiving server's APP_NETWORK_ID, so the same publish landed in a different network on a differently-configured deployment, with no error and a discover that then found nothing. The payload is authoritative now, and both sides read one variable. Verified it binds rather than being decorative: published with visibleTo set to another network, and a discover scoped to local-network then returned 0 catalogues; republished with the correct value and it returned 1 again. Full run 51 assertions, no failures. resourceDirectives is deliberately absent -- Phase 1 refuses any carrying extends with SCH_TYPE_NOT_SUPPORTED, and there is nothing here to inherit.
…nAgriNet/network-adapter#4] 1. Weather -> 1. WeatherObservation 2. Mandi -> 2. MandiPrice The folders now read as the capability codes they exercise -- the second half of every binding key, the @type on every resource, and the schema pack each one validates against. "Weather" and "Mandi" were shorthand that matched nothing else in the system. README updated, including the --folder example. Verified both folders still run standalone under the new names: WeatherObservation 13 assertions, MandiPrice 19, and the full run 51, none failing.
…riNet/network-adapter#4] Base validation covers the Beckn envelope and treats resourceAttributes as a free-form object, so nothing checked a capability's own attributes. The provider adapter now runs extended validation on both its modules: it resolves each resource's @type to a capability schema and validates the object against it. This could not be switched on before. The validator stripped @context and @type as JSON-LD plumbing while the packs list @type in allOf[].required, so every resource was rejected for a missing @type the payload had sent -- the comment on the publish module said as much. The adapter now decides per key, keeping the ones a schema declares. The schemas are fetched, not committed. bin/fetch-schemas.sh downloads the eight published packs plus the five Beckn core schemas they $ref into config/schemas, which is gitignored and mounted read-only. A copy in git would drift from what the network publishes, which is the same reason the mappings are fetched. SCHEMA_PACKS_URL picks the revision, so that stays a deployment decision rather than something frozen into adapter config. The fetch runs in stack.sh step 2, with setup.py, not step 3. It has to: the provider adapter preloads these at startup and REFUSES TO START without them, and the path is a bind mount, so an adapter started first would find a directory Docker had invented and fail on it being empty. Failing closed is the behaviour to want here -- the alternative is accepting unvalidated payloads because a mount was forgotten. Resolution is a memory lookup, so no payload costs a network call and the container needs no egress to validate. The allowlist is narrowed to the host the packs' own @context names, so a local miss fails loudly rather than quietly fetching from elsewhere. Also renames the two capability steps to WeatherObservation and MandiPrice, matching the plugin ids on network-adapter feat/8-capability-schema-conformance -- the .so basename is the id this config refers to, so the two move together or the adapter fails with "unrecognized step". README: the schema validation section said extended was off and that publish was unvalidated. Both are now wrong. It also now records what extended validation does NOT check -- the library parses if/then/else and never evaluates it, so the packs' informationMode rules are unenforced and a pass here is not full pack conformance.
…adapter#4] validity.startsAt and endsAt resolve to ClosedTimePeriod -> TimePeriod, where both ends are format: date-time. The request sent bare dates, which base validation never looked at and extended validation refuses. Now full RFC 3339 timestamps in IST, the zone the market data is actually in, with the window closing at the end of the second day rather than at its start. Safe for the mapping: its request half reads the date with $substring($iso, 8, 2), which yields 20-08-2025 from a bare date and from a timestamp alike. Verified against the running stack -- the select passes extended validation and the collection is 51 of 51. Note arrivalDate wants the opposite: it is format: date, so the response's bare date there is correct and stays.
…nAgriNet/network-adapter#4] Drops bin/fetch-schemas.sh, the config/schemas mount, the two SCHEMA_* .env variables and the fetch step in stack.sh. All of it existed for one reason: the @context the payloads declared -- schemas.openagrinet.global -- does not resolve, and a failed schema fetch rejects the payload, so extended validation could only work off files placed on disk. The published packs do serve context.jsonld. Pointing @context at them makes the fetch the validator already knows how to do work, and the whole local mechanism unnecessary: @context .../network-specs/schema-packs-v0.1/schema/MandiPrice/v0.1/context.jsonld fetched .../network-specs/schema-packs-v0.1/schema/MandiPrice/v0.1/attributes.yaml Better than the mount in two ways. A payload names the revision it wants to be judged against, so nothing in this repo can go stale against the published packs. And the deployment loses a step that could be forgotten -- there is no directory to populate and no startup that fails because it was not. The allowlist becomes raw.githubusercontent.com, the host @context actually resolves to, and is now load-bearing: an @context on any other host is refused before a fetch is attempted. The stack already reaches that host for the mappings, so no new egress. Costs, both in the README: the provider adapter needs that egress, and the first payload after a restart pays for the fetch -- about 2s, then cached for 24h. Verified in oan-local, same configs: publish and select pass, the first payload logs "fetching from network" and later ones "LRU cache hit", a foreign @context is refused with SCH_INVALID_JSONLD_CONTEXT, and the collection is 51 of 51.
…ter image [OpenAgriNet/network-adapter#4] Two gaps this closes, both found by upgrading a live VM. There was no upgrade path in the README. It covered a first run and starting over, with nothing in between, so a sync was reconstructed by hand each time. The new section splits it by what actually changed -- config only, config plus a new image, or payload shapes -- because those need different work and doing them in the wrong order is the likely way to break a working VM. It also gives the check order (steps initialized, then errors, then the collection) and the rollback, which is both halves or neither. ADAPTER_IMAGE pointed at a build that predates the plugin rename. The configs in this repo now name WeatherObservation and MandiPrice, and a plugin id is the basename of a .so inside the image, so the shipped default would have made every adapter exit with "unrecognized step: WeatherObservation" -- a failure that reads like a config typo and is not. It is pinned to a tag built from the adapter branch these configs expect, with the coupling written down next to it and a troubleshooting entry naming the error. The tag is in a personal namespace, as the others already are. An org package published by CI is the real fix; that is issue #5.
…er#4] ADAPTER_IMAGE was pinned to a SHA tag; it follows latest now. That needs two things said, because latest here behaves less dynamically than it reads. pull_policy is missing on every image and nothing in stack.sh pulls, so a tag already on disk is never re-fetched -- `make up` does NOT pick up a newer latest, and a stack can sit on a stale image indefinitely. Both the image comment and the upgrade section now give the explicit pull, and say why the pull without the recreate and the recreate without the pull each do nothing. The rollback note is corrected too. Rolling the config back is exact, but "the old image" has no name once the tag has moved, so recovery is by digest if the layers are even still on the box. Recorded rather than argued: pinning is still the thing to do before a change you might need to undo. The coupling warning is unchanged and matters more, not less -- an id is the basename of a .so inside the image, so latest moving under a deployment can produce "unrecognized step" with nothing in this repo having changed.
…nAgriNet/network-adapter#4] The jsonpath filter compared resourceAttributes."@type" against the capability, which the request already narrows by: context.schemaContext carries the same @context and @type and is matched by exact equality before the filter runs. So the filter demonstrated the mechanism and narrowed nothing that was not already narrowed. subjectCategories is a different dimension -- Weather for one capability, Market for the other -- so the filter now does work the other predicates do not. Written as subjectCategories[*] == "..." because the field is an array. Lax mode would auto-unwrap it and the comparison is existential either way, but the explicit form says what is meant and behaves the same in strict mode. Equality keeps it indexable, which is what the service requires of a filter arriving without a text search or spatial constraint to narrow the corpus first -- ours carry both, but the expression should not depend on that. Verified against the running stack rather than by reading: "Weather" returns the one catalogue, a category nothing publishes returns none, and the other capability's category on this request returns none -- so the predicate discriminates on the value rather than passing everything. Collection is 51 of 51.
…penAgriNet/network-adapter#4] The two path-shaped fields in a discover were spelled differently for no reason: filters.expression in dot notation and spatial.targets in bracket-quoted form. The bracket form was copied from the discovery service's own documentation, where it is the CANONICAL spelling -- the one the service stores -- not a required input. Both go through the same Canonicalise(), which reads .name and ['name'] alike and rewrites either into the stored form before the comparison. So the two spellings were never doing different things, and one request carrying both just invited the reader to look for a distinction that is not there. The distinction that IS real, and is worth keeping straight: filters.expression is EXECUTED, handed to PostgreSQL as @filter::jsonpath, which is why it can carry a predicate. spatial.targets is COMPARED, as the right-hand side of target_path = ANY($1) against the path the publish walker recorded, which is why it can only name a location. That also means a typo in targets fails as an empty result rather than an error -- verified, a pointer at serviceArea instead of coverageAreas returns zero rows quietly. Checked both spellings against the running stack before changing anything: bracket and dot targets return the identical one catalogue and one resource, a wrong pointer returns none, and after the change the spatial constraint still narrows -- the same request with a point in London returns none. Collection is 51 of 51.
manjudr
reviewed
Sep 7, 2026
manjudr
left a comment
Member
There was a problem hiding this comment.
Please - update the readme file; it's verbose at the moment.
- renamed from exp to experience
- only keep local env file in api collection
- rename postgame collection to api-collection
- remove OAN keyword from file names
…twork-adapter#4] OAN-dev-flow.postman_collection.json -> api-collection.json OAN-dev.postman_environment.json -> local_postman_environment.json The old names carried both the tool and the environment in them, which dated badly on two counts: the collection is not dev-specific -- pointing it at another deployment is a URL change, which is what the environment file is for -- and Postman's own suffix convention says nothing a reader needs. Kept the .json on the environment file. Postman filters the import dialog by extension, so a file without it cannot be selected. Two things that named the old state and no longer did: - The collection's own info.name still read "registry, publish, discover, select". The registry requests were dropped several commits ago, so it advertised a folder that is not there. - quick-start/README.md promised nineteen requests and fifty assertions "the registry writes and reads that set the stack up, then publish, discover and select". It is six requests and thirty-two assertions, with no registry requests at all -- setup.py seeds it, because the registry has no route through the edge. Corrected, and it now says why they are absent rather than leaving their absence to look like an omission. Content is otherwise untouched: diffed the renamed file against the old one and only info.name differs. oan-local's collection, which targets the stack running here, is 51 of 51.
…network-adapter#4] config/adapters/exp.yaml.tmpl -> experience.yaml.tmpl, and the rendered experience.yaml with it. The rename is only the filename, and that is the point. setup.py used one string for three different things -- the template's name, the key in keys/keys.json, and the __EXP_* placeholder prefix -- so renaming the role would have renamed the keys.json entry too, and setup.py would then have generated a FRESH keypair for a participant the registry has already published a public key for. This registry cannot update a record and its delete is soft, so the id could not be reused either: the adapter would sign with a key nobody can verify, and it would surface later as an authentication error with no obvious cause. setup.py documents that trap a few lines above, which is what made it worth honouring rather than discovering. So CONFIG_STEM maps the role to its filename and the role key stays "exp". Placeholders stay __EXP_*, keys/keys.json keeps its entry, and EXP_SUBSCRIBER_ID is untouched -- a deployed VM re-renders onto the new name and keeps its identity. Carried the new name into the compose mount, .gitignore, and the two places the README named the file. The compose SERVICE is still exp-adapter and the routing file is still routing-exp.yaml; renaming either is a separate change, the service because it is what NPM's proxy hosts resolve.
…ehind it [OpenAgriNet/network-adapter#4] It was 1218 lines with no entry point. The 299-line section on the public edge came BEFORE "Before you start", so the first thing a reader met was certificates and proxy hosts, and the commands that actually bring the stack up were three hundred lines down. About forty-five minutes of reading to find a ten-minute task. Now: a body of 375 lines, eight minutes, that is only the path through. Two parts, because they are two audiences -- Part 1 runs it on a laptop, Part 2 is the differences on a VM and nothing else, so neither reader wades through the other's material. Part 1 is eight numbered steps: prerequisites, configure, shared services, seed, then one step per layer, then verify. Each layer step has the same four headings -- what it does, the config it needs, bring it up, check it worked -- so the third one is skimmable once the first has been read. The steps run provider, network, experience, which is the reverse of the request flow and deliberately so: exp-adapter depends on the other two, so that is the order Compose starts them and the order the commands work in. The request flow is in the diagram at the top, where it belongs as orientation rather than as instructions. Everything else is Appendix A to O, moved VERBATIM. No reasoning was condensed away -- the edge, the registry contents, the troubleshooting, the request-flow detail, the startup-order argument are all intact, just no longer between a reader and their first command. The long-form "Before you start" and "Test it end to end" are kept as N and O rather than dropped, since the body supersedes their steps but not their explanation. Two errors of mine found while writing it, both by checking rather than assuming: - APP_NETWORK_ID was listed as the network adapter's config. It is the discovery service's -- the network template never references it. Moved to the step where it is actually read. - The provider step listed participant ids without their capabilities. It is the PAIR that makes a binding key, which is what makes a mismatch a 404 rather than a config error, so both halves are named now. Verified: no dangling appendix references, code fences balanced, both tables render, no reference left pointing at an old section title.
id: oanregistry -> id: sunbirdRegistry, in all three adapter templates. The adapter branch renamed the package to say what the registry is rather than whose it is, and the .so basename is the plugin id this config refers to, so the two move together or the adapter exits at startup with failed to load Registry plugin (sunbirdRegistry): plugin sunbirdRegistry not found A matching image is published: ghcr.io/ameersohel45/oan-adapter at 642743e, which latest now points at too, so a deployment following latest picks it up on the next explicit pull. Note that pull has to be explicit -- pull_policy is missing and nothing in stack.sh pulls, as .env.example says. Verified end to end rather than by inspection, and it caught a real mistake: rebuilding only ONE of the three adapter images left the other two carrying the old .so, and both died with exactly the error above while the provider came up fine. All three rebuilt, all three started, 51 of 51 on the collection. Worth recording because a partial rebuild fails in a way that looks like a config typo in one adapter rather than a stale image in two. Left alone, as deployment identity rather than code: container_name oan-provider-adapter, OTEL_SERVICE_NAME, and appName/serviceName in the provider template. Renaming those changes what `docker ps` shows and what telemetry reports, which is a separate decision from naming a Go package.
…ME [OpenAgriNet/network-adapter#4] routing-exp.yaml -> routing-experience.yaml, finishing what the experience.yaml.tmpl rename started. Renamed on BOTH sides of the mount, so the container path matches the file rather than preserving the old spelling invisibly, and the template's routingConfig follows it. postman-collection/README.md is removed. Nothing linked to it, and the quick-start README's Step 8 already covered importing the two files, the counts, and why there are no registry requests. Four things in it were not covered anywhere else, so they moved rather than went away: - Point it at another deployment by editing the ENVIRONMENT file, not the collection: Postman resolves an environment variable ahead of a collection variable of the same name, so the loopback defaults survive for the next person. Step 8. - The newman one-liner for a single folder. Step 8. - What the two select requests demonstrate -- same endpoint, same adapter, different domain packages answering, because dispatch is a binding key built from the payload and not a route. Appendix O. - Why networkAdapterUrl is a variable no request uses. It is the other adapter a deployment exposes publicly, and its endpoints verify signatures, which Postman does not do -- so the variable exists to give the address a home, not because a request is missing. Without this written down it reads like an oversight and invites someone to "fix" it. Appendix O.
…et/network-adapter#4] The appendices were the original sections moved verbatim, which fixed the ordering problem and left the length one: 1224 lines of reference behind a 375-line walkthrough. Now 561. The cuts are prose, not facts. Every command, table, gotcha and reason is still there -- the certificate traps, the /publish deny and why it is a mounted file rather than a click, the Keycloak X-Forwarded-* trap, the append-only registry, the bind-mount-directory failure, the if/then/else the validator never evaluates, the npm-letsencrypt duplicate-certificate limit. What went is the paragraph of build-up in front of each one. Two sections went entirely. "Before you start -- the long form" duplicated Steps 1 and 2 with nothing added. "Testing it end to end -- the long form" duplicated Step 8 except for two explanations, which are now Appendix N on their own: what the two select requests demonstrate, and why networkAdapterUrl is a variable no request uses. THE DIAGRAM WAS WRONG, not just verbose. It drew a box around the experience adapter and put the network and provider adapters INSIDE it, with an arrow leaving the closed box afterwards -- so it read as one service containing two others. Redrawn as what it is: the consumer reaches the experience adapter, which fans out to the network adapter for discover and the provider adapter for select, and the provider calls the mocks. publish running the other way and the shared registry reads are now stated beside it rather than crammed into the drawing. Also dropped a line from Appendix K listing bin/fetch-schemas.sh, which was removed several commits ago. Listing a file that is not there is worse than not listing it. Verified: 4 tables well-formed, code fences balanced, every appendix reference in the body resolves, and no reference left pointing at a dropped section.
…/network-adapter#4] Step 1 was prerequisites and Step 2 was `cp .env.example .env`, so the walkthrough never said where the repo comes from, and every command silently assumed a working directory the reader was never put in. There is a Step 2 now that clones and cds into quick-start/, and it says so explicitly. Part 2 had the same bug, worse: Step V1 ran bin/bootstrap-ubuntu.sh from a repo that had not been cloned. The script installs git ITSELF, and documents a curl | bash form for exactly this case -- so V1 now fetches it over curl, is placed before the clone rather than after, and says to log out and back in for the docker group. The per-layer steps were also dishonest. Steps 5, 6 and 7 each ran a `docker compose up -d <adapter>`, then Step 7 offered `make up-core` as an alternative that does all of it -- so the earlier commands were work the reader did not need. Bring-up is now one command in Step 4, and Steps 5-7 are what each layer is, the .env keys it uses, and the log line that confirms it. And the over-explaining is gone. Each step had two to four paragraphs of reasoning; the reasoning is what the appendices are for, so the steps carry a command, a line of what it does, and a pointer. Part 1: 190 lines to 117. Part 2: 88 to 62. Body 388 to 279, about six minutes. Dropped an ASCII annotation in the provider step that tried to bracket six config keys into pairs with box-drawing characters and did not line up, and said "those same four values" above a list of six. Verified rather than assumed: the bootstrap URL returns 200 for 5345 bytes, the branch the clone names exists on the remote, every appendix reference resolves, and the fences balance.
Models the network-adapter service from docker-deployment/docker-compose.yml as a chart on oan-common. The adapter is stateless: its identity is a keypair in a Secret and everything else comes from the registry. Config is rendered by an init container from a ConfigMap of placeholders plus the identity Secret into an emptyDir, so the keypair is never written to a ConfigMap and never appears in a process environment. Renders fail outright on the five values whose absence would otherwise produce a pod that starts, reports Ready and does not work. HPA, PodDisruptionBudget, Ingress and the upstream readiness gates are all optional and off by default; ci/otel-ingress-values.yaml renders that other branch so lint covers it.
The chart shipped with an empty tag, which renders an image reference with no tag at all. latest keeps the draft chart installable while the discovery service has no released version to pin to.
Renames network-adapter to adapter-service and makes the role a value. The provider, network and experience adapters are the same image and the same config format, so what differed between them was configuration, not three charts: role now selects the handler role, the step list and the routing target. experience is the role that justifies the abstraction. It sits inside the trust boundary and accepts unsigned requests, so it runs as bap with no validateSign step, while the other two receive from the network and must verify first. Its example sets both explicitly rather than relying on the default, so the missing step reads as deliberate. discovery.url becomes routing.rules, a list, because provider fans out to several upstreams and a single target could not express that. Every rule is checked at render time for a target url, a missing trailing slash and a non-empty endpoint list. Each example sets fullnameOverride to <role>-adapter. Without it fullname resolves to "<release>-adapter-service", so a release named network-adapter produced a Service called network-adapter-adapter-service and the experience adapter's route to network-adapter:9201 resolved to nothing. That only worked before because the chart itself was named network-adapter. ci/otel-ingress-values.yaml now renders the experience role, so CI covers the role branches that ci/lint-values.yaml (network) does not reach. BREAKING CHANGE: chart renamed network-adapter -> adapter-service; role is required with no default; discovery.url replaced by routing.rules; config placeholders renamed __NETWORK_* -> __ADAPTER_*; routing file is now routing-<role>.yaml.
Member
Code reviewFound 1 issue:
🤖 Generated with Claude Code - If this code review was useful, please react with 👍. Otherwise, react with 👎. |
…library feat: chart repo scaffold, common library and the first service charts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
The stack —
quick-start/docker-compose.ymlRead in tiers, which the banner comments mark: registry (Postgres, Keycloak, the registry itself), discovery (pgvector plus the service), the three adapters, then two profiles —
observabilityfor ClickStack andreverse-proxyfor Nginx Proxy Manager.Two deliberate constraints. The registry is not on the edge network, so nothing outside the stack can reach it; that is also why
bin/setup.pyhas to create all five participants and both capability bindings, since there is no second way in. And every published port except the edge's 80 and 443 is bound to127.0.0.1, written literally rather than taken from a variable, because one variable that moves every port to the public interface at once is a footgun.Startup order —
quick-start/bin/stack.shEvery
maketarget is one line of delegation to this script, which is where the reasoning lives. The order is not interchangeable: an adapter's config is a bind-mounted file thatsetup.pyrenders, and Docker creates a directory at any bind-mount source that does not exist — so starting an adapter before step 2 both wedges that container and leaves a directory where step 2 needs to write a file.Seeding —
quick-start/bin/setup.pyGenerates the three adapter keypairs, registers five participants, writes both capability bindings, and renders the adapter configs from the
.tmplfiles beside them. Idempotent: keys come fromkeys/keys.jsononce it exists, and participants already registered are left alone. The same.envfeeds both the registry rows and the rendered configs, which is what keeps the two from disagreeing — a mismatch is the most common failure and the README explains which 404 means which side is wrong.Schema validation
The provider adapter validates the Beckn envelope against the pinned v2 LTS spec, and
resourceAttributesagainst the published capability packs. Schemas are fetched from each resource's own@contextand cached, so no copy lives here to drift and the revision comes from the payload. Recorded in the README because a green result is otherwise misleading: the validator library parsesif/then/elseand never evaluates it, so pack rules predicated oninformationModeare unenforced.The collection —
postman-collection/One folder per capability, publish then discover then select, 32 assertions. Deliberately carries no registry requests: the registry has no route through the edge, so nothing in a shared collection could reach it. An environment file ships beside it with loopback defaults, so importing both and changing the base URL is the whole setup.
Why
The stack needed to be deployable by someone who had not built it, on a fresh VM, without reconstructing the startup order or the seeding from source. Everything here is pulled rather than built, so
docker compose up -dis the deployment step and the images are the only moving part.Testing
Brought up from empty on a VM and exercised end to end with the collection — publish, discover and select for both capabilities. The two mocks reproduce the awkward parts of the real APIs on purpose, including Title Case keys with spaces, string prices,
dd-MM-yyyydates and a query token the mandi mock rejects when absent, which is what proves the adapter sent one.Also verified the things that fail silently: the
/publishdeny returns 403 through the edge, which is the only evidence the NPM custom config is still mounted; and losing that mount would quietly open an unauthenticated catalogue write.Notes for review
ADAPTER_IMAGEroll together. The configs name plugins by id, and an id is the basename of a.soinside the image, so an image built before the capability plugins were renamed makes every adapter exit withunrecognized step: WeatherObservation. The matching adapter change isOpenAgriNet/network-adapterPR #17.ADAPTER_IMAGEfollowslatest, which is less dynamic than it reads.pull_policy: missingis set on every image and nothing instack.shpulls, so a tag already on disk is never re-fetched:make upwill not pick up a newerlatest, and a stack can sit on a stale image indefinitely. The upgrade section gives the explicitdocker compose pull. It also means "the old image" has no name once the tag has moved, so a rollback is by digest — worth pinning before a change you might need to undo.OpenAgriNet/network-adapterissue feat: add the quick-start docker compose deployment #5.npm-dataandnpm-letsencryptvolumes, and those are the only copy.make destroydeletes them along with everything else. Worth knowing before reaching for it to clear catalogues — droppingdiscovery-dataalone does that.DB_ADDR=registry-db), so the two cannot be wiped independently. Giving it its own database is a reasonable follow-up and is not done here.config/registry/schemas/is read only at registry startup, so a change there needs the registry service restarted. The README says so in the place you would otherwise get it wrong.