Skip to content

Prepare the first release: README rewrite and weft.alethic.solutions/v1 API - #12

Merged
wasabii merged 9 commits into
mainfrom
claude/readme-rewrite-release-311bea
Oct 18, 2026
Merged

wasabii merged 9 commits into
mainfrom
claude/readme-rewrite-release-311bea

Conversation

@wasabii

@wasabii wasabii commented Oct 18, 2026 •

Copy link
Copy Markdown
Contributor

What this changes

Gets Weft ready for its first release to external users.

README for external readers. The README is rewritten for people meeting Weft for the first time. It now covers what Weft is, a runnable quick start, features, install and image verification, and an index of the docs. The detail the old README carried is moved, not dropped: docs/operating.md covers status conditions, pruning, teardown, flags, metrics and uninstalling; docs/design.md covers the comparison with other tools, why Starlark, and non-goals. Install instructions no longer assume prerelease-only (--devel).

New API group, weft.alethic.solutions/v1, served alongside weft.run/v1alpha1. Weft is already deployed internally under weft.run, so the old group keeps working unchanged:

  • Two CRDs, one schema. Kubernetes can't convert between groups, so each group has its own CRD. Canonical types live in api/v1; api/v1alpha1 embeds v1's spec and status, and a test checks the generated schemas match. The weaves.weft.run CRD changes only in two description lines.
  • Each Weave writes its own group's keys (internal/naming): finalizers, provenance label and annotation, the owned and adopt annotations, and the field manager. A test pins the weft.run keys so they can't change by accident. The leader election lease keeps its old name, so a rolling upgrade's old and new pods share one lease.
  • One reconciler per group, both working on the v1 type internally (internal/controller/served.go). The watch registry, metrics and kube client know which group a Weave belongs to, so same-named Weaves in the two groups never interfere.
  • Moving a Weave between groups keeps its objects in place. A v1 Weave takes over what its same-named weft.run predecessor made, without recreating anything. It moves field ownership from the old field manager to its own, so its first apply removes the old provenance. This happens in two ways:
    • Manually: delete the old Weave with kubectl delete … --cascade=orphan, then apply the v1 one.
    • Through Helm or Flux: swap the apiVersion in one release. Helm, Flux's helm-controller and kustomize-controller all create new objects before pruning removed ones (checked in their source). A weft.run Weave deleted while a live v1 successor exists strips its own owner references and lets go, instead of tearing down. That existence check bypasses the cache, because a stale answer would mean a teardown.
  • Guards against mistakes:
    • A v1 Weave refuses objects a still-live predecessor manages, with the exact command to finish the move.
    • It waits while the predecessor is being deleted.
    • A v1 program that still uses weft.run/owned is refused, rather than owning (and later deleting) what it meant to keep.
  • Both groups are always served. The chart always installs both CRDs and grants access to both, and the controller always reconciles both. There's deliberately no switch to turn weft.run off, because turning it off while weft.run Weaves remain would strand them. It goes away when a future release removes it. reap covers both groups. docs/migrating.md is the guide for internal teams.

Upgrade fix found by a real Helm upgrade test:

  • A stored image digest kept the old binary. The publish job wrote the image digest into values.yaml, so --reuse-values kept the previous release's image under the new chart. The upgrade then reports success, but the old controller keeps running and serves no v1. (On kind, with an intermediate version of this branch, the old binary crash-looped on a since-removed flag.) The digest now ships in charts/weft/files/image-digest. Order of precedence: image.digest, then image.tag, then the published digest, then appVersion. The docs recommend --reset-then-reuse-values for the first upgrade from a prerelease.

Behaviour changes worth noting:

  • Per-Weave metrics gain a group label.
  • The weft.run controller is now named weave-weft-run in controller-runtime's own metrics; weave now means v1.
  • While both CRDs are installed, plain kubectl get weave is ambiguous between the two groups. I expect it resolves to v1, but haven't checked that on a cluster.
  • A user-set image.tag now takes effect on a published chart instead of being silently overridden by the pinned digest.

Checks

  • make generate produces no diff. Run as the equivalent controller-gen steps; make isn't available on the Windows machine this was written on.
  • make test passes, and the controller and chart suites actually ran rather than skipping. The full suite ran with -race in a golang:1.27.1 container against envtest 1.37.0; the controller suite has 54 tests, 8 of them new for the two groups and handover. The chart suite ran locally with Helm 4.3.
  • make lint is clean (golangci-lint v2.13.2 in a container).

