Skip to content

Development - #111

Merged
manjudr merged 279 commits into
release-0.2.0from
development
Nov 2, 2026
Merged

manjudr merged 279 commits into
release-0.2.0from
development

Conversation

@ameersohel45

@ameersohel45 ameersohel45 commented Oct 30, 2026 •

Copy link
Copy Markdown
Contributor

Brings release-0.2.0 up to date with development.

ameersohel45 and others added 30 commits September 28, 2026 16:11
…eded [#37]

Addresses the review on #43.

collector-rbac is deleted. It existed to let clickstack's collector scrape
cAdvisor through the API server, which needs nodes/proxy -- and the
vendored chart defines no ServiceAccount, so the grant went to `default`
and every pod in observability inherited it: ClickHouse, MongoDB and
HyperDX included. `get` on nodes/proxy reaches any kubelet endpoint,
exec among them.

Narrowing the binding was the other option offered. Removing the scrape
is better because the scrape was already redundant: the otel-agent
DaemonSet reads the same kubelets directly, and emits the k8s.* names the
dashboards query rather than the container_* names nothing looked at. The
duplication was also the source of 21.5 million metric rows a day, which
is what forced ClickHouse to 8Gi.

otel-agent loses nodes/proxy too. Its receiver dials the kubelet directly
on :10250 and never uses the API server proxy, so the verb was unused --
inherited by copying the scrape that did need it. nodes/stats, nodes,
pods and namespaces remain, which is what kubeletstats and its metadata
lookup actually use.

The chart now follows CONVENTIONS.md: it depends on common, takes labels
from common.labels and common.selectorLabels so every resource carries
part-of: oan, and has a CHANGELOG and a README. The selector renders the
same two labels as before -- common.name is .Chart.Name without a
nameOverride -- so this is safe on a running DaemonSet, whose selector is
immutable.

Also corrects a comment claiming extra_metadata_labels attaches limits
and requests. It does not; only container.id is set.

adapter-service parses its routing instead of grepping it. The
trailing-slash guard tested lines starting `url:` and ending `/`, missing
a quoted url, a list-item form and a trailing comment -- each a slash
that reaches the router. It now checks routingRules[].target.url through
fromYaml, fails when the document has no rules at all, and names
`routing.config` rather than the chart file when that is where the
routing came from.
feat: turn on signing, signature and extended schema validation
…griNet/discovery-service#45]

Load tests for the OAN network layer, driven by JMeter, reporting
throughput, latency percentiles, error rate and per-pod CPU and memory.

    make data
    make publish|discover|select URL=...

THE DATA IS REAL. Fetched from Agmarknet with credentials that live only in
the environment -- nothing about the endpoints or the credentials is in
this repository. 4,172 markets across 36 states, of which 9 have a
commodity mapping at all: Maharashtra, Tamil Nadu, Madhya Pradesh, Uttar
Pradesh, Karnataka, Chhattisgarh, Meghalaya, Andhra Pradesh and Bihar.
That gives 19,135 real market-commodity pairs.

ONE RESOURCE IS ONE MARKET-COMMODITY PAIR, not a market carrying every
commodity it trades. Markets trade between 1 and 80 commodities, median 4,
so the alternative made every resource a different size and every discover
response a different size with it. Paired, resource size holds within 2%.

targetResources sets the total -- 100,000 -- and the result lands on it
exactly, each state taking a share proportional to its real pairs. The
manifest records realPairs alongside it, so the x5.2 stretch is visible
rather than implied. Catalogs are derived from the markets that survive
filtering, so refetching with a different set of states needs no config
change and a state with no usable market never becomes an empty catalog.

DISCOVER QUERIES ARE DISTINCT AND UNIFORM. Each carries a JSONPath filter
and a spatial constraint together, never one alone. Centres are placed
continuously along the line between two markets either side of a state
border, so the supply of distinct queries is unbounded rather than one per
border pair. The extent is solved rather than guessed: that commodity's
resources are sorted by distance and the circle's edge goes between the
last one wanted and the first not. 20,000 queries, 20,000 distinct, 67-83
matches around a target of 75, every one spanning at least two states.

Select requests name a published resource, so they measure the provider
path rather than its rejection path.

PAYLOADS ARE NOT COMMITTED: 100k resources and 20k queries is about 150 MB
and this repository is public. data/CHECKSUMS holds a digest per directory
and `make verify-data` regenerates and compares -- the same guarantee at
296 bytes. Proven by wiping all three directories and rebuilding.

Traps handled, each found by running it rather than by reading it:
a ${__P()} does not evaluate inside a JMeter doubleProp or boolProp; a
throughput of 0 is an infinite wait rather than no limit; `jmeter
--version` leads with a date banner; and Agmarknet answers 400 when it
simply has no data, which killed an all-India fetch at Delhi.

Nothing environment-specific is committed -- the target is a URL argument.
…ry-service#45]

`kubectl top pod` with no --namespace reads whatever the kubeconfig context
points at, usually `default`, which holds nothing. The samples file came out
with a header and no rows, and the report showed an empty CPU table for a
run that was otherwise fine.

It matters more here than most: this deployment puts each component in its
own namespace -- consumer-adapter, network-adapter, provider-adapter,
discovery, registry, postgres -- so no single namespace covers a request
path.

--all-namespaces prepends a column, so every field shifts one right. Reading
the old three would have taken the namespace as the pod name and the pod
name as the CPU figure: garbage that still parses. Rows are now recorded as
namespace/pod, which the report groups on unchanged and which disambiguates
two namespaces holding a pod of the same name.

Verified against the cluster: 90 rows in 12 seconds, covering all three
adapters, discovery, both Postgres instances and the registry.
dependency-check took `jmeter --version | head -1`, which on a real install
printed

    jmeter   WARN StatusConsoleListener The use of package scanning to ...

JMeter leads with a log4j plugin-scanning warning on some installs and an
ASCII banner on others, so the first line is rarely the version. The same
trap was already fixed in tools/run-benchmark.sh and missed here.

Verified against output shaped like the VM's: reports 5.6.3.
All three plans carried

    <boolProp name="LoopController.continue_forever">true</boolProp>
    <stringProp name="LoopController.loops">${__P(loops,-1)}</stringProp>

and with continue_forever=true JMeter IGNORES the loop count. Each thread
loops until the test is stopped. A positive LOOPS also sets scheduler=false,
so DURATION is not bounding it either -- the run has no end condition at all
and sails past the requested count until killed by hand.

Caught on a 6x167 run that should have sent 1,002 requests and was still
climbing at 1,207. It means every LOOPS run so far ended when someone
pressed Ctrl-C, not when the work was done, and "one full pass" was never
one full pass.

A loops value of -1 still means forever, so DURATION-bounded runs are
unaffected.

Verified by parsing all three plans: continue_forever=false, loops still
read from the property. Not executed here -- JMeter is not installed on this
machine, and the next run on the load VM is the real test.
A .jtl is CSV and JMeter writes the assertion failure into
failureMessage, so a failed request spans seven physical lines where a
successful one spans a single line. Counting lines counted every failure
about seven times, and every figure built on that count was wrong in the
direction that flatters the run: more requests over the same wall time
read as higher throughput, and the same failures over a larger
denominator read as a lower error rate.

On the first 100k publish pass that reported 4,676 requests at 2.45/s
with 12% errors, where JMeter's own summariser said 1,302 at 0.7/s with
43%. The percentiles were dragged down too, by feeding 1,866 values into
a set that holds 1,304.

Records are now rejoined on quote parity, which is the actual CSV rule,
rather than by matching a timestamp at the start of a line. Runs without
failures are unaffected: all seven discover rows report exactly as
before, and 10,000 requests for 20 threads x 500 loops is now arithmetic
rather than coincidence.
Select could not be reproduced from this file. The mock upstream ships in
this directory and was never mentioned, and the three deployment facts
that have to agree with it were written down nowhere: the registry's
upstream pointing at the mock, the provider's auth set to none, and a
mapping tag that still exists. The trap that costs the most time is now
stated too -- the seed only creates rows it does not already have, so
editing a value changes nothing until the row is deleted.

Four things were also wrong or out of date:

- the scenario table routed select through the network adapter. It does
  not go there; the consumer adapter hands straight to the provider.
- CPU and memory were described as coming from the harness sampling
  kubectl top, with nothing in the dashboard. SAMPLE defaults to none, so
  a plain run records neither, and where an OpenTelemetry agent runs the
  dashboard has them and keeps them after the run ends.
- resources.csv was listed as something every run produces. It appears
  only when SAMPLE is set.
- the limit labels read as optional detail. Nothing reads them back, so
  leaving them unset means the run cannot say what hardware produced it,
  which is what happened to every run in the first matrix.
The file opened with commands and buried the folder tree in the middle,
so a reader met `make publish` before knowing what had to exist for it to
work. It now follows the order the work happens in: what is here, what
must be in place, build the data, run, read the results, reference.

Layout moves to the top. "What you need" and the select prerequisites
merge into one Prerequisites section, and a part is added that was
missing entirely -- what the deployed stack has to provide. That last
part matters because nothing environment-specific is committed: the
ingress can be anything, so long as it routes /publish to the provider
adapter and /discover and /select to the consumer, and `URL=` names that
host with no path. Per-pod CPU and memory is listed as good to have
rather than required, since without metrics-server or a collector a run
still measures throughput, latency and errors.

Commands split by when they are used: building payloads under Prepare
the data, the run commands and settings under Run a benchmark, and
fetching runs back and tidying up under After a run. Heading levels are
normalised so nested sections stop reading as top-level ones.

No content dropped -- every make target and every setting in the table
survives the move.
Thirty-three settings in one flat list, in no order, and several rows
named a thing rather than describing it. The seven limit pairs said only
"the consumer adapter", which identifies the service and never mentions
that the setting records rather than sets. SAMPLE and ON_SAMPLE_ERROR
listed their values without saying what they select. ARGS said "extra
flags" without naming --record-host, the only one anybody passes.

They are now six tables grouped by what you are deciding: where to send
it, how hard to push, what to call it and what it ran against, what to
sample, where output goes, and fetching metadata. Every row stands on its
own, so nobody has to read the paragraph underneath to learn that a
limit variable changes nothing in the cluster.

Every setting survives the regrouping.
16% fewer words and 88 of the remaining lines are table rows, which is
the shape reference material should have. Nothing was removed that
anybody needs: every make target and every setting survives.

What went was justification rather than instruction. The data section
said the same thing twice under two headings and is now one. The run
example listed all twenty-six options at once, which reads as a wall
rather than a starting point; it is now the minimum command followed by
one realistic run taken from an actual matrix row. MandiPrice kept its
numbers and lost the reasoning behind them. The select prerequisites
became a table, since three things that must each equal something is a
table and not three paragraphs.

Also removes a duplicated sentence the earlier restructure left behind
at the top of section 4.
…phs [#45]

The rendering was off because the file had two h1s -- Benchmark at the
top and Reference in the middle -- so a reference heading drew the eye
harder than the steps above it. There is now one h1, sections at h2 and
subsections at h3, with Refreshing the data and Known quirks demoted to
bold labels rather than a fourth level.

Also fixes a numbering hole the earlier merge left: 3.2 existed with no
3.1.

Paragraphs that were really lists are now lists -- what the deployed
stack needs, what is not committed, what make data produces, and the
reasoning behind the market-commodity pairing. Nothing removed, only
reshaped, since a requirement in the middle of a sentence is a
requirement people miss.
Two things were wrong rather than merely long. mock-upstream/ was
described in prose but missing from the directory tree above it, and the
link pointing at it resolved to nothing, because numbering the sections
had changed the anchor. Both internal links now resolve.

The rest was the file repeating itself. The limit variables were
explained twice in the same section, CONFIG three times across two, and
Refreshing the data restated section 3.1 command for command. The tail of
4.1 also described what a run writes -- resources.csv, the per-pod table,
the labelled-against-deployed comparison -- which is section 5's subject,
so it moves there.

Also drops a bold "Tidy up." stranded under a heading called Tidying up,
and settles on one URL style; the last example still said
http://<host>:<port> where everything else says https://<host>.

487 lines from 519, with every make target and every setting intact.
Publish is bound by contention rather than by hardware: every request
takes the catalog's row lock, loads everything already stored in it, and
rebuilds the filter documents for the whole catalog. With nine shared
catalogs the requests queue, and the wait is what crosses the adapter's
30-second timeout.

BENCH_FRESH_IDS=1 measures the same work without that. Before each
request goes out, a token unique to it is appended to every `catalog:`
and `resource:` id in the payload, so the request takes a lock nobody
wants, merges into an empty catalog, and rebuilds only its own hundred
resources. The difference between the two runs is the cost of sharing a
catalog.

Applied to the id prefixes rather than to whole ids, which keeps the
document consistent: publishDirectives[].catalogId still names its own
catalog and an offer's resourceIds still name resources in the same
request. Verified over two rewrites of one payload -- no catalog or
resource id in common, directives still matching, context.action
untouched.

Off unless the variable is set, so every existing command behaves as it
did. It needs no new payloads either: the rewrite happens to the body
already being read, beside the transaction and message ids.
…ount [#45]

Two things a reader of a report could not previously work out.

The fresh-catalog rewrite was reached through BENCH_FRESH_IDS in the
environment. It decides what publish measures -- on, every request gets a
catalog of its own and there is no contention; off, requests queue for the
catalogs already stored -- yet both produced identical run.env and identical
reports. Two runs measuring opposite things were indistinguishable afterwards.

It is now --fresh-ids on run-benchmark.sh and FRESH_IDS= in the Makefile,
carried to the plan as -JfreshIds, written to run.env, and stated in the
report. The plan compares it against the allowed values rather than testing it
for truth: the property is always set, and "no" is a true value in Groovy, so
the old `if (fresh)` would have turned it on for every run.

LOOPS is counted per thread, so 10 threads and 501 loops is 5,010 requests,
not 501. The corpus is 1,002 payload files and the list restarts when a run
outlasts it, so that run publishes the set five times over. The README says
both, and the report now prints the total alongside the loop count.
feat: add a benchmark harness for publish, discover and select
OpenBao v2.6.3 from https://openbao.github.io/openbao-helm, committed whole and
byte-identical to the upstream tarball, as kong and cert-manager are. OAN's
settings arrive as a values file, not as edits to the chart.
- openbao.dev.yaml: one server on raft storage, auto-unsealed by the static
  seal from a Kubernetes Secret (no cloud KMS, no manual unseal), audit log to
  its own volume, injector off since ESO delivers secrets.
- openbao-cluster-secret-store: the ClusterSecretStore ESO reads, logging in
  with Kubernetes auth as its own ServiceAccount. Replaces
  asm-cluster-secret-store.
- oan-secrets: asm-secrets pointed at the openbao store. Same 12 secrets, same
  keys, same namespaces.
- openbao-configure.sh: one-time init, KV v2 at secret/, Kubernetes auth, and
  a read-only policy on oan/<env>/* for ESO. Re-runnable.

The asm-* charts stay until a separate cleanup PR.
asm-to-openbao.sh copies every oan/<env>/<name> entry value for value -- the
values are baked into Postgres roles, Keycloak's stored client secret and the
adapter keys the registry holds, so they must not be regenerated.
secrets-snapshot.sh fingerprints every managed Secret (hashes, never values)
so the switch can be proven with an empty diff.
README gains the bring-up, verification and migration steps; CONVENTIONS and
the registry README describe the OpenBao flow. adapter-service's config
comments change too, so it and registry get PATCH bumps with changelog entries.
…es [#166]

- openbao-configure.sh: an unreachable OpenBao (pod not yet running, wrong
  kube context) was read as "not initialised" and triggered an init. It now
  retries for a minute and then stops with an error. Init output goes to a temp
  file moved into place only on success, so a failed init no longer leaves an
  empty file that every re-run refuses to overwrite.
- asm-to-openbao.sh: the aws CLI's stderr was captured into the value, so any
  warning on a successful read failed the JSON check for every entry. It is now
  kept separate and shown only on failure.
- asm-to-openbao.sh: a failed OpenBao write aborted the run under set -e,
  leaving it half-copied with no record. Each failure is now reported, the rest
  still copy, and the script exits non-zero at the end.
CONVENTIONS.md: no chart carries an oan- prefix; the organisation is in the
part-of: oan label, not in names. The chart directory, its name, and every
reference in the docs and scripts change. Rendered output differs only in the
chart name label, so the in-place upgrade of the asm-secrets release works as
before -- its Argo CD app now points at charts/openbao-secrets.
OpenBao refuses any request no audit device can record, and nothing rotates
the audit file, so a full audit volume would stop every request -- ESO's reads
and admin logins alike. A second device writes to stdout, so a request fails
only if both fail, and the audit trail reaches the cluster's log pipeline.

Reproduced on a 1 MiB audit volume: file-only answers 500 once it is full;
file plus stdout keeps serving.
…166]

ESO logs in to OpenBao as its own ServiceAccount whichever namespace an
ExternalSecret is in, so the OpenBao policy cannot tell namespaces apart. An
ExternalSecret in any namespace could read postgres-superuser. The new
allowedNamespaces value (default external-secrets, where openbao-secrets is
installed) renders as the store's spec.conditions, and the chart refuses to
render with it empty.
ameersohel45 and others added 27 commits October 30, 2026 16:38
ADR-0002 records why the tiers moved to /consumer/, /network/ and /provider/,
and how the paths callers see stay what ADR-0001 promised: the edge rewrites
/discover onto /network/ and the transaction actions onto /provider/, so the
mount names are ours and nobody else carries them. NETWORK_BASE_URL goes back
to no path.

The chart banned snippet, rewrite and regex annotations outright, because one
could reach /_internal or /publish. That ban also forbids the mapping this
needs. It is replaced by a check on the rewrite TARGET, refusing only /publish
and /consumer -- the two mounts that take unsigned calls. Anything else is the
deployment's business, so a mount added later needs no chart release. No
ingress controller is named: Kong, ingress-nginx and ALB spell a rewrite
differently and the annotations come from values.

quick-start does the same job in nginx, but on the individual proxy host rather
than server_proxy.conf: that file is included in every server block and the
upstream differs per host -- in multi mode /select belongs to provider-adapter
and /discover to network-adapter, which no shared rule can express. The shared
file keeps what is true everywhere, denying every mount by name. Appendix B
carries the per-host rule.

Verified: the descriptor advertises /discover again, the adapter answers 401 on
/network/discover and /provider/select, 404 on /discover, and the chart accepts
a rewrite onto /network/ while refusing one onto /publish.
A rewrite is an annotation on the Ingress and holds one target, so the paths
sharing an object must share a destination. Single mode serves /discover from
the /network/ mount and /select from /provider/, which are two -- and the chart
could only ever emit one Ingress.

ingress.routes takes a list and emits one Ingress each, with its own
annotations and tls. ingress.annotations/hosts/tls still work and still produce
exactly one, so nothing already deployed changes.

The guard runs per route rather than once: a path that is not a literal
/<action>, or a rewrite targeting /publish or /consumer, fails the render
wherever it appears.

Exactly one route carries the tls block for a hostname. A TLS Secret is
namespaced and cert-manager orders per Ingress, so a second block is a second
ACME order for a name already covered -- against a limit of five failed
authorizations per hostname per hour. Kong terminates on SNI.
The guard refused any rewrite whose value mentioned publish or consumer, which
also refused /network/publish -- the mount that serves the publish action
BEHIND validateSign and checkCrossNetworkRequester. The word was never the
problem; the mount is.

It now works out where a request lands -- the rewrite if there is one, else the
path unchanged -- and refuses only when the first segment is /publish or
/consumer. So /publish rewritten onto /network/publish is allowed, and /publish
with no rewrite is not, which is the case that matters: without one it reaches
the mount that signs on behalf of its caller and checks nothing.

That makes a public /publish safe to expose. The caller has to sign for itself,
with a key the registry publishes. Anything that cannot sign reaches the
unsigned mount by port-forward, which is the only way it should be reachable.
The charts had no crawler at all, so a Helm-deployed network could answer
another network's crawl but never run one of its own.

The network role now carries the catalogcrawler block, with the settings an
operator flips coming from values.federation and the rest from placeholders
the init container already substitutes -- the signing key and key id are the
same ones that sign everything else, so what we send is signed as us.

Off by default, and off builds no schedule and never dials: a cluster that
federates with nobody stays quiet rather than logging a refused connection on
an interval. Answering another network's crawl is unaffected either way; that
is the /network/ mount's job.

No database. dbDsn is required only by staticIndexUrls, networks or
publishPipelines, none of which this uses.

A crawled catalog is published to this adapter's own /network/publish, the
same path a local publish takes, so nothing leaves the process to do it.
Nothing in the charts served .well-known, so a cluster could neither tell
another network who it was nor read anyone else's answer. Federation existed
only in quick-start.

A Job signs the descriptor, JWKS and schema packs from this network's registry
entry and writes them into a ConfigMap; a small server mounts that ConfigMap
and answers for exactly those three paths. Kong routes to it.

The Job mounts the SAME Secret the adapter reads. oanfed refuses to sign with a
key the registry does not publish for this network, so sharing it is what
guarantees the descriptor names the key that signs traffic -- a mismatch there
surfaces much later, as another network failing to verify anything we send. The
private key exists only inside the Job; what reaches the ConfigMap is public.

Each run publishes the next revision, read from what the ConfigMap already
holds. oanfed refuses a revision that does not increase on the one served, and
so does every network holding the old one, so the number cannot come from a
value in git that a re-run would repeat.

Admissions are values, one Job each, so who we agreed to talk to is reviewable
rather than a command somebody ran once. They need no key: admission reads the
other network's public documents and writes a row in our registry.

oanfed's open_registry imported quick-start's setup.py and read .env, which a
Job has neither of. It now takes REGISTRY_URL and BECKN_NETWORK_ID from the
environment when both are set and falls back to setup.py otherwise, so the same
file runs in both places.

The server holds nothing secret and serves nothing else: 404 for any other
path, and the Ingress template refuses one outside /.well-known/.
The chart's Participant schema was the pre-federation one. role was a single
string, so one participant could not serve several layers -- which is what
single-adapter mode is -- and there was no federationRole, revision or
projectionTtl at all, so `oanfed admit` would have been refused by the registry
it writes to. The federation chart could not have worked against it.

SchemaRegistry's schemaUrl pattern allowed only raw.githubusercontent, where a
crawled catalog carries the published openagrinet.github.io ref in its
@context; a pack URL in that form was refused.

Both are now the files quick-start runs, which is where the federation work was
proven. ProviderSchema was already identical.
adapterKeys pointed each tier at its own key Secret, from when each tier was
its own participant. A network has ONE identity: another network admits a
network, not three adapters, and the Network Descriptor names one id and one
keyId -- so a tier signing with a different key signs with one that descriptor
never mentions, and nothing the other side holds can verify it.

All three now default to network-adapter-keys. They remain three settings
because a deployment that seeded three participants before the modes merged
keeps working by pointing each tier at its own Secret, until those identities
are retired.
quick-start dropped Keycloak and reaches the registry without a token;
the charts still required one. Same stack, two behaviours.

authenticationEnabled already existed but did nothing useful: the chart still
demanded keycloak.url and an admin client secret, and still emitted every
sunbird_sso_* and OAUTH2_* variable, so turning it off meant standing up a
Keycloak nothing talked to. Those requirements and that env block are now
conditional on it.

registry-seed mints a token to write. It now mints one only when the registry
checks one, and omits the Authorization header otherwise -- the seed document
carries authenticationEnabled so the Job and the registry cannot disagree.
keycloak.url and seedUser.passwordSecret are required only while it is true.

What kept the registry private was never the token. It has no Ingress and its
own namespace, and Sunbird RC uses POST for reads as well as writes, so no
method rule would separate them if it ever were routed.

Verified: with auth on the chart emits the same 8 Keycloak variables as before;
with it off, none, and neither chart asks for a Keycloak value.
…ok [#72]

Choosing what an agreement covers meant opening their schema-packs.json in a
browser and copying URLs back into PACKS. `make verify` reported a COUNT and
the URL to go and read -- the one command whose job is to tell you about them.

verify now lists them. admit with no PACKS lists what that direction can cover
and refuses, rather than saying only that one is required -- and the list is
theirs for `source`, ours for `requester`, because those are different
questions.

PACKS=all takes every pack they publish TODAY. A snapshot, not a standing
agreement: one that silently widened when they added a pack would not be much
of an agreement, so taking in a new one is the same command again.

Naming a pack the other side does not publish is still refused.
A requester agreement took its candidates from our own SchemaRegistry rows. A
row exists the moment a provider is registered; the other network only learns
of it when host-descriptor republishes. So we could agree to give a capability
that is not in the document they read -- something they have no way to know to
ask for.

Measured on a live stack: register a provider, do not republish, and the
registry holds two packs where the published document holds one.

Both directions now read a DOCUMENT rather than a registry: theirs when we read
from them, ours when they ask us. Agreeing to give an unpublished pack is
refused, and republishing is what makes it available -- which is the same
order the other network sees it in.

admit takes --out for where host-descriptor wrote .well-known, and refuses with
an explanation when it is absent rather than quietly falling back.

The test fixture served one .well-known as both our document and the peer's.
They are separate directories now, because the two lists are different
questions and conflating them is what hid this.
]

A requester agreement covers the packs in the document the other network reads.
The admit Job mounted only the scripts, so it had no .well-known at all and
would have refused every requester admission -- the right refusal for the wrong
reason.

It mounts the ConfigMap the publish Job writes, at the path it passes as --out.
Optional, so a missing document is oanfed naming the command to run rather than
a pod stuck Pending with the reason two layers away; publish is an earlier hook
weight, so a normal upgrade does both in order.
Comments across this branch ran to ten, twelve, thirty-five lines where two
were allowed. The reasoning belongs in a doc, not above a function, and a
newcomer should not have to be told how the rest of the system works before a
comment makes sense.

Nothing about behaviour changes. Both charts lint, every values file renders,
the 38 oanfed tests pass, and a live adapter was re-rendered from the trimmed
templates and came back with no restarts.

What is left over two lines is mostly a worked example -- an ingress route, an
admissions entry, a make command -- where the shape is the explanation.
The split put quick-start files here. They belong on the quick-start branch.
The path split left quick-start files here; they belong on the quick-start
branch. charts/federation keeps its own copy of oanfed.py.
Replaying onto fix/agmarknet-token-auth dropped four things that branch had added: the 0.8.0 bump, its changelog, Grievance in the single-adapter step list, and a pack URL pattern that allows a dotted ref. The pattern is now both widenings rather than either.
release-0.2.0 is where the networkId one-or-many and discover pagination live; the beckn tag predates both.
nginx-unprivileged keeps its temp paths and its pid under /tmp. The emptyDirs were at /var/cache/nginx and /var/run, where the privileged image keeps them, so with a read-only root nginx could not start: mkdir() "/tmp/proxy_temp" failed (30: Read-only file system).
A secret volume is owned by root and the Job runs as 101, so defaultMode 0400 left the key unreadable: PermissionError on /keys/signingPrivateKey. fsGroup gives the file a group the container is in, and 0440 lets that group read it.
oanfed appends /api/v1, the adapter's own registry.url already carries it, and the comment said to match the adapter -- so the value was copied across and Sunbird answered 404 reading "api" as a schema name.
The consumer routed discover there and nothing else, so a publish arriving at the consumer had no rule and errored before it reached the mount that answers it.
Only services that export OTLP themselves (network adapter, discovery)
reached HyperDX; everything else logged to stdout and nothing collected it.
A filelog receiver now reads /var/log/pods on each node, k8sattributes names
the owning workload, and that becomes service.name. Chart 0.2.0.
…charts

feat: a federation chart and its ingress
@manjudr
manjudr merged commit 81491b3 into release-0.2.0 Nov 2, 2026
3 checks passed
@manjudr
manjudr deleted the development branch November 2, 2026 06:48
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

9 participants