Canonical policy for the
hermes.agentAPI group. This document governs every change toHermesInstance,HermesSelfConfig, andHermesClusterDefaultsfor the lifetime of v1.x. Pull requests that change CRD types are reviewed against this document.
- API group:
hermes.agent. - Served versions in v1.x:
v1(storage = hub). - No
v1alpha1/v1beta1spoke. v1 ships as the only version. hermes.agent/v1will not have breaking changes for the lifetime of v1.x. A breaking change requireshermes.agent/v2plus a conversion webhook and at least 6 months of overlap (see §"Breaking changes").- New optional fields are non-breaking and may be added in any minor release. Required-field additions are breaking.
This policy covers:
- CRD schemas (the OpenAPI under
config/crd/bases/). - Status condition types and their reason codes (catalogued in
docs/conditions.md). - Validating- and defaulting-webhook semantics.
- RBAC verbs requested by the operator's
ClusterRole(additions are non- breaking; removals are breaking: see "RBAC changes" below).
It does not cover:
- Operator container image internals (Go package layout, controller implementation, log lines). These may change at any time.
- The Helm chart's
values.yamlschema. The chart has its own semver and its own changelog; chart breaking changes are flagged inCHANGELOG.mdwith thechart!:Conventional Commit prefix. - Metrics names and labels. These follow Prometheus convention (deprecation
notice on the metric
# HELPline for one minor release before rename).
Three independent versions, all semver:
| Surface | Version | Stability |
|---|---|---|
API group hermes.agent |
v1 |
This document. Stable for v1.x. |
| Operator image | vX.Y.Z (e.g. ghcr.io/paperclipinc/hermes-operator:v1.0.0) |
Semver. Breaking ops-surface changes (RBAC removals, metric renames) require a major bump. |
| Helm chart | vX.Y.Z (chart version) |
Semver of the chart itself, decoupled from appVersion. |
appVersion in the chart tracks the operator image version. The chart can
release patches independent of the operator image (e.g. fixing a template bug
without rebuilding the operator).
Allowed at any minor release. Listed exhaustively so reviewers have a checklist.
- New optional fields on any spec, marked
omitemptyand with a default that preserves prior behaviour. Example: addingspec.observability.tracing.enableddefaulting tofalse. - New conditions appended to
docs/conditions.md. Consumers MUST treat unknown condition types as informational. - New reason codes for an existing condition, as long as the prior reason codes remain valid for their original triggers.
- New status sub-trees (e.g. a new
status.tracingobject) populated alongside existing status fields. - New printer columns added to existing CRDs.
- New RBAC verbs added to the operator's
ClusterRole. The webhook never relies on permissions the operator does not hold, so widening is safe. - New webhook warnings (
admission.Warnings). Warnings are advisory and never block a request. - New defaults supplied by
HermesClusterDefaults, as long as the defaulter still only fillsnilfields. - New CRDs in the
hermes.agentgroup. Adding a CRD does not invalidate any existing CR. - Performance improvements to reconciliation (fewer API calls, smaller requeue intervals, additional indexes) that do not change observable state.
The following changes are breaking and may only land via a new served version
hermes.agent/v2. See "v2 plumbing" below for the conversion-webhook plan.
- Field removal from any spec. Removed fields must first be deprecated
per
docs/deprecations.md, kept for at least 2 minor releases AND 6 months, and then removed only in v2. - Semantic change to an existing field. Example: changing the default
of
spec.security.rootFilesystemReadOnlyfromtruetofalseis breaking even though the field name and type are unchanged. - Required-field addition. Adding a field with no default that the
reconciler dereferences is breaking even if the OpenAPI marks it
optional: if the operator panics onnil, that's a break. - Type change on any field (string → enum, int → string, etc.).
- Validation tightening that would reject an instance that previously validated. Loosening validation is non-breaking.
- Condition removal. Removing a condition type that previously was set is breaking because dashboards key off it.
- Condition semantic change: changing the meaning of a reason code without renaming it.
- RBAC verb removal. A v2 operator may need fewer permissions; the
chart's
ClusterRoleshrinking will break GitOps workflows that pin the role. - Finalizer rename. Existing CRs may carry the old finalizer; rename requires a migration path.
- CRD short-name removal (
hi,hsc,hcd). Users have these in scripts.
To make a future v2 cheap, v1.0 ships with conversion-webhook scaffolding even though there are no spokes yet. Concretely:
api/v1/hermesinstance_types.godeclaresHermesInstancewith the+kubebuilder:storageversionmarker.v1is both hub and storage.config/crd/patches/cainjection_in_hermes*.yamlandconfig/crd/patches/webhook_in_hermes*.yamlare committed. They are no-ops while there is only one version, but the kustomize overlay includes them so conversion plumbing is one PR away from being live.cmd/manager/main.goalready registers(&HermesInstance{}).SetupWebhookWithManagerwith both defaulter and validator. A future conversion implementation attaches to the same builder.Makefiletargetmake conversion-stubexists and produces a skeletonapi/v2/conversion.gowhenever a v2 is introduced.
When v2 lands:
api/v2/is added with the v2 types.v2is marked+kubebuilder:storageversion;v1keeps+kubebuilder:served=truebut losesstorageversion.api/v1/hermesinstance_conversion.goimplementsConvertTo/ConvertFromagainstv2.- The conversion webhook is enabled in the kustomize overlay.
- Both versions must serve in parallel for ≥ 6 months before v1 can be
marked
+kubebuilder:served=false. The 6-month clock starts at the first tagged release that serves v2.
Suppose in v1.4 we want to rename spec.runtime.python to
spec.runtime.interpreter.python (because we are adding runtime.interpreter.node
for a future hypothetical TypeScript backend). The migration path is:
- v1.4 (additive): introduce
spec.runtime.interpreter.pythonas an optional field. The defaulter populates it fromspec.runtime.pythonwhen the new field is nil and the old field is set. The validator emits aWarningif the user provides only the old field. Both fields are honoured. This is non-breaking because the old field is still served. - v1.4 release notes include a deprecation entry per
docs/deprecations.md: "spec.runtime.pythonis deprecated, target removal v2.0.0, no earlier than v1.4 release date + 6 months." - v1.5, v1.6: the deprecation persists. The warning becomes stronger if
adoption is slow (
WarningbecomesWarning + audit-log Event). - v2.0 (breaking):
api/v2/hermesinstance_types.goremovesspec.runtime.python. The conversion webhook reads v1 CRs with the old field set and produces a v2 object withruntime.interpreter.pythonpopulated. The reverse conversion (v2 → v1) writes the legacy field for compatibility with v1 clients. - v2.0 + 6 months (or v2.1, whichever is later):
v1is markedserved=false. The CRD still exists butkubectl apply -fagainst v1 returns a clear error pointing at the conversion webhook. - v2.x later:
v1is dropped from the CRD entirely. At this point all stored objects are v2 (the storage version flipped to v2 at v2.0.0, and any objects written since then are v2).
This is the only sanctioned shape for a breaking change. Reviewers reject PRs that try to "just rename it, it's a minor field": the contract is binding.
- Kubernetes API conventions: https://github.com/kubernetes/community/blob/master/contributors/devel/sig-architecture/api-conventions.md
- CRD versioning: https://kubernetes.io/docs/tasks/extend-kubernetes/custom-resources/custom-resource-definition-versioning/
- Kubebuilder conversion webhooks: https://book.kubebuilder.io/multiversion-tutorial/conversion.html
This policy itself follows the same rules: tightening (e.g. expanding the
list of breaking changes) requires a docs!: Conventional Commit, an entry
in CHANGELOG.md, and a discussion thread before merge. Loosening (more
things become non-breaking) is a plain docs: change.