Validate Helm Charts / Detect changed charts (pull_request) Successful in 4s
Validate Helm Charts / HTTPRoute match (pull_request) Successful in 11s
Install-test / Install-test patchmon (pull_request) Successful in 1m41s
Validate Helm Charts / Resource quantities (pull_request) Successful in 11s
Validate Helm Charts / Selector match (pull_request) Successful in 16s
Validate Helm Charts / Bump level (pull_request) Successful in 7s
Validate Helm Charts / NetworkPolicy (pull_request) Failing after 11s
Validate Helm Charts / YAML scalars (pull_request) Successful in 10s
Validate Helm Charts / Build apache-tika (pull_request) Successful in 6s
Validate Helm Charts / Build bentopdf (pull_request) Successful in 17s
Validate Helm Charts / Build bulwark (pull_request) Successful in 16s
Validate Helm Charts / Build calibre-web-automated (pull_request) Successful in 7s
Validate Helm Charts / Trivy config apache-tika (pull_request) Successful in 13s
Validate Helm Charts / Build cyberchef (pull_request) Successful in 4s
Validate Helm Charts / Build homeassistant (pull_request) Successful in 5s
Validate Helm Charts / Build gotenberg (pull_request) Successful in 6s
Validate Helm Charts / Build rss-bridge (pull_request) Successful in 5s
Validate Helm Charts / Build searxng (pull_request) Successful in 5s
Validate Helm Charts / Build sure (pull_request) Successful in 5s
Validate Helm Charts / Build template-chart (pull_request) Successful in 5s
Validate Helm Charts / Build outline (pull_request) Successful in 5s
Validate Helm Charts / Build patchmon (pull_request) Successful in 5s
Validate Helm Charts / Build rallly (pull_request) Successful in 5s
Validate Helm Charts / Build urlaubsverwaltung (pull_request) Successful in 15s
Validate Helm Charts / Build voucher-vault (pull_request) Successful in 7s
Validate Helm Charts / Build ytdlp-web-player (pull_request) Successful in 7s
Install-test / Install-test rallly (pull_request) Successful in 1m41s
Validate Helm Charts / Build zipline (pull_request) Successful in 7s
Validate Helm Charts / Trivy config bulwark (pull_request) Successful in 13s
Validate Helm Charts / Trivy config cyberchef (pull_request) Successful in 18s
Validate Helm Charts / Trivy config gotenberg (pull_request) Successful in 13s
Validate Helm Charts / Trivy config bentopdf (pull_request) Successful in 12s
Validate Helm Charts / Trivy config calibre-web-automated (pull_request) Successful in 16s
Validate Helm Charts / Trivy config homeassistant (pull_request) Successful in 12s
Validate Helm Charts / Trivy config outline (pull_request) Successful in 14s
Validate Helm Charts / Trivy config rss-bridge (pull_request) Successful in 12s
Validate Helm Charts / Trivy config searxng (pull_request) Successful in 13s
Validate Helm Charts / Trivy config sure (pull_request) Successful in 20s
Validate Helm Charts / Trivy config patchmon (pull_request) Successful in 24s
Validate Helm Charts / Trivy config rallly (pull_request) Successful in 15s
Validate Helm Charts / Trivy config template-chart (pull_request) Successful in 14s
Validate Helm Charts / Trivy config ytdlp-web-player (pull_request) Successful in 17s
Validate Helm Charts / Trivy config urlaubsverwaltung (pull_request) Successful in 15s
Validate Helm Charts / Trivy config voucher-vault (pull_request) Successful in 11s
Validate Helm Charts / Trivy config zipline (pull_request) Successful in 24s
Install-test / Install-test rss-bridge (pull_request) Successful in 1m9s
Install-test / Install-test searxng (pull_request) Successful in 1m9s
Install-test / Install-test sure (pull_request) Successful in 2m24s
Install-test / Install-test apache-tika (pull_request) Successful in 1m23s
Install-test / Install-test urlaubsverwaltung (pull_request) Successful in 1m44s
Install-test / Install-test bentopdf (pull_request) Successful in 59s
Install-test / Install-test bulwark (pull_request) Successful in 1m23s
Install-test / Install-test voucher-vault (pull_request) Successful in 1m58s
Install-test / Install-test calibre-web-automated (pull_request) Successful in 1m55s
Install-test / Install-test cyberchef (pull_request) Successful in 1m6s
Install-test / Install-test gotenberg (pull_request) Successful in 1m19s
Install-test / Install-test ytdlp-web-player (pull_request) Successful in 59s
Install-test / Install-test zipline (pull_request) Successful in 1m52s
Install-test / Install-test homeassistant (pull_request) Successful in 2m26s
Validate Helm Charts / Install-test (pull_request) Successful in 27m21s
Install-test / Install-test outline (pull_request) Successful in 2m7s
bulwark, calibre-web-automated, patchmon, rallly, urlaubsverwaltung, voucher-vault, zipline, sure, homeassistant. One major each. These are the charts with real outbound dependencies, and they all share one limitation that is now written into every one of them: THE EGRESS PORTS ARE THE STANDARD ONES, NOT THE CONFIGURED ONES. Databases, caches and OIDC issuers arrive as connection URLs or as fields inside 1Password items that the charts never parse, so a chart cannot know where its dependency listens and cannot validate a guess. A dependency somewhere else is blocked, and networkPolicy.egress.extraPorts is the escape hatch. That is the upgrade risk of this rollout, stated per chart instead of discovered per release. Four of them differ in ways that mattered: HOMEASSISTANT gets policyTypes: [Ingress] and NO egress section at all, and that is the decision in the file rather than an omission. Its outbound traffic is the union of every integration an operator enables - mDNS and SSDP discovery, MQTT, a Zigbee or Z-Wave bridge on whatever port its firmware chose, ESPHome, printers, speakers, inverters, cloud APIs, NTP - which no chart can know. A cap would protect little, since the devices sit on the node's own LAN, and would break integrations one at a time, months apart, each looking like a flaky device rather than like a policy. So valkey's rule applies in the other direction: policyTypes names only what there is something to restrict. URLAUBSVERWALTUNG allows BOTH 5432 and 3306, deliberately. It is handed a full JDBC URL inside a 1Password item and never parses it, so it cannot tell PostgreSQL from MariaDB - and this cluster runs both. Allowing one would break half the possible deployments on upgrade, with the symptom buried in a Java stack trace. A small widening against a large and silent failure. RALLLY derives its SMTP rule from rallly.smtp.enabled rather than always rendering it: with mail switched off there is nothing to permit, and a rule that permits nothing makes the object look stricter than it is. BULWARK is the chart where the cap can fail most quietly. Its probes test its own port and say nothing about the JMAP server, so a jmapServerUrl on a port the policy does not allow yields a Ready pod serving an empty webmail. 443 is allowed, Stalwart's in-cluster 8080 is not, and both halves of that are in values.yaml and NOTES.txt. PATCHMON and HOMEASSISTANT select their pods by the BARE release-and-chart label, with no -deployment or -sfs suffix. Copying a selector from a neighbouring chart would have matched nothing and left a policy that filters nothing - which is precisely the first assertion of the render test. Five CI fixtures gain networkPolicy.egress.extraPorts, and they are the only place in the repository where the cap's cost is actually exercised: the CI dex runs on 5556 rather than 443 (patchmon, rallly, urlaubsverwaltung, voucher-vault) and the CI JMAP server on plain 8080 (bulwark). Part of #5
427 lines
23 KiB
Plaintext
427 lines
23 KiB
Plaintext
{{- $replicas := include "homeassistant.replicas" (dict "scaleDown" .Values.scaleDown "replicas" .Values.replicas) -}}
|
|
{{- $res := fromYaml (include "homeassistant.effectiveResources" .Values.resources) -}}
|
|
{{/*
|
|
The ephemeral-storage limit goes through formatNumber for the reason issue #157
|
|
documents: it comes back from fromYaml as float64, and a bare byte count would
|
|
print as "1e+06" here just as it did in the rendered manifest.
|
|
*/}}
|
|
{{- $ephRaw := index ($res.limits | default dict) "ephemeral-storage" -}}
|
|
{{- $eph := "the computed ephemeral-storage limit" -}}
|
|
{{- if not (kindIs "invalid" $ephRaw) -}}
|
|
{{- $eph = include "homeassistant.formatNumber" $ephRaw -}}
|
|
{{- end -}}
|
|
{{- $startup := fromYaml (include "homeassistant.startupProbe" .Values.startupProbe) -}}
|
|
{{- $budget := "" -}}
|
|
{{- if and (kindIs "float64" $startup.periodSeconds) (kindIs "float64" $startup.failureThreshold) -}}
|
|
{{- $budget = printf "%.0f seconds" (mulf $startup.periodSeconds $startup.failureThreshold) -}}
|
|
{{- end -}}
|
|
{{- $ctr := fromYaml (include "homeassistant.containerSecurityContext" .Values.securityContext) -}}
|
|
{{- $caps := (index $ctr "capabilities") | default dict -}}
|
|
{{- $netRaw := has "NET_RAW" ((index $caps "add") | default list) -}}
|
|
{{- /*
|
|
NOTHING IN CI READS THIS FILE. `helm template` does not emit notes and no
|
|
workflow or script under ci/ reads it, so every claim below carries
|
|
review-only assurance - it is checked by being read, not by a stage. A
|
|
rendering ERROR here does fail `helm template`, which is all the pipeline can
|
|
see of it.
|
|
*/ -}}
|
|
{{- $ing := .Values.ingress -}}
|
|
{{- $httpRoute := .Values.httpRoute | default dict -}}
|
|
{{- /*
|
|
"Published" rather than "has an Ingress": the two modes are alternatives, and
|
|
both read ingress.hosts (Gitea #114).
|
|
*/ -}}
|
|
{{- $published := or $ing.enabled $httpRoute.enabled -}}
|
|
{{- $hosts := $ing.hosts | default list -}}
|
|
{{- $tls := $ing.tls | default list -}}
|
|
{{- $ann := $ing.annotations | default dict -}}
|
|
{{- $issuer := or (hasKey $ann "cert-manager.io/cluster-issuer") (hasKey $ann "cert-manager.io/issuer") -}}
|
|
{{- $certName := "" -}}
|
|
{{- range $t := $tls }}{{ if not $certName }}{{ $certName = ($t.secretName | default "") }}{{ end }}{{ end }}
|
|
{{- $placeholder := false -}}
|
|
{{- range $h := $hosts }}{{ if eq ($h.host | default "") "chart-example.local" }}{{ $placeholder = true }}{{ end }}{{ end }}
|
|
{{- $pod := printf "%s-%s-sfs-0" .Release.Name .Chart.Name -}}
|
|
{{- $sfs := printf "%s-%s-sfs" .Release.Name .Chart.Name -}}
|
|
Home Assistant {{ .Chart.AppVersion }} — release {{ .Release.Name }} in namespace {{ .Release.Namespace }}.
|
|
|
|
{{- if $ing.enabled }}
|
|
|
|
ACCESS — INGRESS MODE (ingress.enabled: true, httpRoute.enabled: false)
|
|
{{- range $h := $hosts }}
|
|
{{- $scheme := "http" }}
|
|
{{- range $t := $tls }}
|
|
{{- if has ($h.host | default "") ($t.hosts | default list) }}{{ $scheme = "https" }}{{ end }}
|
|
{{- end }}
|
|
{{- range $p := ($h.paths | default list) }}
|
|
{{ $scheme }}://{{ $h.host }}{{ $p.path }}
|
|
{{- end }}
|
|
{{- end }}
|
|
|
|
This chart keeps the upstream-style ingress block — hosts and tls lists
|
|
instead of the domain/clusterIssuer pair the other charts in this repo use.
|
|
That is a documented exception, and it has one consequence worth knowing
|
|
before you go looking for a certificate:
|
|
{{- if not $tls }}
|
|
|
|
ingress.tls is empty, so the Ingress carries no TLS section at all and the
|
|
URLs above are plain http. Nothing here requests a certificate.
|
|
{{- else if not $issuer }}
|
|
|
|
ingress.tls names a secretName, but NOTHING IN THIS RELEASE CREATES THAT
|
|
SECRET. The chart adds no cert-manager annotation of its own — unlike the
|
|
other charts here, which build one from ingress.clusterIssuer — and
|
|
ingress.annotations carries neither cert-manager.io/cluster-issuer nor
|
|
cert-manager.io/issuer. Until that Secret exists by some other means,
|
|
ingress-nginx answers with its own default certificate and every browser
|
|
shows a name mismatch. Add the annotation under ingress.annotations:
|
|
|
|
ingress:
|
|
annotations:
|
|
cert-manager.io/cluster-issuer: letsencrypt
|
|
{{- else }}
|
|
|
|
The certificate is requested by cert-manager through the annotation you set
|
|
under ingress.annotations; the chart adds none by itself, and it does not
|
|
name the TLS Secret either — you did, in ingress.tls. cert-manager's
|
|
ingress-shim names the Certificate after that Secret, so this is the object
|
|
to watch while the browser still shows a warning:
|
|
|
|
kubectl -n {{ .Release.Namespace }} get certificate {{ $certName | default "<your ingress.tls secretName>" }}
|
|
{{- end }}
|
|
{{- else if $httpRoute.enabled }}
|
|
|
|
ACCESS — GATEWAY MODE (httpRoute.enabled: true, ingress.enabled: false)
|
|
{{- range $h := $hosts }}
|
|
{{- range $p := ($h.paths | default list) }}
|
|
https://{{ $h.host }}{{ $p.path }}
|
|
{{- end }}
|
|
{{- end }}
|
|
|
|
ONE HTTPRoute PER ENTRY of ingress.hosts, and nothing else of the routing
|
|
path — no Gateway, no class, no Certificate, no policy object:
|
|
{{- range $i, $h := $hosts }}
|
|
{{ $.Release.Name }}-{{ $.Chart.Name }}-httproute{{ if $i }}-{{ $i }}{{ end }} {{ $h.host }}
|
|
{{- end }}
|
|
|
|
This chart has no ingress.domain — it is the documented ingress exception and
|
|
keeps upstream's flexible host list — so there is no httpRoute.hostnames
|
|
either: the hostnames and the paths above come from ingress.hosts, which is
|
|
read in BOTH modes. ingress.className, ingress.tls and ingress.annotations
|
|
are NOT read in this mode.
|
|
|
|
Three things therefore belong to the cluster, and if any is missing the
|
|
hostnames above simply do not answer:
|
|
|
|
1. The Gateway-API CRDs. Without them this install would have failed, so
|
|
they are present — nothing to check.
|
|
2. The Gateway named in httpRoute.parentRefs
|
|
{{- range $httpRoute.parentRefs }}
|
|
{{ .name }}{{ if .namespace }} in namespace {{ .namespace }}{{ end }}{{ if .sectionName }}, listener {{ .sectionName }}{{ end }}
|
|
{{- end }}
|
|
must exist AND its listener must allow routes from this namespace
|
|
({{ .Release.Namespace }}) in allowedRoutes. A rejected attachment is visible only
|
|
in the route's status:
|
|
|
|
kubectl -n {{ .Release.Namespace }} get httproute {{ .Release.Name }}-{{ .Chart.Name }}-httproute -o yaml
|
|
|
|
Read status.parents[].conditions — "Accepted: True" and
|
|
"ResolvedRefs: True" is what a working attachment looks like. An empty
|
|
status means no controller has looked at the route at all.
|
|
3. The TLS certificates for those hostnames, on the Gateway's listeners.
|
|
THE URLS ABOVE SAY https BECAUSE THAT IS WHAT A GATEWAY LISTENER
|
|
NORMALLY TERMINATES, not because this release arranged it: ingress.tls
|
|
is not read in this mode and this chart requests no certificate in
|
|
either mode (see the Ingress-mode text — it never did).
|
|
|
|
WHAT IS GONE IN THIS MODE, generally rather than specifically: the Gateway
|
|
API has no annotation channel for controller behaviour, so everything in
|
|
ingress.annotations is inert. For THIS chart there is no measured loss to
|
|
report — the installation this repository serves does not deploy
|
|
homeassistant at all, so unlike its five siblings in this sweep it had no
|
|
operator-supplied annotation to lose. That is an absence of evidence about
|
|
that installation, not a property of the chart: whatever YOU put in
|
|
ingress.annotations disappears here.
|
|
|
|
AND ONE THING TO CHECK IN HOME ASSISTANT ITSELF, because no Kubernetes object
|
|
reports it: Home Assistant refuses requests whose Host header is not in its
|
|
own configuration and builds absolute URLs from it, and it needs the proxy's
|
|
address in trusted_proxies to read X-Forwarded-For at all. Changing the data
|
|
path changes that address — the Gateway's data-plane pods are not the ingress
|
|
controller's. See the trusted-proxies section below; it applies to the
|
|
Gateway exactly as it did to ingress-nginx, with a different CIDR.
|
|
{{- else }}
|
|
|
|
ACCESS — NEITHER INGRESS NOR HTTPROUTE
|
|
ingress.enabled and httpRoute.enabled are both false, so this release
|
|
publishes nothing. Nothing else in this chart reads ingress.hosts, so no
|
|
other setting silently depends on it.
|
|
Reach the instance inside the cluster with
|
|
|
|
kubectl -n {{ .Release.Namespace }} port-forward svc/{{ .Release.Name }}-{{ .Chart.Name }}-service 8123:{{ .Values.service.port }}
|
|
|
|
and open http://localhost:8123. The Service listens on port {{ .Values.service.port }} and
|
|
forwards to the container's 8123; keeping 8123 as the LOCAL port matters,
|
|
because a browser that has already talked to another Home Assistant on
|
|
localhost:8123 will otherwise mix up the stored tokens.
|
|
{{- end }}
|
|
{{- if and $published $placeholder }}
|
|
|
|
THIS RELEASE IS PUBLISHED UNDER THE CHART'S PLACEHOLDER HOST
|
|
{{ if $ing.enabled }}ingress.enabled{{ else }}httpRoute.enabled{{ end }} is true while ingress.hosts still contains the default entry
|
|
"chart-example.local". That name resolves nowhere, so the {{ if $ing.enabled }}Ingress{{ else }}HTTPRoute{{ end }} exists,
|
|
{{ if $ing.enabled }}ingress-nginx accepts it{{ else }}the Gateway accepts it{{ end }}, and no request ever arrives. Set ingress.hosts to
|
|
the name you actually serve — for example example.com — before assuming the
|
|
release is unreachable for some deeper reason.
|
|
{{ if $httpRoute.enabled }}ingress.hosts is read in Gateway mode too; it is not an "Ingress-only"
|
|
value.{{ end }}
|
|
{{- end }}
|
|
{{ if eq $replicas "0" }}
|
|
SCALED TO ZERO
|
|
{{ if .Values.scaleDown }}scaleDown: true{{ else }}replicas: 0{{ end }} — the StatefulSet runs with replicas: 0, so nothing answers
|
|
until you set it back and upgrade again. The PVC, the OnePasswordItem and
|
|
the Service{{ if $ing.enabled }} and Ingress stay in place, so no state is lost and no
|
|
certificate is re-issued.{{ else if $httpRoute.enabled }} and the HTTPRoutes stay in place, so no state is
|
|
lost — the routes keep pointing at a Service with no endpoints, and the
|
|
Gateway answers with an error of its own rather than with nothing.{{ else }} stay in place, so no state is lost.{{ end }}
|
|
|
|
Bringing this release back up is not a no-op: the pod re-reads the config
|
|
item from the vault as it starts, and it starts on whatever app version the
|
|
chart carries now. A release parked at zero for a long time therefore comes
|
|
back changed in two ways at once.
|
|
{{ end }}
|
|
THE CONFIGURATION IS A 1PASSWORD ITEM, AND IT IS READ-ONLY IN THE CONTAINER
|
|
Home Assistant is configured from files, not from the environment, and this
|
|
chart delivers those files out of a single 1Password item. The operator
|
|
copies the item's FIELDS into the Kubernetes Secret, one key per field name,
|
|
and this chart mounts exactly three of them. The field names are decided
|
|
here and appear nowhere in 1Password, so they have to match to the letter:
|
|
|
|
secrets.configSecretRef → {{ .Values.secrets.configSecretRef | default "(not set — the render fails without it)" }}
|
|
→ Secret {{ .Release.Name }}-{{ .Chart.Name }}-config-secret
|
|
|
|
config → /config/configuration.yaml
|
|
http-config → /config/http-config.yaml
|
|
recorder-config → /config/recorder-config.yaml
|
|
|
|
The three are separate files on purpose, and configuration.yaml has to pull
|
|
the other two in itself — the chart only mounts them, it does not wire them
|
|
together. A working minimum for the "config" field is
|
|
|
|
default_config:
|
|
http: !include http-config.yaml
|
|
recorder: !include recorder-config.yaml
|
|
|
|
A field spelled differently yields a Secret without that key. The pod then
|
|
hangs in CreateContainerConfigError while the Secret itself exists, so it
|
|
looks as if the operator had done its job. What it actually delivered:
|
|
|
|
kubectl -n {{ .Release.Namespace }} get secret {{ .Release.Name }}-{{ .Chart.Name }}-config-secret -o jsonpath='{.data}'
|
|
|
|
All three are mounted with subPath from a Secret volume, which makes them
|
|
READ-ONLY inside the container. Home Assistant's own file editor and every
|
|
UI page that offers to write configuration.yaml will fail on these three
|
|
paths — and only on these three. Everything else under /config is on the
|
|
PVC and writable as usual.
|
|
|
|
Pointing secrets.configSecretRef at a DIFFERENT item rolls the pod: the
|
|
StatefulSet carries a checksum over the rendered OnePasswordItem, so the
|
|
item path is part of the pod spec. Changing the CONTENT of the same item
|
|
does not, and this is the sharp edge — Helm never sees that content, and a
|
|
subPath mount is never refreshed by the kubelet even after the operator has
|
|
updated the Secret. The running pod keeps the old files until something
|
|
else restarts it, which may be months later, so a broken value surfaces far
|
|
away from the edit that caused it. After every content change:
|
|
|
|
kubectl -n {{ .Release.Namespace }} rollout restart statefulset/{{ $sfs }}
|
|
|
|
BEHIND A PROXY, THE http BLOCK DECIDES BETWEEN 400 AND WRONG CLIENT IPs
|
|
Every request reaching Home Assistant through {{ if $httpRoute.enabled }}the Gateway{{ else }}ingress-nginx{{ end }} arrives from
|
|
{{ if $httpRoute.enabled }}its data-plane pod IP{{ else }}the controller's pod IP{{ end }}, not from the browser. The http-config field is
|
|
where that is dealt with, and both ways of getting it wrong are quiet in
|
|
their own way:
|
|
|
|
use_x_forwarded_for: true without the proxy's address in trusted_proxies
|
|
makes Home Assistant reject the request outright — the browser gets
|
|
400 Bad Request and the log names the untrusted address it saw.
|
|
|
|
Neither setting at all means every client looks like {{ if $httpRoute.enabled }}the Gateway{{ else }}the ingress
|
|
controller{{ end }}. Nothing fails; you simply get one address in the logbook for
|
|
everyone, and Home Assistant's login-attempt ban then locks out all users
|
|
at once because they share that address.
|
|
|
|
The value belongs to the {{ if $httpRoute.enabled }}GATEWAY's{{ else }}ingress controller's{{ end }} POD network, not to the nodes
|
|
and not to the clients{{ if $httpRoute.enabled }} — and switching from an Ingress to a Gateway CHANGES
|
|
that network, because a Gateway's data plane normally runs in a namespace of
|
|
its own. A trusted_proxies list carried over unchanged from Ingress mode
|
|
turns every request into a 400 if the pod CIDR differs, or silently keeps
|
|
attributing all clients to one address if it does not{{ end }}:
|
|
|
|
use_x_forwarded_for: true
|
|
trusted_proxies:
|
|
- 10.42.0.0/16 # the cluster's pod CIDR — check yours
|
|
|
|
Read the address Home Assistant actually sees straight out of the log
|
|
rather than guessing at the CIDR:
|
|
|
|
kubectl -n {{ .Release.Namespace }} logs {{ $pod }} | grep -i forwarded
|
|
|
|
THE FIRST VISITOR BECOMES THE OWNER
|
|
Against an empty /config Home Assistant has no users and sends every visitor
|
|
into onboarding, where the first account created is the owner. There is no
|
|
password to look up anywhere and no Secret holding one — whoever reaches the
|
|
URL first owns this instance. Do the onboarding right after the first
|
|
rollout, not the next morning.
|
|
|
|
Onboarding also decides the instance's location, unit system and time zone
|
|
and stores them under /config/.storage. That is state on the PVC, not
|
|
configuration in the 1Password item: it survives a pod restart, and it is
|
|
gone with the volume.
|
|
|
|
TZ AND HOME ASSISTANT'S OWN TIME ZONE ARE TWO SETTINGS, NOT ONE
|
|
core.timezone ({{ .Values.core.timezone | default "unset" }}) is passed to the container as TZ and is the only
|
|
environment variable this chart sets by itself. Home Assistant's own time
|
|
zone is the one from onboarding or from configuration.yaml, and it is NOT
|
|
derived from this value. When the two disagree, log timestamps and
|
|
time-based automations disagree with them, and nothing reports a problem.
|
|
|
|
WHAT A READY POD PROVES — AND WHAT IT DOES NOT
|
|
All three probes fetch "/" on the container port. That path is served by the
|
|
HTTP integration as soon as it has come up, which happens early in the
|
|
bootstrap and says nothing about the rest:
|
|
|
|
A configuration.yaml that cannot be loaded does not necessarily keep the
|
|
pod from becoming Ready. Home Assistant falls back to safe mode, serves
|
|
the frontend with none of your configuration in effect, and the rollout
|
|
goes green. An instance that comes back "empty" after a config change is
|
|
this, and the reason is only in the log.
|
|
|
|
An integration that fails to set up, a recorder that cannot reach its
|
|
database, a full volume — none of them are in the probe's answer.
|
|
|
|
So check the log after every config change, not the rollout status:
|
|
|
|
kubectl -n {{ .Release.Namespace }} logs {{ $pod }}
|
|
|
|
The startup probe gates liveness and readiness and gives the first boot
|
|
{{ if $budget }}{{ $budget }}{{ else }}its configured budget{{ end }}, which is deliberately generous: a cold start
|
|
installs and initializes integrations. If a start outruns that budget the
|
|
kubelet kills the container mid-setup and the next attempt begins again,
|
|
which looks like CrashLoopBackOff with no error in the log at all.
|
|
|
|
/config IS THE WHOLE OF THE STATE, AND helm uninstall DELETES IT
|
|
The owner account, every long-lived token, all UI-created automations and
|
|
dashboards, every integration's stored credentials and the recorder
|
|
database live under /config — on {{ .Release.Name }}-{{ .Chart.Name }}-pvc ({{ .Values.core.storage.size }}
|
|
from storage class {{ .Values.core.storage.storageClass | default "the cluster default" }}), inside its subPath "hass-config".
|
|
Only the three files above come from the vault; nothing else here is
|
|
reproducible from the chart's values.
|
|
|
|
That PVC is an ordinary claim in this release, not a StatefulSet
|
|
volumeClaimTemplate: "helm uninstall {{ .Release.Name }}" deletes it, and with a Delete
|
|
reclaim policy the instance is then gone for good. Back it up before
|
|
uninstalling — a resource-policy annotation added to the live PVC afterwards
|
|
does not help, because Helm reads that annotation from the release manifest.
|
|
|
|
The recorder is what makes the volume grow, and its retention is a field of
|
|
the 1Password item (purge_keep_days in recorder-config), not a chart value.
|
|
Growing core.storage.size only takes effect if the storage class allows
|
|
volume expansion; shrinking it is refused by Kubernetes and makes the next
|
|
helm upgrade fail on the PVC instead of touching the data.
|
|
|
|
/tmp AND /run ARE emptyDirs AND COUNT AGAINST ephemeral-storage
|
|
The root filesystem is read-only, so the two paths the image must write to —
|
|
/run for s6-overlay's state and /tmp for Home Assistant's scratch files —
|
|
are emptyDirs. An emptyDir counts against the container's ephemeral-storage
|
|
limit ({{ $eph }}). Going over it does not fail a request: the kubelet evicts the
|
|
whole pod, and the reason is on the pod, not in Home Assistant's log. Where
|
|
integrations pull large downloads through /tmp, raise
|
|
resources.requests.ephemeralStorage — the limit follows it through
|
|
resources.limitFactor.
|
|
{{- if gt (atoi $replicas) 1 }}
|
|
|
|
MORE THAN ONE REPLICA WILL NOT WORK HERE
|
|
replicas is {{ $replicas }}, but every pod of this StatefulSet mounts the SAME
|
|
ReadWriteOnce claim {{ .Release.Name }}-{{ .Chart.Name }}-pvc at /config — there is no
|
|
volumeClaimTemplate and no per-pod volume. Unless the storage class hands
|
|
that claim to several nodes, the additional pods stay stuck before start;
|
|
where they do start, two Home Assistant processes write the same state
|
|
directory and the same recorder database at once. This chart is built for
|
|
one replica, and Home Assistant does not run active-active.
|
|
{{- end }}
|
|
{{- if not $netRaw }}
|
|
|
|
NET_RAW HAS BEEN DROPPED FROM THE CONTAINER
|
|
The chart's default adds NET_RAW back after dropping ALL, and the effective
|
|
securityContext.container for this release no longer has it. That is a
|
|
values override, not a chart default. Home Assistant's device discovery —
|
|
the DHCP watcher, the ping and arp integrations — opens raw sockets, and
|
|
without the capability it does not fail loudly: those integrations log
|
|
"Operation not permitted" and simply find nothing. Devices that used to
|
|
appear on their own will stop appearing.
|
|
{{- end }}
|
|
{{- if .Values.env }}
|
|
|
|
env IS APPENDED, AND HOME ASSISTANT WILL NOT READ IT ON ITS OWN
|
|
The {{ len .Values.env }} entries land after TZ and are rendered through tpl, so a value may
|
|
contain template expressions. Note what they do NOT do: apart from TZ, Home
|
|
Assistant takes no configuration from the environment. An entry here reaches
|
|
the application only if the YAML in the 1Password item picks it up with the
|
|
!env_var tag — otherwise it sits in the pod spec unused.
|
|
|
|
Anything sensitive belongs in valueFrom.secretKeyRef rather than a plain
|
|
value: a value is stored in the pod spec and readable by anyone who can read
|
|
pods in {{ .Release.Namespace }}.
|
|
{{- end }}
|
|
{{- if .Values.networkPolicy.enabled }}
|
|
|
|
NETWORKPOLICY IS ACTIVE — WHAT IT CAPS, AND WHAT IT DOES NOT
|
|
{{ .Release.Name }}-{{ .Chart.Name }}-networkpolicy selects this workload's pods.
|
|
Inbound: TCP 8123 only — anything else this image or an add-on binds is not
|
|
reachable. THERE IS NO EGRESS RESTRICTION AT ALL, deliberately: outbound
|
|
traffic here is the union of every integration you enable (mDNS and SSDP
|
|
discovery, MQTT, a Zigbee or Z-Wave bridge, ESPHome, printers, speakers,
|
|
inverters, cloud APIs, NTP), a chart cannot know that set, and a cap would
|
|
break integrations one at a time — months apart, each looking like a flaky
|
|
device rather than like a policy. An installation that wants one should write
|
|
it at release level, against its own integration list.
|
|
|
|
Three limits belong in the same breath, because none of them is visible in
|
|
the object itself:
|
|
|
|
- EGRESS IS BY PORT, NEVER BY DESTINATION. A NetworkPolicy matches CIDRs and
|
|
selectors and cannot express a hostname, so every egress rule permits its
|
|
port to ANY address. That is a limit of the object, not a shortcut — this
|
|
cluster's CNI (Canal: Flannel plus Calico) has no FQDN rule either. An
|
|
outbound port the chart does not know about is BLOCKED: add it to
|
|
networkPolicy.egress.extraPorts, and expect to find out through an
|
|
application-level connection error rather than through this object.
|
|
{{- if .Values.networkPolicy.probeSources }}
|
|
- THE PROBE PATH IS RESTRICTED in this release, because
|
|
networkPolicy.probeSources is set to
|
|
{{ join ", " .Values.networkPolicy.probeSources }}
|
|
Probes come from the IP of the WORKER NODE running the pod. If none of those
|
|
CIDRs covers the node this pod lands on, the kubelet's probes are dropped
|
|
and THE POD NEVER BECOMES READY — the rollout then times out and the symptom
|
|
looks like a broken application. Compare
|
|
|
|
kubectl -n {{ .Release.Namespace }} describe pod -l app="{{ .Release.Name }}-{{ .Chart.Name }}"
|
|
|
|
against `kubectl get node -o wide`.
|
|
{{- else }}
|
|
- THE PROBED PORT IS OPEN INSIDE THE CLUSTER. networkPolicy.probeSources is
|
|
empty, so that rule carries no source selector at all and any pod the CNI
|
|
routes to can reach it. Not from the internet — but this is NOT pod-level
|
|
isolation. Narrowing it means listing the worker node IPs in
|
|
networkPolicy.probeSources, because the kubelet probes from the node's own
|
|
address and the chart cannot know it; a wrong guess there leaves the pod
|
|
permanently un-Ready.
|
|
{{- end }}
|
|
- IT IS ONLY A BOUNDARY WHERE THE CNI ENFORCES POLICIES. Where it does not,
|
|
this object is accepted and filters nothing, so everything reads as
|
|
configured while nothing is filtered. `kubectl -n {{ .Release.Namespace }} get networkpolicy`
|
|
shows the object exists; only a connection attempt from an unrelated
|
|
namespace shows that it bites.
|
|
|
|
Switch it off with networkPolicy.enabled: false.
|
|
{{- end }}
|