bankd v1.0.18 takes only a lux.id access token issued to lux-financial for
the lux org, maps only a member of the admin org to _superusers, and listens
on HTTP alone. Its pod reads none of the bank-secrets keys, so the five env
references to them are gone.
bank-docs states the amd64 node selector its pod already runs with.
e2e/40-lux-financial proves the one way in: neither console serves a password
input or ships a credential in its JS; every sign-in screen hands off to lux.id
with client lux-financial, PKCE S256 and the console's /auth/callback; bankd
answers 404 on every password and Base sign-in route, the code exchange takes
only lux-financial by code or refresh, and a token lux.id did not issue to
lux-financial reads as anonymous. The password-form sign-in and the seeded
ledger totals it asserted are deleted: neither exists.
k8s/lux-k8s/{bank-api,bank-dash,bank-docs,bank-sandbox,bankd,financial} and
scripts/seed-bank-admin.ts are deleted. deploy/lux-bank declares the bank,
lux.financial is a static site, and no host those files name resolves.
Co-authored-by: Hanzo Dev <[email protected]>
deploy — the fleet, and the only thing that deploys it
Every service is one values file:
deploy/<namespace>/<name>.yaml
That is the whole inventory. Adding a service is adding a file; there is no
second list to keep in step with it, and pin.sh --which reads the answer to
"who runs this image" out of the directory rather than out of a table.
Each file is values for one shared chart, referenced by version:
oci://oci.hanzo.ai/charts/app:0.1.11
helm template <name> oci://oci.hanzo.ai/charts/app --version 0.1.11 \
-n <namespace> -f deploy/<namespace>/<name>.yaml
What actually runs
The lux-fleet generator and its fence live in hanzo/universe infra/k8s/hanzo-cd/project-lux.yaml; they read this repo from GitHub and deploy to the cluster named lux-cluster.
a git generator over deploy/*/*.yaml, one Application per file, destination
lux-cluster, and a templatePatch that reads each file's own cd.automated:
syncPolicy:
automated:
prune: false
selfHeal: true
19 of the 122 values files set cd.automated: true: the static sites,
each verified answering from the edge on the current cluster. Every pod-based
file is false until it is shown to run there; a pin bump does not opt a file
back in. This file previously said the ApplicationSet "deliberately has no
automated block" and that "there is no CD in this repository yet"; neither has
been true since the templatePatch was added, and reading it that way is how a
hand-edited object gets healed back and nobody knows why.
prune: false throughout. Nothing here deletes: a values file that stops
declaring an object leaves that object running, which is why removing a service
is a deliberate act on the cluster and not a git rm.
Moving an image — pin.sh
deploy/pin.sh <namespace>/<service> <version> # a release
deploy/pin.sh --verify [<service>…] # every pin, against its registry
deploy/pin.sh --which <repository> # who runs this image
deploy/pin.sh --freeze <service>… # a moving tag names bytes
cd.hanzo.ai does not watch the registry — it watches this directory. Publishing
an image changes nothing; moving the pin here is the deploy. .hanzo/workflows/ image-published.yml is the only automated caller, and it does nothing else.
A pin is two fields and one fact. tag is how a human says which release;
digest is what the kubelet pulls. The chart renders repo:tag@digest, and a
reference carrying both resolves by digest — so a tag written from one
registry read and a digest from another can name two different releases, and the
pod then reports the tag while running the digest. pin.sh resolves both from
one HEAD /v2/<path>/manifests/<tag> and writes them in one edit.
That is not hypothetical here. zoo/exchange-api pinned 2.3.34 to a digest
that 2.3.34 no longer resolves to, beside three other deployments of the same
version with no digest at all: four pods, one version number, two sets of bytes.
These files were generated from what is RUNNING
Not from the manifests in k8s/. Those were applied by hand over a long period
and their currency is unknown. The running fleet is the only statement of what is
true, so it is what was read.
Where the two disagreed, k8s/ was the stale one — and where k8s/ held
something these files did not (an IngressRoute, a ConfigMap, a claim, a Role),
that object moved here and the manifest was deleted. See k8s/README.md for the
two objects that could not move and why.
selectorLabels — why most files have one
spec.selector cannot be changed after a workload is created. Most services here
select on app: <name>, not the chart's app.kubernetes.io/name + instance,
so each file records the selector its workload already carries. Without it the
chart would emit its own and the apply would be refused.
It is the selector only — the full label set on metadata is still the chart's.
Verifying, before changing anything
# render every file and diff it against the live object
python3 <universe>/charts/app/hack/diff-live.py --context <ctx> --values deploy
- BREAKING — the apiserver would refuse the apply. Only a workload's
spec.selectorqualifies; it is immutable. - DRIFT — live differs from the file. Someone changed one of them.
- BENIGN — an intended consequence of moving to the chart.
What is NOT here
Chain CRs (LuxChain, LuxNetwork, LuxIndexer, LuxGateway, LuxMPC) are
not services and have their own controllers. They stay in k8s/.
Secrets are not here and never will be. The chart has no way to carry a secret
value: kmsSecrets names a path in KMS and the operator syncs it.
lux-system/s3 is deliberately absent. hack/live2values.py generated one from
Deployment/s3 and both halves were wrong: that Deployment is a 0-replica husk,
and Service/s3 selects app: seaweedfs with a single endpoint on a
StatefulSet — so a sync would have repointed it at a selector matching zero pods
and taken S3 down for every in-cluster consumer. Correcting it means picking one
name for the thing, and renaming a StatefulSet is a recreate against its bound
volume. Until that lands, lux-system S3 is visibly outside CD, which is the
truthful state; a values file claiming otherwise is worse than an absence.
Why the chart is pinned at 0.1.11 and not 0.1.13
One values file does not render under 0.1.13:
deploy/lux-cloud/price.yaml
kms: Additional property hostAPI is not allowed
kms: Additional property credentialsSecret is not allowed
0.1.13 moved KMSSecrets to a ZAP transport: transport + zapAddr replace
hostAPI and the per-CR credential, and the rendered object's group changes from
secrets.lux.network/v1alpha1 to kms.hanzo.ai/v1. That is a cluster-side
change, not a values edit — the lux KMS operator has to serve the new group, and
the ApplicationSet's ignoreDifferences still names the old one. The chart's
default zapAddr is cloud.hanzo.svc:9653, which is a hanzo-cluster address;
Lux's KMS plane is kms.lux.cloud, and what its ZAP endpoint is has not been
established.
So the bump is a migration with a cluster prerequisite, and it is not done here.
The rest of 0.1.13 is already satisfied: image.digest is present wherever the
chart's claim guard requires it, and lux-explorer/lux-faucet no longer carries
its version inside image.repository. Under 0.1.11 all 122 files render.
Pins still to move
pin.sh --verify reads 121 pins and 116 agree with their registry. The five that
do not are credentials rather than defects:
| Pin | Why it cannot be read |
|---|---|
hanzo/lux-admin |
registry.digitalocean.com, no DO registry credential |
lux-devnet/explore-fe-pars, lux-testnet/explore-fe-pars, lux-mainnet/explore-fe-pars |
ghcr.io/parsdao/explore answers 403 to a luxfi token |
About a hundred services still carry a concrete version with no digest. Those are
not ambiguous the way :latest was — the tag names a release — but a registry
tag is mutable, which is how zoo/exchange-api came to be pinned at bytes its
own version no longer meant. The end state is a digest beside every tag, written
by the release that built it. pin.sh <service> <version> does that for any
service whose repo dispatches on publish; --freeze is the interim for one that
does not.