End-to-end on kind (Kubernetes 1.37, Helm 4.3):

  1. Installed the published 0.1.0-pre.37 chart.
  2. Deployed two weft.run Weaves (ConfigMaps, a Secret, an unowned ConfigMap, a Deployment) from an app Helm chart.
  3. Upgraded Weft to this branch with --reuse-values (with an earlier revision that still had a toggle, since removed). Both groups were served, the old Weaves stayed Ready, and they were actively reconciled (an input edit propagated within about a second).
  4. Switched the app chart's apiVersion in one helm upgrade. With a real garbage collector running, every object kept its UID, ownership moved to the v1 Weaves, no weft.run labels, annotations or field managers were left, and the Deployment's pod never restarted.
  5. A first attempt at step 4 failed partway, on a Helm server-side-apply conflict I had caused. Helm had created the v1 Weaves and deleted nothing. The v1 Weaves refused the still-owned objects, and nothing was torn down.

Nothing here needs a +semver bump; the next version is still the first 0.1.0.

Does this touch the impersonation boundary?

Yes: internal/kube and the controller's RBAC.

  • RBAC: the controller gains the same three rules on weaves in weft.alethic.solutions that it already held in weft.run. It gains no access to anything a composition might generate.
  • Factory.For now takes the Weave's group. It only selects the field manager; the impersonated identity is still built solely from the Weave's own namespace and ServiceAccount.
  • New Client.TransferFieldOwnership. It runs on the impersonated client, so it needs patch on the object as that ServiceAccount, the same verb server-side apply already needs. It only rewrites metadata.managedFields entries for apply operations.
  • The handover's removal of owner references goes through the existing impersonated RemoveOwnerReference.
  • The only unimpersonated reads added check whether a Weave of the same name exists in the other group: a metadata-only get on weaves, which the controller already reads with its own identity. No composition data is read without impersonation.

So a Weave still cannot do more than its ServiceAccount can. The handover takes over only objects whose provenance names the same Weave, and anyone able to forge that provenance could already grant consent with the adopt annotation.

Rewrite the README for people meeting Weft for the first time: what it
is, a runnable quick start, a feature summary, install and verification,
and a documentation index. The behavioural detail and design rationale
the old README carried move into docs/operating.md (status conditions,
pruning, teardown, flags, metrics, uninstall) and docs/design.md
(comparison with other tools, why Starlark, non-goals), so nothing is
lost.

Install instructions in the README, chart README and demo no longer
assume there is no stable release: plain `helm install` resolves the
newest release, with --devel described for prereleases.

Move the API group from weft.run to weft.alethic.solutions before
anything is released under the old one. This changes the CRD name
(weaves.weft.alethic.solutions), the apiVersion, and every annotation,
label and finalizer key Weft writes. The group is still defined once in
internal/naming; the rest is generated output, markers, docs and
examples.
The previous commit renamed the API group outright, which would strand
every Weave in clusters that already run weft.run. Instead, serve both:
weft.alethic.solutions/v1 is the current API, and weft.run/v1alpha1
keeps working unchanged so existing installations can upgrade without
touching anything and move their Weaves across one at a time.

Kubernetes cannot convert between groups, so these are two CRDs. The
canonical types live in api/v1; api/v1alpha1 embeds v1's spec and
status so the schemas cannot drift, and a test asserts the generated
CRDs agree. The weft.run CRD is unchanged apart from descriptions.

internal/naming now describes each group, and every key a Weave writes
follows its own group: finalizers, provenance, the owned and adopt
annotations, and the server-side-apply field manager. The weft.run
identifiers are pinned by a test, because they are on objects that
predate v1. The leader election lease keeps its old name so a rolling
upgrade's old and new replicas share one.

The controller runs one reconciler per group, working on v1 internally
and converting the envelope at the API boundary. The watch registry,
metrics and kube client are group-aware, so a weft.run Weave and a v1
Weave of the same name are fully independent.

Moving a Weave is a delete with --cascade=orphan and a create. A v1
Weave takes over what its same-named weft.run predecessor made, in
place, once that predecessor is gone: it transfers field ownership from
the weft.run manager to its own, so its first apply prunes the old
provenance and anything else the program no longer sets. While the
predecessor exists it refuses with the exact kubectl command to finish
the move; while it is deleting it waits. A v1 program still using
weft.run/owned is refused rather than silently owning what it meant to
keep.

