diff --git a/README.md b/README.md index 08c33c0..8e5c2d0 100644 --- a/README.md +++ b/README.md @@ -115,7 +115,7 @@ If the dependency dashboard reports `Failed to look up github-releases package ## Creating a new chart -1. Copy `template-chart/` to `/` and replace every `xxx` placeholder: template file names (`xxx.deployment.yaml` → `.deployment.yaml`), `Chart.yaml`, the `values.yaml` keys, the `.Values.xxx.*` references inside the templates, **and both the `xxx.` helper prefixes and the values paths in `values.schema.json`**. Then walk the five points in "Values that must not lie" below — the schema and the null-safe fallbacks are copied with the template, but their content is chart-specific and wrong until you adapt it. +1. Copy `template-chart/` to `/` and replace every `xxx` placeholder: template file names (`xxx.deployment.yaml` → `.deployment.yaml`), `Chart.yaml`, the `values.yaml` keys, the `.Values.xxx.*` references inside the templates, **and both the `xxx.` helper prefixes and the values paths in `values.schema.json`**. Then walk the five points in "Values that must not lie" below — the schema and the null-safe fallbacks are copied with the template, but their content is chart-specific and wrong until you adapt it. The same goes for `templates/NOTES.txt`: it comes with the template as a worked example and is placeholder text until you replace it with what an operator of *your* app needs after the install (see "Everything else" below); its leading `{{/* … */}}` block explains the bar and is meant to be deleted. 2. Add `.github/workflows/publish-.yml` (copy an existing one, e.g. `publish-outline.yml`). 3. Add the chart name to the choice list in `.github/workflows/change-app-version.yml`. 4. Add a `customManagers` block for the chart's main image in `renovate.json5` (copy a neighbouring one, swap `managerFilePatterns` and `depNameTemplate`) — without it the chart's app version is never updated. Check the image's tag list first: if it carries a flavour, date or prerelease suffix, add a `versioningTemplate` with a comment saying why. @@ -256,6 +256,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); eight charts ship one so far (`apache-tika`, `cyberchef`, `freesailarr`, `gotenberg`, `homeassistant`, `patchmon`, `rss-bridge`, `searxng`). +- `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. - 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/template-chart/Chart.yaml b/template-chart/Chart.yaml index 3cf9a13..e636b77 100644 --- a/template-chart/Chart.yaml +++ b/template-chart/Chart.yaml @@ -5,7 +5,7 @@ icon: https://helm.sh/img/helm.svg type: application -version: 4.0.0+upxxx +version: 4.1.0+upxxx appVersion: "xxx" maintainers: diff --git a/template-chart/templates/NOTES.txt b/template-chart/templates/NOTES.txt new file mode 100644 index 0000000..4d9aa4e --- /dev/null +++ b/template-chart/templates/NOTES.txt @@ -0,0 +1,146 @@ +{{/* +NOTES.txt — the reference implementation for issue #43. + +Copy this file with the rest of template-chart and replace every xxx: the +values paths (.Values.xxx.*), the helper prefix ("xxx.replicas") and the app +name in the headline, exactly as in the other template files. This comment +block renders nothing and is meant to be deleted along the way. + +THE STANDARD THIS FILE IS MEASURED AGAINST. A NOTES.txt that repeats the +values is worthless - it is skimmed once after the first install and never +read again. Print what an operator would otherwise have to look up in the +templates: the rendered access URL, WHICH FIELDS the 1Password item has to +carry, the options that are actually active in THIS release, and the traps of +this chart. Keep it conditional on the values that are really set, so the +output describes the installation at hand instead of every possible one. + +NEVER PUT A REAL DOMAIN IN HERE. The URL is rendered from +.Values.ingress.domain, so it comes from the operator's values and not from +the chart; where an example is unavoidable, use example.com. + +Note for the copier: a NOTES.txt renders no Kubernetes object, so adding one +is purely additive - a MINOR bump, and safe to roll out at any time. +*/}} +{{- $replicas := include "xxx.replicas" (dict "scaleDown" .Values.scaleDown "replicas" .Values.xxx.replicas) -}} +xxx {{ .Chart.AppVersion }} — release {{ .Release.Name }} in namespace {{ .Release.Namespace }}. + +{{- if .Values.ingress.enabled }} + +ACCESS + https://{{ .Values.ingress.domain }} + + The certificate is requested by cert-manager through the "{{ .Values.ingress.clusterIssuer }}" + cluster issuer. Until it has been issued the browser shows a warning; follow it with + + kubectl -n {{ .Release.Namespace }} get certificate tls-{{ .Release.Name }}-{{ .Chart.Name }}-ingress +{{- else }} + +ACCESS — NO INGRESS + ingress.enabled is false, so this release has no Ingress and no certificate, + and ingress.clusterIssuer is not needed. ingress.domain STAYS REQUIRED even + so: the env block in values.yaml reads it for NORMAL_ENV, which is why that + read carries a required() of its own. Without it the workload would start + with an empty address and nothing would point at the mistake. + + Reach the release inside the cluster with + + kubectl -n {{ .Release.Namespace }} port-forward svc/{{ .Release.Name }}-{{ .Chart.Name }}-service 8080:80 + + and open http://localhost:8080. +{{- end }} +{{ if eq $replicas "0" }} +SCALED TO ZERO + {{ if .Values.scaleDown }}scaleDown: true{{ else }}xxx.replicas: 0{{ end }} — the workload runs with replicas: 0, so the endpoint + above does not answer until you set it back and upgrade again. PVC, Secret{{ if .Values.ingress.enabled }}, + Service and Ingress stay in place, so the certificate is not re-issued.{{ else }} and + Service stay in place.{{ end }} +{{ end }} +THE 1PASSWORD ITEM MUST CARRY THE FIELDS THE CHART READS + secrets.xxxSecretRef points at + {{ .Values.secrets.xxxSecretRef }} + The 1Password operator materializes that item as the Kubernetes Secret + {{ .Release.Name }}-{{ .Chart.Name }}-secret, with one Secret key per field of the item. So the + fields are not free-form: this chart reads the field + + secretKey (values.yaml, env entry ENV_FROM_SECRET) + + and an item without it leaves the container stuck in + CreateContainerConfigError. Watch the materialization with + + kubectl -n {{ .Release.Namespace }} get onepassworditem {{ .Release.Name }}-{{ .Chart.Name }}-secret + kubectl -n {{ .Release.Namespace }} get secret {{ .Release.Name }}-{{ .Chart.Name }}-secret + + When you copy this chart, list YOUR chart's fields here. This is the one + piece of information that is otherwise only findable by reading the + templates, and it is the reason this section exists. + +CHANGING THE SECRET DOES NOT RESTART ANYTHING HERE + Neither editing the values inside the 1Password item nor repointing + secrets.xxxSecretRef at a different item rolls the pods: this chart stamps + checksum/config over the ConfigMap, but no checksum/secret-ref over the + rendered OnePasswordItem. Helm never sees a secret's content, so the content + case cannot be closed at all — but the ref case can, and a chart that needs + it adds the second annotation (README, "Config changes must restart the + workload"). Until then, after any secret change: + + kubectl -n {{ .Release.Namespace }} rollout restart deployment/{{ .Release.Name }}-{{ .Chart.Name }}-deployment + +CONFIGURATION + {{ .Release.Name }}-{{ .Chart.Name }}-configmap is mounted into the workload, and the pod template + carries checksum/config over it — so a changed ConfigMap really does roll the + pods. That annotation is not decoration: a subPath mount is never refreshed + by the kubelet, and most apps read their config only at start, so without it + a config change would apply to nothing while everything stayed green. + + The hash is taken over the rendered ConfigMap, not over a timestamp: an + unchanged config yields an identical hash and the upgrade stays a no-op. + +STORAGE + {{ .Release.Name }}-{{ .Chart.Name }}-pvc, {{ .Values.storage.amount }}, {{ if .Values.storage.storageClass }}storage class "{{ .Values.storage.storageClass }}"{{ else }}on the cluster's default storage class + (storage.storageClass is unset){{ end }}. + + helm uninstall does NOT delete this PVC, which is deliberate — the data + survives a reinstall of the same release name. It also means a release + removed and recreated under a DIFFERENT name starts on an empty volume while + the old one keeps occupying space. + +WHAT A GREEN POD PROVES + All three probes are HTTP GETs against "/" on the port named http. They + prove that something answers on that port — not that the application is + functional, and not that anything behind the Ingress works, because probes + go straight to the pod and never pass through it. + + When you copy this chart, point the probes at a real health endpoint and + carry the probe's CONTEXT into the fallback defaults in _helpers.tpl, not + just its timings: a default that loses a port name, a Host header or an exec + command leaves the workload with a probe that cannot succeed. + +THIS CHART IS A SCAFFOLD — IT IS NOT MEANT TO RUN + If you are reading this from an actual install of template-chart, three + things will keep it from becoming Ready, all of them on purpose: + + - The image is the placeholder "ghcr.io/ORG/IMAGE:{{ .Chart.AppVersion }}" and does not + exist, so the pods stay in ImagePullBackOff. This is also why the chart + sits in ci/excluded-charts.txt and is never published. + - The chart renders BOTH a Deployment and a StatefulSet, as the two shapes + to choose between. Delete the one your app does not need; the Service + selects the Deployment's pods, so a stateful app has to move that selector + along with it. + - The env block in values.yaml demonstrates the three supported forms + (value, secretKeyRef, configMapKeyRef) — it is an example, not a working + configuration. Note in particular that ENV_FROM_CONFIGMAP names + "{{ .Release.Name }}-{{ .Chart.Name }}-config-map" while the ConfigMap this chart creates is + "{{ .Release.Name }}-{{ .Chart.Name }}-configmap": replace the whole block with your app's real + variables rather than repairing the example. +{{- if .Values.ingress.enabled }} +{{- with .Values.ingress.annotations }} + +EXTRA INGRESS ANNOTATIONS ARE ACTIVE +{{- range $key, $value := . }} + {{ $key }}: {{ $value }} +{{- end }} + These are merged with the cert-manager cluster-issuer annotation the chart + always sets. Annotations are interpreted by the ingress controller, so a + misspelled key is silently ignored — it does not fail the render. +{{- end }} +{{- end }} diff --git a/template-chart/templates/_helpers.tpl b/template-chart/templates/_helpers.tpl index f157873..410c0fb 100644 --- a/template-chart/templates/_helpers.tpl +++ b/template-chart/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 "xxx.multiplyQuantity" -}} -{{- $quantity := printf "%v" .quantity -}} +{{- $quantity := include "xxx.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 "xxx.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 "xxx.formatNumber" -}} +{{- if kindIs "float64" . -}} +{{- regexReplaceAll "\\.?0+$" (printf "%.10f" .) "" -}} +{{- else -}} +{{- printf "%v" . -}} {{- end -}} {{- end -}}