Skip to content

Why the manifest is helvilette.naughtian.org/v1alpha1

helvilette.yml opens with two lines that look like boilerplate:

apiVersion: helvilette.naughtian.org/v1alpha1
kind: PlaybookDeployment

They used to say apps/v1 and Cluster. Both halves were wrong, in ways worth explaining, because the reasoning applies to anyone borrowing Kubernetes’ surface for a tool that is not Kubernetes.

The decision is recorded upstream as ADR-0002, which carries the full argument and the rejected alternatives.

The group belongs to whoever owns the domain

Section titled “The group belongs to whoever owns the domain”

Kubernetes apiVersion values come in two shapes. The core group is bare — v1, covering Pod, Service, ConfigMap, Secret. Everything else is <group>/<version>, where the group is required to be a DNS subdomain.

The short, domain-less groups that everyone recognises — apps, batch, policy, autoscaling — are not a pattern to copy. They are holdovers from Kubernetes 1.x, kept for compatibility, and the API conventions explicitly ask new groups to use a domain the project owns. The .k8s.io suffix is reserved for Kubernetes itself.

apps/v1 is worse than merely unfashionable: it is an occupied group. Deployment, StatefulSet, DaemonSet and ReplicaSet live there. Filing kind: Cluster into apps means putting a foreign kind inside someone else’s namespace.

The decisive evidence is k3s, because it had every excuse to do otherwise. It is a Kubernetes distribution. If any project could plausibly file its own kinds under a core group, it is the one that ships the core groups.

It does not. Every k3s-owned kind sits under a domain Rancher owns:

ProjectGroup for its own kinds
k3sk3s.cattle.io, helm.cattle.io
Fluxsource.toolkit.fluxcd.io, kustomize.toolkit.fluxcd.io
Argo CDargoproj.io
cert-managercert-manager.io
Prometheus Operatormonitoring.coreos.com
Cluster APIcluster.x-k8s.io

Six projects, one rule, no exceptions: your kinds go under your domain. Helvilette follows it.

Helvilette got this half right the first time and had to correct it.

The original group was helvilette.io, which looks correct: it is a DNS subdomain, it is not an occupied Kubernetes group, and it names the project. It was also a domain nobody here had registered. Reading the convention as “use a DNS-shaped string” instead of “use a domain you own” leaves the group squatting on a name someone else can buy.

The group is now helvilette.naughtian.org, a subdomain of the domain the project actually owns. That matches what every project in the table above does: Rancher owns cattle.io, Argo owns argoproj.io, and their groups sit underneath.

The practical risk of the old name was low, because Helvilette manifests are never submitted to a Kubernetes API server and therefore cannot collide with a real CustomResourceDefinition. The group functions as a namespace label rather than a registered API surface. Low risk is a reason to fix something cheaply while the project is still alpha, which is what happened.

This is the half that is easiest to get wrong, because v1 looks like a starting point rather than a commitment.

In Kubernetes the suffix is a contract:

LevelWhat it promises
v1alpha1May change or disappear without notice
v1beta1Enabled by default, may still change, with a conversion path
v1Backward compatible for the lifetime of v1

Helvilette cannot honour the third. Two sections — spec.vault and nodeGroups[].probes — appear in the project’s own example manifests with no parser behind them, and the schema has already been rewritten once since it was first proposed. Declaring v1 would promise stability the manifest has not earned.

v1alpha1 states the true situation and leaves room for those sections to land without breaking anyone. The reference page lists exactly what is not yet real.

kind names the object a manifest declares. That is the whole job of the field.

helvilette.yml lives inside a playbook repository and declares which playbook is deployed to which node groups with which variables. That is not a cluster. In Cluster API, cluster.x-k8s.io/v1beta1 kind: Cluster means an actual cluster resource — a real thing with a lifecycle, not a deployment description.

PlaybookDeployment names what the file does. It is longer, and it is self-explanatory to somebody reading a manifest for the first time, which is the case that matters.

Naming is only half the story. Before this change, nothing read either field. apiVersion was declared in the Go types and never referenced anywhere in the codebase, and the parser unmarshalled YAML with no validation at all.

The symptom was three different spellings of the schema identity coexisting in one repository — apps/v1/Cluster in the example manifests, v1/Helvilette in the parser’s own tests, helvilette.io/v1/Blueprint in the original design issue. They coexisted precisely because no code ever compared them.

The consequence was worse than untidiness. A manifest with a stale apiVersion, or with nodegroups misspelled, unmarshalled cleanly into an empty object. It matched no node. Every agent received 204 No Content and kept polling contentedly. There was no error, no warning, and no log line naming the file.

That is the same failure shape as the configuration precedence problem: something explicit and version-controlled, read successfully, doing nothing, with no signpost pointing at the cause. Both were resolved on the same principle — the expensive failure is the silent one, so make it loud and make it early.

Renaming without enforcing would have produced a fourth spelling and fixed nothing.

Every existing manifest had to be updated. The project was pre-release, so the bill was three files, and the group, version and kind moved together in one breaking change rather than three.

It was then spent a second time on the domain correction, which is the honest cost of having read the convention too loosely the first time. Both moves landed while the project is alpha and the manifest count is small, which is exactly when a schema identity is cheap to change and exactly why v1alpha1 is the right level to be at.

The failure mode for anyone who misses either update is a rejection message naming both the value found and the value expected — which is the cheapest possible way to discover a schema change.