diff --git a/README.md b/README.md index 0216865..c7313cf 100644 --- a/README.md +++ b/README.md @@ -7,11 +7,12 @@ Collection of some self-made helm-charts. Each top-level directory is a standalo | Chart | Deploys | Notes | |---|---|---| | `apache-tika` | Apache Tika | Deployment + Service only (no ingress) | +| `bentopdf` | BentoPDF | Stateless Deployment + `config.json` ConfigMap, `subPath`-mounted into the web root and always rendered (empty renders as `{}`) | | `calibre-web-automated` | Calibre-Web Automated | StatefulSet, extra consume-PVC | | `cyberchef` | CyberChef | Stateless Deployment | | `freesailarr` | gluetun VPN media stack (qbittorrent, prowlarr, flaresolverr, radarr, recyclarr, sonarr, seerr, bazarr) | Single-pod StatefulSet: all apps share gluetun's network namespace; config volumes via volumeClaimTemplates, media PVCs as existingClaims (optional per-category `subPath`, so one claim can serve all three categories). One ClusterIP Service per app; no global `ingress.domain`. Ingress in two shapes that mix: one host per app (default, each app's host required), or one shared `ingress.host` with the apps as sub-paths (documented ingress exception below) | | `gotenberg` | Gotenberg | Deployment + Service only (no ingress) | -| `homeassistant` | Home Assistant | StatefulSet, has `NOTES.txt` and Helm tests | +| `homeassistant` | Home Assistant | StatefulSet; the only chart carrying Helm tests (`templates/tests/`) | | `matrix-synapse` | Matrix Synapse | StatefulSet plus an nginx delegation Deployment/Service | | `minecraft-router` | itzg/mc-router | Deployment + routing ConfigMap + NodePort Service (TCP only, no ingress); `mappings` is a required value | | `minecraft-server` | itzg/minecraft-server | StatefulSet + PVC + Service + init-script ConfigMap (no ingress); service type is a value (`ClusterIP` by default, routed via `minecraft-router`). Published once per Java line — the image tag comes from `.Chart.AppVersion`, so the Java line is chosen by picking the chart version, and `minecraft.version` ships a default matching that line. Since chart 2.0.0 the tag is an **immutable dated upstream release** (`appVersion: -java8` / `-java17` / `-java21`, e.g. `2026.8.2-java8`) instead of the continuously rebuilt rolling line tag. Pilot for the strict `values.schema.json` (#68), which is now binding for every chart | @@ -274,6 +275,6 @@ Reference implementation: `template-chart/values.schema.json` and `template-char - `icon:` is mandatory in every `Chart.yaml`. **Documented exception — freesailarr, the only chart without one**: the rule everywhere else is "the icon is this chart's one upstream's logo", and a chart bundling nine independently versioned projects has no such upstream — picking one of the nine would imply a "main app" the stack deliberately does not have. That is the whole criterion, and it applies to no other chart in the repo: a single-upstream chart without an icon is a missing icon, not an exception. The omission costs an INFO line in `helm lint --strict` (`icon is recommended`), which still exits 0. - `helm lint --strict` must pass without findings. -- `NOTES.txt` for every chart is planned (#43); nine charts ship one so far (`apache-tika`, `bentopdf`, `cyberchef`, `freesailarr`, `gotenberg`, `homeassistant`, `patchmon`, `rss-bridge`, `searxng`). Reference implementation: `template-chart/templates/NOTES.txt`, which is a placeholder-carrying template rather than finished prose — a new chart adapts it instead of starting from an empty file. The bar it sets: print what an operator would otherwise have to look up in the templates (the rendered access URL, **which fields the 1Password item must carry**, the options actually active in this release, and the chart's known traps), conditional on the values really set. A `NOTES.txt` that only repeats the values is skimmed once and never read again. **No real domain anywhere** — the URL renders from `ingress.domain`, and where an example is unavoidable it is `example.com`. Adding one renders no Kubernetes object, so it is purely additive: a minor bump. +- **Every chart ships a `templates/NOTES.txt`** (#43), `template-chart` included — so this is a requirement for a new chart, not a nice-to-have it can defer. No list of names here on purpose: a count goes stale the moment the next chart lands, which is exactly how the previous wording came to claim a number it never matched. Reference implementation: `template-chart/templates/NOTES.txt`, a placeholder-carrying template rather than finished prose, so a new chart adapts it instead of starting from an empty file. The bar it sets: print what an operator would otherwise have to look up in the templates (the rendered access URL, **which fields the 1Password item must carry**, the options actually active in this release, and the chart's known traps), conditional on the values really set. A `NOTES.txt` that only repeats the values is skimmed once and never read again. **No real domain anywhere** — the URL renders from `ingress.domain`, and where an example is unavoidable it is `example.com`. A `NOTES.txt` renders no Kubernetes object, so writing or reworking one is purely additive: a minor bump. - CI: every installable chart has fixtures under `ci//` (`test-values.yaml`, optionally `fixtures.yaml`/`fixtures.env.yaml`/`settings.env`) or a justified entry in `ci/excluded-charts.txt` — the install-test fails if both are missing. - Charts have no dependencies (`charts/` directories are empty). diff --git a/freesailarr/Chart.yaml b/freesailarr/Chart.yaml index 7b261f3..db2b74b 100644 --- a/freesailarr/Chart.yaml +++ b/freesailarr/Chart.yaml @@ -8,7 +8,7 @@ description: A Helm chart to deploy a Wireguard-tunneled media automation stack type: application -version: 3.0.1+up1.0.0 +version: 3.0.2+up1.0.0 # The chart's own version marker, freely chosen and tied to no upstream. The # stack bundles nine independently versioned projects, so there is no single # "app version" to track here. diff --git a/freesailarr/templates/_helpers.tpl b/freesailarr/templates/_helpers.tpl index a49f2c4..93a2ef4 100644 --- a/freesailarr/templates/_helpers.tpl +++ b/freesailarr/templates/_helpers.tpl @@ -67,7 +67,7 @@ Handles plain numbers ("2", "0.5", 1073741824), decimal ("1.5Gi"), binary results are printed without a decimal point. */}} {{- define "freesailarr.multiplyQuantity" -}} -{{- $quantity := printf "%v" .quantity -}} +{{- $quantity := include "freesailarr.formatNumber" .quantity -}} {{- $number := regexFind "^[0-9]+(\\.[0-9]+)?" $quantity -}} {{- if not $number -}} {{- fail (printf "cannot parse resource quantity %q" $quantity) -}} @@ -80,7 +80,24 @@ results are printed without a decimal point. {{- if eq $product (floor $product) -}} {{- printf "%d%s" (int64 $product) $suffix -}} {{- else -}} -{{- printf "%v%s" $product $suffix -}} +{{- printf "%s%s" (include "freesailarr.formatNumber" $product) $suffix -}} +{{- end -}} +{{- end -}} + +{{/* +Render a number as a plain decimal string, never in exponential notation. + +Why this exists (issue #157): the null-safe fallback helpers pass values +through fromYaml, which parses every number as float64. Go's "%v" switches +float64 to exponential notation from about 1e6 upwards, so a bare byte count +like 1048576 became "1.048576e+06" - not a valid Kubernetes quantity, and the +render aborted. Strings ("512Mi") are passed through untouched. +*/}} +{{- define "freesailarr.formatNumber" -}} +{{- if kindIs "float64" . -}} +{{- regexReplaceAll "\\.?0+$" (printf "%.10f" .) "" -}} +{{- else -}} +{{- printf "%v" . -}} {{- end -}} {{- end -}} diff --git a/homeassistant/Chart.yaml b/homeassistant/Chart.yaml index c70530c..e5ec7ff 100644 --- a/homeassistant/Chart.yaml +++ b/homeassistant/Chart.yaml @@ -4,7 +4,7 @@ description: A Helm chart to deploy Home Assistant on Kubernetes icon: https://www.home-assistant.io/images/home-assistant-logo.svg type: application -version: 6.0.0+up2026.8.3 +version: 6.1.0+up2026.8.3 appVersion: "2026.8.3" diff --git a/homeassistant/templates/NOTES.txt b/homeassistant/templates/NOTES.txt index b8394df..31fb77d 100644 --- a/homeassistant/templates/NOTES.txt +++ b/homeassistant/templates/NOTES.txt @@ -1 +1,291 @@ -Hello :) \ No newline at end of file +{{- $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) -}} +{{- $ing := .Values.ingress -}} +{{- $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 +{{- 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 "" }} +{{- end }} +{{- else }} + +ACCESS — NO INGRESS + ingress.enabled is false, so this release has no Ingress. 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 $ing.enabled $placeholder }} + +THE INGRESS IS PUBLISHED UNDER THE CHART'S PLACEHOLDER HOST + ingress.enabled is true while ingress.hosts still contains the default entry + "chart-example.local". That name resolves nowhere, so the Ingress exists, + ingress-nginx accepts it, 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. +{{- 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 }} 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 AN INGRESS, THE http BLOCK DECIDES BETWEEN 400 AND WRONG CLIENT IPs + Every request reaching Home Assistant through ingress-nginx arrives from the + controller's pod IP, 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 the ingress + controller. 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 ingress controller's POD network, not to the nodes + and not to the clients: + + 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 }} diff --git a/homeassistant/templates/_helpers.tpl b/homeassistant/templates/_helpers.tpl index 78611e0..987eb67 100644 --- a/homeassistant/templates/_helpers.tpl +++ b/homeassistant/templates/_helpers.tpl @@ -67,7 +67,7 @@ Handles plain numbers ("2", "0.5", 1073741824), decimal ("1.5Gi"), binary results are printed without a decimal point. */}} {{- define "homeassistant.multiplyQuantity" -}} -{{- $quantity := printf "%v" .quantity -}} +{{- $quantity := include "homeassistant.formatNumber" .quantity -}} {{- $number := regexFind "^[0-9]+(\\.[0-9]+)?" $quantity -}} {{- if not $number -}} {{- fail (printf "cannot parse resource quantity %q" $quantity) -}} @@ -80,7 +80,24 @@ results are printed without a decimal point. {{- if eq $product (floor $product) -}} {{- printf "%d%s" (int64 $product) $suffix -}} {{- else -}} -{{- printf "%v%s" $product $suffix -}} +{{- printf "%s%s" (include "homeassistant.formatNumber" $product) $suffix -}} +{{- end -}} +{{- end -}} + +{{/* +Render a number as a plain decimal string, never in exponential notation. + +Why this exists (issue #157): the null-safe fallback helpers pass values +through fromYaml, which parses every number as float64. Go's "%v" switches +float64 to exponential notation from about 1e6 upwards, so a bare byte count +like 1048576 became "1.048576e+06" - not a valid Kubernetes quantity, and the +render aborted. Strings ("512Mi") are passed through untouched. +*/}} +{{- define "homeassistant.formatNumber" -}} +{{- if kindIs "float64" . -}} +{{- regexReplaceAll "\\.?0+$" (printf "%.10f" .) "" -}} +{{- else -}} +{{- printf "%v" . -}} {{- end -}} {{- end -}} diff --git a/patchmon/Chart.yaml b/patchmon/Chart.yaml index 95b0002..f06dc76 100644 --- a/patchmon/Chart.yaml +++ b/patchmon/Chart.yaml @@ -5,7 +5,7 @@ icon: https://raw.githubusercontent.com/PatchMon/PatchMon/main/frontend/public/a type: application -version: 7.0.0+up2.1.3 +version: 7.0.1+up2.1.3 appVersion: "2.1.3" maintainers: diff --git a/patchmon/templates/_helpers.tpl b/patchmon/templates/_helpers.tpl index eb9d1d5..5baae8e 100644 --- a/patchmon/templates/_helpers.tpl +++ b/patchmon/templates/_helpers.tpl @@ -67,7 +67,7 @@ Handles plain numbers ("2", "0.5", 1073741824), decimal ("1.5Gi"), binary results are printed without a decimal point. */}} {{- define "patchmon.multiplyQuantity" -}} -{{- $quantity := printf "%v" .quantity -}} +{{- $quantity := include "patchmon.formatNumber" .quantity -}} {{- $number := regexFind "^[0-9]+(\\.[0-9]+)?" $quantity -}} {{- if not $number -}} {{- fail (printf "cannot parse resource quantity %q" $quantity) -}} @@ -80,7 +80,24 @@ results are printed without a decimal point. {{- if eq $product (floor $product) -}} {{- printf "%d%s" (int64 $product) $suffix -}} {{- else -}} -{{- printf "%v%s" $product $suffix -}} +{{- printf "%s%s" (include "patchmon.formatNumber" $product) $suffix -}} +{{- end -}} +{{- end -}} + +{{/* +Render a number as a plain decimal string, never in exponential notation. + +Why this exists (issue #157): the null-safe fallback helpers pass values +through fromYaml, which parses every number as float64. Go's "%v" switches +float64 to exponential notation from about 1e6 upwards, so a bare byte count +like 1048576 became "1.048576e+06" - not a valid Kubernetes quantity, and the +render aborted. Strings ("512Mi") are passed through untouched. +*/}} +{{- define "patchmon.formatNumber" -}} +{{- if kindIs "float64" . -}} +{{- regexReplaceAll "\\.?0+$" (printf "%.10f" .) "" -}} +{{- else -}} +{{- printf "%v" . -}} {{- end -}} {{- end -}} diff --git a/rallly/Chart.yaml b/rallly/Chart.yaml index 4fc6c3a..463c7fd 100644 --- a/rallly/Chart.yaml +++ b/rallly/Chart.yaml @@ -5,8 +5,8 @@ icon: https://raw.githubusercontent.com/lukevella/rallly/main/apps/landing/publi type: application -version: 8.1.0+up4.13.0 -appVersion: "4.13.0" +version: 8.1.1+up4.13.1 +appVersion: "4.13.1" maintainers: - name: jklein \ No newline at end of file diff --git a/rss-bridge/Chart.yaml b/rss-bridge/Chart.yaml index bde18f1..f4170d0 100644 --- a/rss-bridge/Chart.yaml +++ b/rss-bridge/Chart.yaml @@ -5,7 +5,7 @@ icon: https://raw.githubusercontent.com/RSS-Bridge/rss-bridge/master/static/logo type: application -version: 1.0.1+up2025.08.05 +version: 1.0.2+up2025.08.05 appVersion: "2025-08-05" maintainers: diff --git a/rss-bridge/templates/_helpers.tpl b/rss-bridge/templates/_helpers.tpl index bab5acb..f05cc2a 100644 --- a/rss-bridge/templates/_helpers.tpl +++ b/rss-bridge/templates/_helpers.tpl @@ -63,7 +63,7 @@ Handles plain numbers ("2", "0.5", 1073741824), decimal ("1.5Gi"), binary results are printed without a decimal point. */}} {{- define "rss-bridge.multiplyQuantity" -}} -{{- $quantity := printf "%v" .quantity -}} +{{- $quantity := include "rss-bridge.formatNumber" .quantity -}} {{- $number := regexFind "^[0-9]+(\\.[0-9]+)?" $quantity -}} {{- if not $number -}} {{- fail (printf "cannot parse resource quantity %q" $quantity) -}} @@ -76,7 +76,24 @@ results are printed without a decimal point. {{- if eq $product (floor $product) -}} {{- printf "%d%s" (int64 $product) $suffix -}} {{- else -}} -{{- printf "%v%s" $product $suffix -}} +{{- printf "%s%s" (include "rss-bridge.formatNumber" $product) $suffix -}} +{{- end -}} +{{- end -}} + +{{/* +Render a number as a plain decimal string, never in exponential notation. + +Why this exists (issue #157): the null-safe fallback helpers pass values +through fromYaml, which parses every number as float64. Go's "%v" switches +float64 to exponential notation from about 1e6 upwards, so a bare byte count +like 1048576 became "1.048576e+06" - not a valid Kubernetes quantity, and the +render aborted. Strings ("512Mi") are passed through untouched. +*/}} +{{- define "rss-bridge.formatNumber" -}} +{{- if kindIs "float64" . -}} +{{- regexReplaceAll "\\.?0+$" (printf "%.10f" .) "" -}} +{{- else -}} +{{- printf "%v" . -}} {{- end -}} {{- end -}} diff --git a/searxng/Chart.yaml b/searxng/Chart.yaml index 3f0cf7e..28e1c69 100644 --- a/searxng/Chart.yaml +++ b/searxng/Chart.yaml @@ -9,7 +9,7 @@ type: application # (2026.8.22-9fea41204) and "latest" moves daily. appVersion is therefore the # exact tag, and the +up suffix normalizes the dash to a dot as the versioning # scheme requires - the same shape rss-bridge and minecraft-server use. -version: 1.0.0+up2026.8.22.9fea41204 +version: 1.0.1+up2026.8.22.9fea41204 appVersion: "2026.8.22-9fea41204" maintainers: diff --git a/searxng/templates/_helpers.tpl b/searxng/templates/_helpers.tpl index a2bee5f..a648573 100644 --- a/searxng/templates/_helpers.tpl +++ b/searxng/templates/_helpers.tpl @@ -67,7 +67,7 @@ Handles plain numbers ("2", "0.5", 1073741824), decimal ("1.5Gi"), binary results are printed without a decimal point. */}} {{- define "searxng.multiplyQuantity" -}} -{{- $quantity := printf "%v" .quantity -}} +{{- $quantity := include "searxng.formatNumber" .quantity -}} {{- $number := regexFind "^[0-9]+(\\.[0-9]+)?" $quantity -}} {{- if not $number -}} {{- fail (printf "cannot parse resource quantity %q" $quantity) -}} @@ -80,7 +80,24 @@ results are printed without a decimal point. {{- if eq $product (floor $product) -}} {{- printf "%d%s" (int64 $product) $suffix -}} {{- else -}} -{{- printf "%v%s" $product $suffix -}} +{{- printf "%s%s" (include "searxng.formatNumber" $product) $suffix -}} +{{- end -}} +{{- end -}} + +{{/* +Render a number as a plain decimal string, never in exponential notation. + +Why this exists (issue #157): the null-safe fallback helpers pass values +through fromYaml, which parses every number as float64. Go's "%v" switches +float64 to exponential notation from about 1e6 upwards, so a bare byte count +like 1048576 became "1.048576e+06" - not a valid Kubernetes quantity, and the +render aborted. Strings ("512Mi") are passed through untouched. +*/}} +{{- define "searxng.formatNumber" -}} +{{- if kindIs "float64" . -}} +{{- regexReplaceAll "\\.?0+$" (printf "%.10f" .) "" -}} +{{- else -}} +{{- printf "%v" . -}} {{- end -}} {{- end -}}