Changelog¶
All notable changes to this project will be documented in this file.
The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.
Unreleased¶
Changed¶
- E2E test suite optimization — CI e2e tests now run faster; see
docs/development/testing.md(and.claude/TESTING.mdfor AI guidance) for details.
Added¶
-
Admission-time validation for colliding automation/scene/script identifiers, and a recorder-configuration warning —
HomeAssistantAutomation,HomeAssistantScene, andHomeAssistantScripteach gained their own validating webhook: creating or updating a resource whose effective identifier (spec.id, ormetadata.namewhenspec.idis left empty) collides with a sibling of the same kind already targeting the sameHomeAssistantinstance is now rejected immediately, naming the conflicting resource. Previously this was completely undetected — the two resources would silently overwrite each other in Home Assistant'sautomations.yaml/scenes.yaml/scripts.yaml, both still reportingReady. Separately,HomeAssistantConfigurationnow warns (without rejecting) whenspec.recordersets bothdatabaseanddatabaseSecretRef, namingdatabaseSecretRefas the value that takes effect — this precedence already existed, but was previously invisible at apply time.HomeAssistantAutomation.spec.idandHomeAssistantScene.spec.idare now restricted to the same safe character set (^[a-z][a-z0-9_]*$)HomeAssistantScript.spec.idalready enforced, closing a three-way inconsistency between the sibling kinds. This is a breaking change for existing resources: an already-appliedHomeAssistantAutomationorHomeAssistantScenewhosespec.idcontains uppercase letters or hyphens keeps working after the CRD upgrade, but its next update (even an unrelated field) will be rejected by the tightened schema untilspec.idis renamed to a conforming value — rename any such identifiers before upgrading, or be prepared to do so before the next edit. All new checks are best-effort (failurePolicy: Ignore), matching the existingHomeAssistantwebhook, and require no new RBAC permissions. -
Device passthrough for Zigbee/Z-Wave coordinators (alpha) — new
spec.alpha.devicesfield lets aHomeAssistantresource declare host device nodes (e.g./dev/ttyACM0for a Conbee2/SkyConnect/Z-Wave USB coordinator) to mount into the Home Assistant container, so integrations like Zigbee2MQTT, Z-Wave JS, or ZHA can open the serial port — previously this required a manual StatefulSet edit that the operator would silently revert on the next reconcile. Each entry (hostPath, optionalcontainerPath) is mounted as ahostPathvolume typed as a character device; the operator never setsprivileged: truefor this — the container already runs with enough default capability to open a root-owned device node. A resource declaring no devices is completely unaffected (no security-context or volume changes). Malformed paths (empty, not rooted under/dev, containing..) and duplicatehostPathentries are rejected by the validating webhook before ever reaching a pod. When a declared device is missing on the node the pod is scheduled to, the newDevicesReadystatus condition names the offending path, diagnosable directly fromkubectl describe homeassistant. This changes the pod's security context, so it starts underspec.alpha; it does not itself pin the pod to the right node — use the newspec.scheduling.nodeSelector(see below) for that. -
Default trusted proxies for Ingress/Gateway exposure — when
spec.ingress.enabledorspec.gateway.enabledistrue, the operator now automatically injectshttp.use_x_forwarded_for: trueandhttp.trusted_proxies(the RFC1918 ranges10.0.0.0/8,172.16.0.0/12,192.168.0.0/16) into the generatedconfiguration.yaml, so Home Assistant no longer rejects every request through the exposed endpoint with400 Bad Requestuntil these are set by hand. No defaults are injected while exposure is disabled. The real cluster pod/service CIDR can't be reliably read from the Kubernetes API, so this is a conservative, commonly-correct default rather than autodetection. Each key is added independently and only when missing — a value you already set yourself inHomeAssistantConfigurationis never overridden, and an externally managedhttp:block (e.g.http: !include http.yaml) is left completely untouched — and the newspec.disableDefaultTrustedProxiesfield opts aHomeAssistantinstance out entirely for clusters where the default doesn't apply (for example, to set narrowertrusted_proxiesmatching your actual Ingress/Gateway proxy instead of the broad RFC1918 ranges). Injecting, changing, or removing these keys follows the existing hot-reload classification (no forced pod restart), and theHomeAssistant'sExposureReadycondition message reports which of the three states applies (defaults applied / user-configured / opted out). -
Gateway API route filters (
spec.gateway.filters) — theHomeAssistantCRD'sspec.gatewaynow supports declaring HTTP route-level behaviors on the operator-managedHTTPRoute: request/response header modification, redirects (e.g. enforcing HTTPS), and URL rewrites — mirroring upstream Gateway API's ownHTTPRouteFilterfield names/shape so existing Gateway API knowledge transfers directly (RequestMirrorandExtensionRefare intentionally out of scope). Filters are validated at admission time by the existing validating webhook: an unknown filter type, a missing or mismatched sub-object for the declared type, or an all-empty filter are all rejected with a message naming the problem, before ever reaching the cluster. Changing filters never restarts the Home Assistant pod (route exposure is fully decoupled from the pod template hash), and omittingfiltersentirely leaves the managed route byte-for-byte unchanged from today's behavior. -
Pod scheduling controls (
spec.scheduling) — new field lets aHomeAssistantresource declarenodeSelector,affinity(node and pod affinity/anti-affinity — one field, matching Kubernetes' ownAffinityshape),tolerations, andpriorityClassName, all copied verbatim onto the generated pod template. Closes the node-placement gap left open byspec.alpha.devices: a USB Zigbee/Z-Wave coordinator only exists on one specific node, and until now nothing kept the pod from being scheduled elsewhere. Every field is optional; a resource that sets none of them continues to schedule exactly as before. Editing a scheduling field on an already-running instance triggers a pod recreation so the change actually takes effect (Kubernetes only evaluates these fields at pod creation). A newSchedulingReadystatus condition mirrors the pod's own built-inPodScheduledcondition, so an unsatisfiablenodeSelector/affinitycombination is diagnosable directly fromkubectl describe homeassistant.priorityClassNameis validated against realPriorityClassobjects at admission time — a nonexistent one is rejected immediately rather than surfacing later as an opaque StatefulSet failure. -
HACS-compatible community repository installs (alpha) — new
HomeAssistantCommunityRepositoryCRD (ha.homeassistant.io/v1alpha1, short namehacr) declaratively installs community extensions the same way HACS does, without requiring HACS or its UI to be installed. Supports the 5 HACS categories that don't require a separate runtime:integration,plugin,theme,python_script, andtemplate(appdaemon/netdaemonare out of scope — they need their own runtime container this operator does not deploy).spec.repository(owner/repo) andspec.ref(tag/branch/commit, explicitly pinned — no "latest" tracking) are fetched directly from GitHub (codeload.github.com, nogitbinary or authenticated REST API dependency) and validated against the category's expected HACS structure before anything is installed. Conflict detection is stronger than HACS's own: two resources that would resolve to the same(category, resolvedTarget)on the sameHomeAssistantinstance are rejected rather than silently overwriting each other. Activation matches HACS's own per-category mechanism — a pod restart only forintegration(needed because Python components are imported at HA startup), hot-reload via HA's own service calls fortheme/python_script/template, and Lovelace resource registration forplugin(with apluginYAML-dashboard-mode edge case reported informationally, not as a failure, since HA itself has no API to register a resource in that mode). A failed update never breaks the previously working installation:status.installedVersiononly changes once the new version is fully confirmed active. Files are materialized inside the Home Assistant pod by a new init-container (integrationonly, since it must be present before HA starts) and a lightweight sidecar (the other four categories, polling every ~30s) — both reuse the existing HA image and are only added to the pod spec when at least oneHomeAssistantCommunityRepositoryactually targets that instance, so the change is invisible to every existing installation. As with allspec.alpha/v1alpha1surfaces, this CRD carries no API stability guarantee between releases.
v1.2.0 - 2026-07-24¶
Added¶
- Helm chart is now generated from a single source of truth — the chart's static parts (all 10 CRDs and the operator RBAC rules) are generated from the authoritative Kustomize sources under
config/viamake helm-sync, so the Helm and Kustomize install paths can no longer physically drift. New CI gates (make helm-verify) block a PR that leaves the chart out of sync (verify-chart-sync), that lets the two render paths diverge on RBAC / securityContext / PSA labels (verify-equivalence), or that broadens the operator's RBAC versus the previous release without an explicit justification inhack/rbac-allowlist.txt(verify-rbac-upgrade, enforcing the minimal-RBAC principle). -
Helm chart test layers — the chart now ships with
values.schema.json(install-time validation ofvalues.yamltypes/enums),helm-unittesttemplate unit tests undercharts/homeassistant-operator/tests/, an auto-generated parameter table inREADME.mdviahelm-docs(with a CI drift gate), a k3d end-to-end job covering a fresh install and an upgrade from the previous release (N-1 → latest, including the explicit CRD apply step), and a post-publish smoke test that installs the exact published OCI artifact and, on failure, raises a maintainer alert and marks the release "needs verification" without yanking the artifact. See the new.github/workflows/test-helm.ymland the##@ Helmtargets in the Makefile. -
cert-manager TLS integration — foundation (opt-in) — groundwork for issuing TLS certificates via cert-manager for three modes: native TLS in Home Assistant, Ingress / API Gateway exposure, and the operator's webhook. New opt-in API fields land as
spec.ingress.tls.issuerRefandspec.gateway(enabled,host,issuerRef,secretName,parentRef,manageGateway) at the stable top level, plusspec.alpha.tls.native(enabled,issuerRef,dnsNames,secretName) underspec.alpha— native TLS changes how Home Assistant serves traffic and how the operator connects to it, so it follows the experimentalspec.alphalifecycle, while Ingress/Gateway only manage sibling routing resources and are stable opt-ins. All modes share anIssuerReference(the operator only references an existingIssuer/ClusterIssuer, never creates issuers). The operator now detects cert-manager at runtime (via the API RESTMapper, cached with a 60s TTL) and never installs it and never requires it: when a TLS mode is requested but cert-manager is absent, the resource reportsCertManagerAvailable=False/TLSReady=Unknown, emits aCertManagerUnavailableevent, and keeps Home Assistant fully functional over HTTP without erroring or looping — a later cert-manager install is picked up automatically. Narrow RBAC forcert-manager.io/certificates,networking.k8s.io/ingresses, andgateway.networking.k8s.io/{httproutes,gateways}was added. Implemented on top of this gate: cert-managerCertificateprovisioning for all three modes; native TLS runtime (Secret mounted into the HA pod at/config/ssl,http.ssl_certificate/ssl_keyinjected, and the operator→HA connection switched to HTTPS withca.crttrust — neverInsecureSkipVerify, all gated on a single readiness predicate so HA and the operator flip together); operator-managed Ingress and Gateway API (HTTPRoute/Gateway) exposure; and a validating admission webhook, enabled by default (opt-out), whose serving certificate is self-managed by the operator (self-signed, auto-rotated, CA injected into its ownValidatingWebhookConfigurationvia cert-controller) — so the webhook needs no cert-manager; cert-manager can still issue the serving certificate as an opt-in (webhook.certManager.enabled=true). Cluster end-to-end validation is the remaining follow-up. Thespec.alpha.tls.nativefield is off by default and may change or be removed without a deprecation notice.
Security¶
-
Signed releases (keyless, Sigstore/cosign) — starting with v1.2.0, the release pipeline signs the container image (full multi-arch manifest), the Helm chart OCI artifact, and a
checksums.txtbundle attached to a newly-created GitHub Release for each version tag. Signing is keyless: it uses the release workflow's own GitHub Actions OIDC identity via Sigstore (Fulcio short-lived certificates + a public Rekor transparency-log entry) — no long-lived key is generated, stored, or rotated by the maintainer. Verification needs only public information; see the Signed Releases guide for thecosign verify/verify-blobcommands andhack/verify-signatures.shfor a one-command local check. A sample KyvernoClusterPolicy(hack/kyverno/verify-homeassistant-operator-image.yaml, tested viakyverno testin CI) lets cluster operators enforce this at admission time for the container image; this is entirely opt-in and does not change the default Helm chart install. Releases published before v1.2.0 are not signed. -
Hardened the operator container securityContext in the Kustomize path —
config/managernow setsreadOnlyRootFilesystem: true,runAsUser: 65532, andrunAsGroup: 65532on the controller-manager container, matching the hardening the Helm chart already applied. This was surfaced by the new Kustomize↔Helm equivalence gate, which found the Kustomize install path shipped a weaker container securityContext than Helm. -
Opt-in NetworkPolicy for the Home Assistant pod (alpha) — new
spec.alpha.networkPolicy.enabledfield (defaultfalse). When enabled, the operator creates aNetworkPolicythat restricts ingress to the HA pod to the same namespace and the operator's own namespace on the Service port, while leaving egress unrestricted (HA needs broad, unpredictable egress to IoT devices, cloud APIs, and MQTT brokers). The operator only managesNetworkPolicyobjects it owns (via controller reference), so a pre-existing policy of the same name is left untouched. The operator-namespace ingress peer is added only when the controller knows its own namespace via theOPERATOR_NAMESPACEenv var (set by the shipped manifests); otherwise it is omitted with a warning. Being anspec.alphafeature, it is off by default and may change or be removed without a deprecation notice. -
Restricted Pod Security Standard enforced on the operator namespace — the shipped manifests now label the operator's own namespace with
pod-security.kubernetes.io/{enforce,audit,warn}=restricted(versionlatest), so the controller-manager pod runs under and is enforced at the strictest Pod Security profile. The pod'ssecurityContextalready satisfiedrestricted(runs as non-root,seccompProfile: RuntimeDefault,allowPrivilegeEscalation: false, all capabilities dropped); this change adds enforcement plus amake verify-pssCI check — covering both the Kustomize (config/default) and Helm render paths — that fails if either regresses. For Helm, enforcement is opt-in via the newnamespace.create=truevalue (install without--create-namespaceso the chart owns and labels the namespace); the operator pod is restricted-compliant regardless. Scope is the operator only — Home Assistant workload pods run in their own namespaces and are unaffected (they often need elevated privileges such ashostNetworkor USB/Zigbee device access thatrestrictedwould block). On clusters without Pod Security Admission the labels are inert, so installation never fails.
v1.1.0 - 2026-07-05¶
Added¶
-
Helm chart:
watchNamespaces— newvalues.yamlfield (default[]) restricts the operator to watching only the listed namespaces. When set, generates per-namespaceRoleBindingobjects instead of a cluster-wideClusterRoleBinding, reducing the operator's blast radius to only the namespaces it needs. SetWATCH_NAMESPACESenv var is injected automatically. Backwards compatible — empty list (default) preserves the existingClusterRoleBindingbehaviour. -
IP ban self-recovery via init-container — when the operator's IP is banned by Home Assistant (HTTP 403), the operator now deletes the HA pod so it restarts with a new
unban-operator-ipinit-container. The init-container (using the same HA image already cached on the node) runs an idempotent Python script that removes the operator's IP from/config/ip_bans.yamlbefore HA starts. Nopods/execRBAC permission needed. Sliding window protection: at most 3 pod restarts within 30 minutes; once the limit is reached theBanRecoveryFailed=Truecondition is set and manual intervention is required. The window resets automatically on a successful HA connection. Requires thePOD_IPdownward-API env var on the operator Deployment (set by default in the Helm chart).
Security¶
-
Removed
pods/execRBAC permission — the operator no longer requirescreateonpods/exec. The unban flow uses pod deletion + init-container instead of exec-into-pod. Runmake manifeststo regenerateconfig/rbac/role.yaml. -
Narrowed Secret RBAC — removed unused
patchverb from thehomeassistantsecretscontroller; moveddeleteexclusively to thehomeassistantconfigurationcontroller which is the only one that deletes Secrets. All operator-managed Secrets now carry theapp.kubernetes.io/managed-by: homeassistant-operatorlabel for auditing and Kyverno policies.
Deprecated¶
- ClusterRoleBinding mode (
watchNamespaces: []) — deprecated since v1.1.0, planned removal in v2.0.0. See DEPRECATIONS.md for migration instructions.
v1.0.1 - 2026-06-21¶
Changed¶
- Dependency upgrades — Go 1.26.4 (fixes CVEs GO-2026-5037/5038/5039),
controller-runtimev0.24.1,k8s.io/*v0.36.2,ginkgo/v2v2.31.0,gomegav1.42.0,golang.org/x/netv0.55.0 (fixes GO-2026-5026),actions/checkoutv7.
v1.0.0 - 2026-06-11¶
Breaking Changes¶
-
API promoted from
v1alpha1tov1— all CRDs now useapiVersion: ha.homeassistant.io/v1. Existing resources must be migrated:Affected kinds:kubectl get <resource> -o yaml | sed 's|ha.homeassistant.io/v1alpha1|ha.homeassistant.io/v1|' | kubectl apply -f -HomeAssistant,HomeAssistantSecrets,HomeAssistantConfiguration,HomeAssistantAutomation,HomeAssistantScene,HomeAssistantScript,HomeAssistantIntegration,HomeAssistantFloor,HomeAssistantLabel,HomeAssistantArea. -
api/v1alpha1package removed — Go import path changed fromgithub.com/przemekhys/homeassistant-operator/api/v1alpha1togithub.com/przemekhys/homeassistant-operator/api/v1.
Changed¶
-
API naming consistency — Go field names now follow acronym conventions (
CreateAPIToken,APITokenSecretName,HTTPConfig,MQTTConfig). JSON/YAML wire format is unchanged (createApiToken,http,mqtt). -
ObservedGenerationadded to all status structs — all CRDs now surfacestatus.observedGenerationwith the last reconciled generation number. -
LastErroradded to all status structs — all CRDs now surfacestatus.lastErrorwith a human-readable error description, eliminating the need forkubectl describe. -
Condition type standardized to
Ready—HomeAssistantIntegrationpreviously used condition typeIntegrationReady; now usesReadyconsistent with all other CRDs. Existing conditions are migrated automatically on first reconcile.
v0.10.1 - 2026-04-26¶
Fixed¶
-
Helm chart OCI path collision —
helm pushwas publishing the chart tooci://ghcr.io/przemekhys/homeassistant-operator, overwriting the Docker image tag. Chart is now published tooci://ghcr.io/przemekhys/charts/homeassistant-operator. Install command updated accordingly. -
runAsNonRootadmission failure — containersecurityContextnow explicitly setsrunAsUser: 65532andrunAsGroup: 65532, eliminating reliance on image manifest UID resolution which failed on some k3s versions. -
Go stdlib CVEs — upgraded Go 1.26.0 → 1.26.2, fixing 10 vulnerabilities in
crypto/x509(GO-2026-4947, GO-2026-4946, GO-2026-4866, GO-2026-4600, GO-2026-4599),crypto/tls(GO-2026-4870),html/template(GO-2026-4865, GO-2026-4603),os(GO-2026-4602), andnet/url(GO-2026-4601).
Added¶
-
Helm chart:
priorityClassName— newvalues.yamlfield (default"") setspriorityClassNameon the operator Deployment. Previously required apostRenderersJSON patch workaround in Flux HelmRelease. -
Helm chart:
topologySpreadConstraints— newvalues.yamlfield (default[]) for spreading operator pods across failure domains. Includes commented example for zone-based spreading. -
Helm chart:
nodeSelectorandaffinityexamples —values.yamlnow includes commented examples for ARM64 node pinning (kubernetes.io/arch: arm64) and pod anti-affinity across nodes.
Dependencies¶
k8s.io/apimachinery0.35.4 → 0.36.0k8s.io/streaming0.36.0 (new transitive dependency of apimachinery 0.36.0)actions/checkoutv4 → v6actions/setup-gov5 → v6actions/setup-pythonv5 → v6
v0.10.0 - 2026-04-22 [YANKED]¶
This release is yanked. The Helm chart OCI artifact overwrote the Docker image tag — installing via
helm installpulled a Helm chart manifest instead of a runnable container image, causing pods to fail with/manager: no such file or directory. Use v0.10.1 instead.
Security¶
- CVE-2026-33186 — upgraded
google.golang.org/grpcv1.78.0 → v1.80.0 (fixes gRPC-Go authorization bypass via malformed HTTP/2:pathpseudo-header; operator does not expose a gRPC server so not directly exploitable, but upgraded as a precaution). - CVE-2026-39883 — upgraded
go.opentelemetry.io/otelv1.40.0 → v1.43.0 (fixes OpenTelemetry-Go path traversal via untrustedkenvsearch path on BSD/Solaris; not exploitable on Linux, upgraded as a precaution).
Added¶
-
jsonValuefield forHomeAssistantIntegration— new field inIntegrationValuethat accepts a JSON-encoded string and submits it as a native JSON object to the Config Flow API. Fixes integrations likeopenweathermapthat require dict-type fields (e.g.location: {"latitude": 54.17, "longitude": 18.55}). Example:location: { jsonValue: '{"latitude": 54.17, "longitude": 18.55}' }. -
Documentation site — MkDocs Material documentation deployed to GitHub Pages (
https://przemekhys.github.io/homeassistant-operator/). Includes getting started guides, CRD API reference auto-generated from Go type comments (make docs-api), changelog auto-included fromCHANGELOG.md, Contributing and Testing developer guides. New Makefile targets:make docs-serve,make docs-build,make docs-api.README.mdsimplified to a short landing page pointing to the full docs. -
Helm chart —
charts/homeassistant-operator/provides a Helm chart for installing the operator. CRDs are bundled incrds/(installed automatically by Helm). Configurable viavalues.yaml: image, replicas, resources, nodeSelector, tolerations, affinity, serviceAccount. Published to OCI registry on each release:helm install homeassistant-operator oci://ghcr.io/przemekhys/homeassistant-operator --version <version>. New Makefile targets:make helm-lint,make helm-package,make helm-push. -
Auto-unban operator IP from HA
ip_bans.yaml— when HA returns 403/429 (operator IP banned after too many failed login attempts), the operator automatically execs into the HA pod, removes its IP from/config/ip_bans.yaml, and deletes the pod so StatefulSet recreates it (clears in-memory bans). Limits: at most 5 unbans total, 5-minute cooldown between each. Once the limit is reached aSelfUnbanLimitReachedwarning event is emitted and manual intervention is required. New status fields:selfUnbanCount,lastSelfUnban. New event reasons:SelfUnbanned,SelfUnbanFailed,SelfUnbanLimitReached. RequiresPOD_IPenv var (injected via downward API) and new RBACpods/exec create.
v0.9.0 - 2026-04-17¶
Added¶
-
spec.recorder.databaseSecretReffor HomeAssistantConfiguration — database URL is read from a K8s Secret and mounted into the HA pod as a file;configuration.yamlreferences it viadb_url: !include recorder_db_url.yamlso credentials are never written to a ConfigMap. Takes precedence overspec.recorder.databasewhen both are set.spec.recorder.enabled: falseskips injection and cleans up the mounted Secret.purgeKeepDaysis also injected when set. The injection usesyaml.Nodeso!include/!secrettags in other sections are preserved. -
API readiness gate in bootstrap —
CheckAPIReady(GET/api/configwithout auth) is now called between health check and onboarding status check. Returns 401 when HA routes are fully loaded, 404 during startup (same as/api/onboarding). This eliminates the ambiguity that required the 10-minute confirmation window.
Changed¶
-
onboardingConfirmDelayreduced from 10 minutes to 30 seconds — now thatCheckAPIReadygates the bootstrap flow, a 404 from/api/onboardingis only seen after the API is fully loaded, making it a reliable signal. The 30-second window remains as a safety net for edge cases. -
loggeradded to hot-reloadable config sections —loggerwas documented as hot-reloadable but missing fromreloadableSectionsmap, causing the controller to trigger a full pod restart instead of callingReloadCoreConfigwhen it was added or modified.
Fixed¶
-
Bootstrap infinite loop on slow CI runners — two interacting bugs caused
LoginRecoveryAttemptsto never advance past 1, trapping bootstrap in an infiniteLoginNoUsercycle. (1) The "first seen OnboardingDone" handler resetLoginRecoveryAttempts = 0on every cycle, undoing any progress. (2)LoginNoUser(type=form) is a transient startup race where onboarding routes have not yet registered — it is not a credential failure and should not count toward the retry limit. Fixed: the counter is no longer reset in the first-seen handler, andLoginNoUserdoes not increment it. The loop now continues until onboarding routes load andCheckOnboardingStatusreturns nil. -
E2E critical path: wrong Service name — test looked up
<ha-name>-homeassistantbut the controller creates the service as<ha-name>. Fixed to use the correct name. -
E2E critical path: wrong PVC name — test looked up
data-<ha-name>-0(StatefulSet volumeClaimTemplate convention) but the controller creates a standalone PVC named<ha-name>-data. Fixed to use the correct name. -
E2E critical path: script ID with hyphen rejected by HA —
HomeAssistantScriptCR namedcp-scriptused the CR name as the HA script ID whenspec.idwas not set. HA rejects script IDs containing hyphens (valid format:[a-z0-9_]+). Added explicitid: critical_path_script. -
E2E critical path: debug info not collected on failure —
AfterAllcheckedCurrentSpecReport().Failed()which returns the state of the lastItblock (Backup — passed), so debug info was never collected even when earlier tests failed. AddedsuiteFailedflag updated byAfterEach.
v0.8.0 - 2026-04-07¶
Added¶
-
HomeAssistantFloor CRD (
hafloor,hafl) — declarative management of Home Assistant floors via WebSocket registry API (config/floor_registry/*). Supportsname,level, andicon. Adopts existing floors by name, finalizer-based cleanup on deletion. -
HomeAssistantLabel CRD (
halabel,halb) — declarative management of Home Assistant labels via WebSocket registry API (config/label_registry/*). Supportsname,icon, andcolor. Adopts existing labels by name, finalizer-based cleanup on deletion. -
HomeAssistantArea CRD (
haarea,haar) — declarative management of Home Assistant areas via WebSocket registry API (config/area_registry/*). Supportsname,icon,floorName(resolved tofloor_idat reconcile time), andlabels[](resolved tolabel_ids). Requeues withFloorNotFoundif referenced floor doesn't exist; missing labels produce a warning but don't block creation. -
SendWebSocketCommandhelper in haclient — reusable one-shot WebSocket command pattern (connect → auth → command → response → close) used by Floor/Label/Area controllers. RefactoredCreateLongLivedTokento use the same helper. -
spec.backupfor HomeAssistant CR — declarative configuration of Home Assistant's built-in backup system via WebSocket API (backup/config/info,backup/config/update). Supports schedule (daily, per-day-of-week, never), time, retention (copies/days), and database inclusion. Requires bootstrap with API token enabled. - Idempotent: compares current HA config with desired state, only updates when drift detected
- Condition:
BackupConfiguredwith reasons:BackupConfigured,BackupConfigFailed,TokenNotAvailable - Events:
BackupConfigured(Normal),BackupConfigFailed(Warning)
Fixed¶
-
Bootstrap fails when onboarding already completed —
CheckOnboardingStatusdid not handle HTTP 404 (which HA returns when onboarding is fully done, as the endpoint is unregistered). Also, when onboarding was already done, the operator tried to delete the pod (which doesn't reset PVC data) instead of logging in with credentials. Fixed: 404 is now correctly detected as "onboarding done", partial onboarding (user step done) is detected from the step array, andhandleOnboardingAlreadyDonenow logs in via HA's auth flow and creates the API token instead of deleting the pod. -
Bootstrap pod delete forbidden — RBAC ClusterRole was missing
deleteverb onpodsresource, causing bootstrap to fail with "pods is forbidden" when the operator tried to restart the HA pod after onboarding. -
!secretYAML tags stripped during location injection —injectLocationusedmap[string]interface{}for YAML round-trip, which discards custom tags like!secretand!include. Switched toyaml.Nodetree which preserves all YAML tags through the unmarshal/marshal cycle. Only affected configs withspec.bootstrap.locationset. -
Bootstrap missing
integrationonboarding step —PerformBootstrap()only completed 3 of 4 required HA onboarding steps (user,core_config,analytics), leaving outintegration. This caused non-admin users to be blocked from accessing HA's websocket API and redirected to/onboarding.html. -
Config Flow
descriptionas object —FlowField.Descriptionwas typed as*string, but some integrations (e.g. OpenWeatherMap) return it as an object ({"suggested_value": "..."}), causing JSON unmarshal errors. Changed tojson.RawMessageto accept both formats. -
Enable automation 404 on HA 2025.x+ — the operator called
POST /api/config/automation/config/{id}/enableafter every PUT, but this endpoint no longer exists in HA 2025.x/2026.x. Automations created via API are enabled by default. Removed theEnableAutomationcall; onlyDisableAutomationis used whenspec.enabled: false. -
Bootstrap startup race: false-positive onboarding 404 — on slow clusters (CI, resource-constrained environments), HA's HTTP server becomes healthy before the onboarding component registers its routes, causing
/api/onboardingto return 404 transiently. The controller now uses a time-based 10-minute confirmation window (OnboardingDoneFirstSeen) and re-polls every 30 seconds instead of trusting a single 404. If onboarding routes register during the window, normal bootstrap proceeds without disruption. -
Bootstrap stuck in
LoginRecoveryFailedwhen onboarding was never completed — when the 10-minute confirmation window elapsed but/api/onboardingwas still transiently unavailable, login recovery ran and failed withtype=form(HA's auth flow returns this when no user exists). The controller now detectstype=formas a signal that onboarding was never completed, resetsOnboardingDoneFirstSeen, and restarts the confirmation window.LoginRecoveryAttemptsis preserved across resets to limit total retries tomaxLoginRecoveryRetries(3), preventing infinite loops on genuine credential errors.
Changed¶
- Bump
k8s.io/api,k8s.io/apimachinery,k8s.io/client-gofrom v0.35.2 to v0.35.3
v0.7.1 - 2026-03-19¶
Fixed¶
auto_includestrips!includetags after YAML round-trip — wheninjectLocationre-serialisedconfiguration.yaml, the!includeYAML tag was lost (e.g.automation: !include automations.yamlbecameautomation: automations.yaml). HA treated the bare filename as a literal string and disabled all automations.ensureAutoIncludesnow detects bare filenames and restores the!includedirective in-place.
v0.7.0 - 2026-03-17¶
Added¶
- HomeAssistantIntegration CRD (
haint) — declarative management of Home Assistant integrations via Config Flow API. Does not deploy containers — only registers integrations in HA. - Supports single-step Config Flows (MQTT, ESPHome, and others)
spec.configurationfields support plain text values andsecretKeyRefreferences to K8s Secrets- Adopt pattern: if integration already exists in HA (e.g. configured via UI), operator adopts the existing
entryIDwithout reconfiguring - Day-2 reconfiguration: changing
spec.configurationtriggers delete + re-create of the config entry - Finalizer-based cleanup: removes config entry from HA on CR deletion (best-effort)
- Events:
IntegrationConfigured,IntegrationAdopted,IntegrationReconfigured,IntegrationRemoved,IntegrationFailed - Condition:
IntegrationReadywith reasons:IntegrationConfigured,AlreadyConfigured,TokenNotAvailable,HANotReady,ConfigFlowFailed,SecretResolutionFailed spec.hostNetworkfor HomeAssistant CR — enables host networking for IoT device discovery (mDNS/SSDP/DHCP). When enabled, setshostNetwork: trueanddnsPolicy: ClusterFirstWithHostNeton the pod.- Config Entry Flow API methods in haclient —
ListConfigEntries,IsIntegrationConfigured,StartConfigFlow,SubmitConfigFlow,SubmitConfigFlowUntilDone,RemoveConfigEntry.
Fixed¶
-
Auto-inject
!includedirectives — the operator now automatically appendsautomation: !include automations.yaml,scene: !include scenes.yaml, andscript: !include scripts.yamltoconfiguration.yamlif not already present. HA 2025.x requires explicit includes for PVC-managed files. -
Recovery mode on first start (
spec.storage.initContainer) — HA was entering recovery mode withUnable to read file /config/automations.yamlbecauseauto_include.goinjects!include automations.yamlunconditionally, but the files are created by the automation/scene/script controller only after the pod is already running. An init container (busyboxby default) now pre-creates empty[]files (automations.yaml,scenes.yaml,scripts.yaml) on the PVC before the main container starts. Image, tag, and repository are configurable viaspec.storage.initContainer. -
Location not set after bootstrap —
SetCoreConfigduring the onboarding flow was silently ignored (HA returns an error when the endpoint is not yet ready), leavingzone.homeatlatitude: 0, longitude: 0. The configuration controller now injectslatitude,longitude,elevation,unit_system, andtime_zoneinto thehomeassistant:section ofconfiguration.yaml(only for keys not already defined by the user). The correct location is applied on the first configuration reconcile via hot-reload or restart.
Removed¶
- BREAKING CHANGE: HomeAssistantAddon CRD removed —
HomeAssistantAddon(haad) has been completely removed. Use Helm charts or standard Kubernetes resources (Deployment, Service, PVC) to deploy companion services like Mosquitto, MariaDB, or Node-RED. Use the newHomeAssistantIntegrationCRD (haint) to register integrations declaratively.
v0.6.0 - 2026-03-09¶
Added¶
- HomeAssistantAutomation / Scene / Script: individual management via HA REST API — each CR is now managed individually via
POST /api/config/{type}/config/{id}(create/update) andDELETE /api/config/{type}/config/{id}(removal). Home Assistant writes directly toautomations.yaml/scenes.yaml/scripts.yamlon the PVC. The old ConfigMap aggregation approach (<ha-name>-automations,<ha-name>-scenes,<ha-name>-scripts) has been removed. - Status condition
ReloadReadyreflects the last API call result - When the bootstrap token is not yet available, the controller requeues with backoff (30s) and sets
ReasonTokenNotAvailable - Deletion via finalizer calls DELETE to HA API (best-effort — continues even when HA is unavailable)
Migration (v0.5.x → v0.6.0)¶
Note for existing deployments: when upgrading from v0.5.x to v0.6.0, the operator will automatically remove the old aggregation ConfigMaps (
<ha-name>-automations,<ha-name>-scenes,<ha-name>-scripts) and their volume mounts from the Home Assistant StatefulSet. The HA pod will be restarted once during this migration. After restart, existing automations/scenes/scripts CRs will be re-synced to HA via the REST API.
v0.5.1 - 2026-03-07¶
Fixed¶
-
HomeAssistantConfiguration: restart not triggered after adding new integration (e.g.
prometheus:) — when a reconcile attempt updated the ConfigMap content but failed before saving status, a subsequent retry would readoldConfig == newConfigand incorrectly choose hot-reload instead of restart. The controller now defaults to restart when the ConfigMap is already synced but status hash is stale. -
HomeAssistantAddon mosquitto profile: conflict with Flux GitOps and HA 2025.x incompatibility — the mosquitto profile was writing
mqtt: broker: ...toHomeAssistantConfigurationCR on every reconcile. HA 2025.x dropped support for thebrokerkey inconfiguration.yaml(returns'broker' is an invalid option), and Flux GitOps would immediately revert the change, causing an infinite reconcile loop. TheHAIntegrationhas been removed from the mosquitto profile. Configure the MQTT broker via the Home Assistant UI: Settings → Integrations → MQTT. Automatic setup via Config Flow API is planned in Phase 6.
v0.5.0 - 2026-03-01¶
Added¶
- HomeAssistantAddon CRD: Declarative addon management for Home Assistant
- Profile system with built-in profiles:
mosquitto,mariadb,node-redwith sensible defaults - User overrides: user-provided fields take priority over profile defaults
- Automatic Home Assistant integration (
spec.haIntegration) — adds integration section to HomeAssistantConfiguration CR - Auto-provisioning of K8s resources: Deployment/StatefulSet, Service, PVC, ConfigMap, Ingress
- Configuration via ConfigMap (
spec.config) — mount configuration files into the addon container - Finalizer-based cleanup — removes integration section from HomeAssistantConfiguration on CR deletion
- Status tracking: phase (Pending/Running/Failed), resolvedImage, workloadType, serviceName
- Short names:
haaddon,haad
v0.4.0 - 2026-02-21¶
Added¶
- Prometheus metrics for hot-reload operations — three new domain-specific metrics exposed at
/metrics: homeassistant_reload_total{component, result}— counter per component (automation/scene/script) and result (success/failed/skipped)homeassistant_reload_duration_seconds{component}— histogram (buckets: 0.5s–30s) for reload latency percentileshomeassistant_reload_retries_total{component}— extra retry attempts beyond the first; non-zero value indicates the reload required more than one attempt-
All data sourced from existing
ReloadResultfields — no additional API calls required -
HomeAssistantAutomation CRD: Declarative automation management with hot-reload capabilities
- Full automation definition via CRD with triggers, conditions, and actions
- Uses
runtime.RawExtensionfor flexible YAML compatibility with Home Assistant syntax - Aggregates multiple automation CRs into single ConfigMap (
<name>-automations) - Finalizer-based deletion: regenerates ConfigMap without removed automation before CR deletion
- Enable/disable without deletion via
spec.enabledfield -
Short names:
haautomation,haauto -
HomeAssistantScene CRD: Declarative scene management for Home Assistant
- Aggregation pattern - multiple CR instances → single
scenes.yaml - Entity validation with pattern regex (
domain.object_id) - Flexible entity attributes support via
runtime.RawExtension - Short names:
hascene,hasc - Status tracking: Ready, LastActivated, LastReloadTime
- Finalizer-based cleanup - regenerates ConfigMap without deleted scene
-
Auto-reload control via
spec.autoReload(default: true) -
HomeAssistantScript CRD: Declarative script management for Home Assistant
- Aggregation pattern - multiple CR instances → single
scripts.yaml - Flexible sequence definition via
runtime.RawExtension - Input parameters support via
spec.fieldsmap - Short names:
hascript,hascp - Status tracking: Ready, LastReloadTime, LastReloadMethod
- Finalizer-based cleanup - regenerates ConfigMap without deleted script
- Auto-reload control via
spec.autoReload(default: true)
v0.3.0 - 2026-01-27¶
Added¶
- HomeAssistantConfiguration CRD: Declarative configuration management with intelligent hot-reload capabilities
- Full
configuration.yamlmanagement viaspec.configurationfield - Smart reload strategy: automatically determines if changes require restart or can be hot-reloaded
- Zero-downtime updates for reloadable sections (automations, scripts, logger, input helpers, etc.)
- Three reload strategies:
auto(default, analyzes changes),hot-reload(force REST API),restart(force pod restart) - Requires bootstrap-generated API token for hot-reload functionality
- Short names:
haconfig,hacfg
Changed¶
- BREAKING CHANGE: HomeAssistantConfiguration CRD now REQUIRED for every HomeAssistant instance
- Every
HomeAssistantCR must have a correspondingHomeAssistantConfigurationCR - Controller validates HomeAssistantConfiguration exists before creating StatefulSet
- ConfigMap auto-generated from HomeAssistantConfiguration spec (pattern:
<name>-configuration) - BREAKING CHANGE: PVC naming convention changed from
<name>-configto<name>-data - The PersistentVolumeClaim for Home Assistant data storage now uses the suffix
-datainstead of-config
v0.2.0 - 2026-01-11¶
Added¶
- Zero-Touch Bootstrap: Automatic Home Assistant onboarding without manual UI interaction
- Creates admin user with credentials from Kubernetes Secret
- Configures location, timezone, units, and currency (
spec.bootstrap.location) - Sets analytics preferences (
spec.bootstrap.analytics) - Generates long-lived API token via WebSocket API
- Stores API token in Kubernetes Secret for programmatic access
- HomeAssistantSecrets CRD: Declarative secrets management for Home Assistant
- References existing Kubernetes Secrets
- Auto-generates
secrets.yamlfile - Automatic pod restart on secret changes (configurable via
spec.autoRestart) - New haclient package: Native Go HTTP/WebSocket client for Home Assistant API
Changed¶
- Service naming simplified: now uses
<name>instead of<name>-homeassistant
v0.1.0 - 2026-01-06¶
Added¶
- Initial release of Home Assistant Operator
HomeAssistantCustom Resource Definition (CRD) with support for:- Version/image configuration (
spec.version,spec.image) - Storage configuration with PVC (
spec.storage) - Service configuration (ClusterIP, NodePort, LoadBalancer) (
spec.service) - Ingress configuration with TLS support (
spec.ingress) - Resource limits and requests (
spec.resources) - Timezone configuration (
spec.timezone) - External ConfigMap for
configuration.yaml(spec.configurationFrom) - External Secret for
secrets.yaml(spec.secretsFrom) - Reconciliation controller that manages:
- StatefulSet for Home Assistant deployment
- PersistentVolumeClaim for data storage
- Service for network access
- Ingress for external access (optional)
- Health checks (liveness and readiness probes)
- Status reporting with phase, conditions, and ready state
- Multi-architecture Docker images (amd64, arm64)
- CI/CD with GitHub Actions (lint, test, e2e tests)
- k3d support for local testing
Target Environments¶
- Primary: k3s on Raspberry Pi 4/5 (ARM64)
- Also supported: Any Kubernetes cluster (AMD64/ARM64)