Create the annia enqueuer #95

Merged
tepichord merged 11 commits from feat/89-annia-enqueuer into main 2026-09-24 11:29:03 -07:00
Owner

Closes #89

Automated PR created by forgejo-done adapter.

Closes #89 Automated PR created by forgejo-done adapter.
Refs: #89
Enable msgTopologyOperator with nodeSelector and resources pinned.
The chart's useCertManager: false default generates self-signed webhook
certs, so no cert-manager dependency is introduced.

Refs: #89
Vhost annia, exchanges annia.events/annia.dlx (topic, durable),
quorum+durable queues annia.dispatch (x-delivery-limit: 2, DLX annia.dlx,
routing key annia.dead) and annia.dispatch.dead, both bindings, and
annia's Permission. This is the cross-repo contract with annia-infra #308.

Refs: #89
VaultAuth default in annia namespace with cross-namespace vaultConnectionRef.
Two VaultStaticSecrets: annia-hmac-secret (annia/forgejo-webhook) and
annia-rabbitmq-credentials (annia/rabbitmq), both kv-v2, refreshAfter 1h,
destination.create + overwrite true.

Refs: #89
Service port 80 -> http, Deployment with image pinned from annia-infra,
env from both Vault secrets (HMAC + RabbitMQ credentials), TCP readiness
probe, worker nodeSelector, resource requests/limits.

Refs: #89
- VSO tripwire: (7, 11) -> (8, 13) for annia's VaultAuth + 2 VaultStaticSecrets
- msgTopologyOperator.enabled assertion flipped to True with nodeSelector/resources checks
- New test_annia_topology_matches_the_cross_repo_contract asserting queue
  type/durable/arguments, DLQ binding key, events->dispatch binding, Permission,
  and Service name/port

Refs: #89
- docs/vault.md §4c-ter: add annia-read policy and annia to the role loop
- docs/rabbitmq.md §16e: annia's vhost, user, topology, DLX inspection,
  imperative user creation, and cross-repo contract
- OPERATIONS.md §1: add annia broker-side bootstrap row