The chart installs both CRDs and grants both groups by default;
legacyAPI.enabled=false drops the CRD, the grant and the reconciler
together. reap covers both groups. docs/migrating.md describes the move.

Per-Weave metrics gain a group label, and the weft.run controller is
now named weave-weft-run in controller-runtime's metrics.
Deploying tools move a Weave between groups by creating the new object
and then deleting the old one. Helm, Flux's helm-controller and
kustomize-controller all apply first and prune after, with background
propagation, and none can orphan. The weft.run Weave being pruned would
then tear down everything it made, only for its v1 successor to
recreate it.

A weft.run Weave deleted while a live v1 Weave of the same name exists
now strips its own owner references, so garbage collection has nothing
to collect, releases its holds, and lets go. The successor takes each
object over through the existing handover. The existence check reads
past the cache, since a stale answer here is the difference between a
handover and a teardown, and an error is retried rather than treated as
absence.

The chart now renders when legacyAPI is missing from the values, as it
is under helm upgrade --reuse-values from a release that predates it,
and treats missing as enabled.
helm upgrade --reuse-values carries the previous release's values in
place of the new chart's defaults. The publish job wrote the image
digest into values.yaml, so a reused upgrade kept the previous
release's image under the new chart. Verified on kind: the
0.1.0-pre.37 binary exits on the new --serve-legacy-api flag, so the new
pod crash-loops while the old one keeps serving.

The digest now goes to files/image-digest, read by the image helper
after anything the user set: image.digest, then image.tag, then the
published digest, then the appVersion tag. A tag set by the user now
takes effect on a published chart instead of being silently overridden.

Prereleases already carry a digest in their stored values, so their
first reused upgrade needs --set image.digest=; docs/migrating.md says
so.
--reuse-values keeps the old chart's defaults, which for prereleases
include the image digest. --reset-then-reuse-values starts from the new
chart's defaults and re-applies only the user's overrides, which is
what keeping one's settings across an upgrade should mean. Also restore
a line continuation the previous edit lost.
The legacyAPI toggle let a cluster leave weft.run out, but turning it
off while weft.run Weaves remained would strand them: nothing would
reconcile them or release their finalizers. The saving - one unused
CRD and a few RBAC rules - is not worth that. Both groups are now
always installed, granted and reconciled, until a future release
removes weft.run altogether. The --serve-legacy-api flag goes with it.

With the flag gone, the --reuse-values digest problem no longer
crash-loops the old binary; it silently keeps the old controller
running, which serves no v1. The migration doc says so.
@wasabii
wasabii force-pushed the claude/readme-rewrite-release-311bea branch from 8de4fad to a9c34db Compare October 18, 2026 17:05
The line stripped carriage returns from GitVersion's output with a
literal CR byte between quotes. An earlier edit of the Makefile
normalised line endings and turned that byte into a newline, splitting
the line, so make refused to parse the file and every CI job that runs
make failed. Write it as the escape '\r', which tr understands and
which no editor will reinterpret.
govulncheck now reports nine vulnerabilities reachable from this code,
in net/http, crypto/tls, net/textproto and the HTTP/2 implementation in
both the standard library and golang.org/x/net. All are fixed in Go
1.27.2 and x/net v0.60.0, and CI's vulncheck fails until they are.
main pins the same versions, so it fails this check too.

Also brought forward while here:
- prometheus/client_golang 1.24.1 -> 1.25.0
- go.starlark.net to the latest commit
- golang.org/x/* to current releases (x/net 0.61.0)
- controller-gen 0.22.0 -> 0.23.0 (generated CRDs change only in the
  version annotation)
- setup-envtest 0.25.0 -> 0.25.2
- golangci-lint 2.13.2 -> 2.15.0 (no new findings)

The Kubernetes libraries and controller-runtime were already current.
Their own indirect dependencies (kube-openapi, structured-merge-diff,
k8s.io/utils, sigs.k8s.io/json) stay where Kubernetes 0.37.1 pins them:
the newer kube-openapi needs structured-merge-diff v7, which
apimachinery 0.37.1 does not build against.
@wasabii
wasabii merged commit 624b1bd into main Oct 18, 2026
11 checks passed
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.

1 participant