Refs: #89
docs(rabbitmq): pass annia's password to rabbitmqctl via stdin
Some checks failed
ci/woodpecker/pr/gates Pipeline failed
7487e0b799
`rabbitmqctl add_user annia -` makes the literal `-` the password:
add_user reads stdin only when the password argument is omitted. The
command also lacked `kubectl exec -i`, so nothing forwarded stdin.
Drop the argument and add `-i` so the Vault-held password is used as
the broker user's actual credential.
Two resolutions, both in kustomizations that each side added to:

  * apps/annia/kustomization.yaml — union of the two resource lists. main
    lists llm-gateway.yaml (the tailnet-egress Service, #93); the branch lists
    service.yaml + deployment.yaml (the enqueuer). Both survive: the conflict
    was in the one line that names the resources, not in their content.

  * infrastructure/kustomization.yaml — `namespaces/annia.yaml` was listed
    twice. git merged both additions with no conflict marker because they
    landed at different offsets (main put it after postgres.yaml, the branch
    after rabbitmq.yaml), and `kustomize build infrastructure` rejects the
    second entry as "may not add resource with an already registered id" —
    which test_kustomize_build catches, demonstrated by reinstating the
    duplicate. main's position is kept.

Nothing else needed reconciling. The Vault* tripwire in tests/test_gitops.py is
already (8, 13) for the merged tree — main added no VaultAuth/VaultStaticSecret
(the preview-system pair predates the fork), so the branch's bump from (7, 11)
is the whole of the change. `.woodpecker/gates.yaml`, tests/test_static.py and
tests/test_docs.py merge byte-identical to main, so the CI-resolver, step-tool
and docs guards need no reconciliation.

Still open, deliberately not decided here: apps/annia/deployment.yaml pins
`.../annia-enqueuer:<annia-infra-pin>`, a `<...>` placeholder, and the registry
answers 404 NAME_UNKNOWN for that repository. `apps.yaml` has `wait: true`, so
the layer will not reach Ready until annia-infra #310 publishes the image.

tests: 227 passed (`nix develop --command python3 -m pytest tests/ -q`).
`apps/annia/deployment.yaml` pins `zot.faun-snapper.ts.net/milner/annia-enqueuer:<annia-infra-pin>`,
a `<...>` placeholder where a tag goes, because the image is built by
annia-infra (#310) and has not been published. Verified against the registry
(read-only): `GET /v2/milner/annia-enqueuer/tags/list` answers 404
NAME_UNKNOWN — the repository itself is absent, while every other image this
tree pins resolves (oz 0.1.7, router 0.1.3, reaper 0.1.0,
woodpecker-secret-extension 0.2.4).

A reference like that is unparseable, so the pod does not even pull
(`InvalidImageName`, not `ImagePullBackOff`), and `apps.yaml` declares
`wait: true` for the layer — so listing it holds all of `apps` not-Ready for as
long as the other repository takes, retrying every interval. The cluster is
currently green on every layer (`main@ee9b6d35`, all seven Ready /
ReconciliationSucceeded), and a layer that is permanently red is how the next
real failure hides. Contained, but not visible to any pipeline tier: kubeconform
sees a string, conftest's `_is_placeholder` recognises `${...}` postBuild
substitution rather than `<>`, the server-side dry-run does not validate image
references, and L3 applies `apps/preview` rather than this layer.

So the manifests land, reviewable, and the workload does not: `service.yaml`
and `llm-gateway.yaml` stay listed (the Service is the address the enqueuer will
answer on and the shape the cross-repo contract test asserts; the gateway is
#93's egress path). Wiring the Deployment in and replacing the placeholder with
a real tag are one change. `../evolved-npcs/README.md` states the same rule for
a directory with no workloads in it at all.

Guarded, because nothing else can catch this offline:
`test_no_rendered_layer_pins_a_placeholder_image` renders every layer and refuses
any container image containing `<>`. Demonstrated to bite by re-listing
`deployment.yaml` — it fails naming the layer, workload, path and image. It
asserts it saw containers at all so it cannot pass vacuously, and the walk covers
Deployment/StatefulSet/DaemonSet/Job, CronJob and bare Pod (4 pod templates
across the layers today).

docs/rabbitmq.md §16e is updated in the same change: it described the pod as
`ImagePullBackOff`, which is no longer the state and was never the failure mode.

tests: 228 passed (was 227, this guard is the one new test).
fix(annia): give the topology CRs the spec.name and broker the CRDs require, because L1 caught what the suite skips
All checks were successful
ci/woodpecker/pr/gates Pipeline was successful
ci/woodpecker/pull_request_closed/cleanup Pipeline was successful
6446088b9d
Pipeline 84 (PR #95, commit 3a960ac, `l1-validate`) failed on the five new
`rabbitmq.com` objects. Reproduced exactly: kubeconform with L1's CRD-catalog
schema location rejects them with `-strict` —

  Vhost annia              missing properties 'name', 'rabbitmqClusterReference'
  Exchange annia.events    missing property 'name'
  Exchange annia.dlx       missing property 'name'
  Queue annia.dispatch     missing property 'name'
  Queue annia.dispatch.dead missing property 'name'

This is not a catalog nit. Verified against the CRDs the pinned chart actually
installs (`helm show crds oci://registry-1.docker.io/cloudpirates/rabbitmq-cluster-operator
--version 0.6.16`, generated from messaging-topology-operator v1.19.3):
`Vhost`, `Exchange` and `Queue` all declare `spec.required = ['name',
'rabbitmqClusterReference']`. Both fields are also *semantic*: `spec.name` is
the RabbitMQ entity name while `metadata.name` is only the Kubernetes object
name, and `rabbitmqClusterReference` is the sole link to apps/rabbitmq's
Cluster — there is no default. So the API server would have rejected the first
Vhost with `spec.name: Required value` and taken the whole `data` layer's apply
with it, identically on every retry.

Why the suite did not catch it, and L1 did: tests/test_gitops.py runs
kubeconform with no `-schema-location`, so with `-ignore-missing-schemas` every
`rabbitmq.com` kind is skipped rather than validated. L1 is the only gate that
fetches the datree CRD catalog, which is exactly why AGENTS.md keeps that fetch
there. `rabbitmq.com` is new in this change, so nothing had exercised that gap
before.

Fixed by declaring the fields the CRDs require:

  * `spec.name` on the Vhost, both Exchanges and both Queues.
  * `rabbitmqClusterReference` on the Vhost, which omitted it entirely.

The offline guard is an explicit assertion in
`test_annia_topology_matches_the_cross_repo_contract`, because no offline gate
can see a `rabbitmq.com` schema: every object must carry
`rabbitmqClusterReference = {name: rabbitmq, namespace: rabbitmq}`, and the five
kinds that require `spec.name` must carry one equal to their entity name. It
also asserts the inverse — `Binding` and `Permission` must *not* carry
`spec.name`, since their CRDs declare no such field and a structural schema
rejects unknown ones. Both halves demonstrated to bite, with messages rather
than `KeyError`s.

The manifest header records the provenance of the required-field claim and the
reason the offline gate is blind to it, so the next person does not read
`-ignore-missing-schemas` as "validated".

Verified: L1's two commands reproduced locally end to end — kubeconform (with
the catalog) PASS over all eight renders, conftest 596 passed / 0 failures.
tests: 228 passed.
tepichord deleted branch feat/89-annia-enqueuer 2026-09-24 11:29:03 -07:00
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
tepichord/milner!95
No description provided.