From 8189b74f55f92a04e92150a0dade1ff175728200 Mon Sep 17 00:00:00 2001 From: jklein Date: Sat, 29 Aug 2026 00:19:11 +0200 Subject: [PATCH] rallly: NOTES.txt and the #157 number formatting (8.1.0+up4.13.0) Two additive changes in one release. multiplyQuantity turned a bare integer resource value of about 1e6 or more into exponential notation and aborted the render with "unsupported resource quantity suffix \"e+06\"". The null-safe helpers from #99 pass values through fromYaml, which parses every number as float64, and Go's "%v" switches those to exponential form. The new rallly.formatNumber helper forces plain decimal notation on both the input and the non-integer product; it is the same wording already merged into template-chart, zipline and voucher-vault. The NOTES.txt prints what an operator would otherwise have to read out of the templates: the rendered access URL or the port-forward command, the fields each 1Password item has to carry, the OIDC redirect URI, and the traps of this chart - OIDC is the only way in because email login and registration are hard-coded off, a rotated 1Password item does not restart the pods, the probes only prove that the web process answers, and the Next.js cache emptyDir counts against the container's ephemeral-storage limit. Part of #157 Part of #43 --- rallly/Chart.yaml | 2 +- rallly/templates/NOTES.txt | 195 ++++++++++++++++++++++++++++++++++ rallly/templates/_helpers.tpl | 21 +++- 3 files changed, 215 insertions(+), 3 deletions(-) create mode 100644 rallly/templates/NOTES.txt diff --git a/rallly/Chart.yaml b/rallly/Chart.yaml index c64aa64..4fc6c3a 100644 --- a/rallly/Chart.yaml +++ b/rallly/Chart.yaml @@ -5,7 +5,7 @@ icon: https://raw.githubusercontent.com/lukevella/rallly/main/apps/landing/publi type: application -version: 8.0.1+up4.13.0 +version: 8.1.0+up4.13.0 appVersion: "4.13.0" maintainers: diff --git a/rallly/templates/NOTES.txt b/rallly/templates/NOTES.txt new file mode 100644 index 0000000..541372a --- /dev/null +++ b/rallly/templates/NOTES.txt @@ -0,0 +1,195 @@ +{{- $replicas := include "rallly.replicas" (dict "scaleDown" .Values.scaleDown "replicas" .Values.rallly.replicas) -}} +{{- $startup := fromYaml (include "rallly.startupProbe" .Values.rallly.startupProbe) -}} +{{- $limits := (fromYaml (include "rallly.effectiveResources" .Values.rallly.resources)).limits | default dict -}} +{{- $ephLimit := index $limits "ephemeral-storage" -}} +{{- $deployment := printf "%s-%s-deployment" .Release.Name .Chart.Name -}} +{{- $service := printf "%s-%s-service" .Release.Name .Chart.Name -}} +{{- $envNames := list -}} +{{- range $entry := .Values.rallly.env }}{{- $envNames = append $envNames $entry.name }}{{- end }} +Rallly {{ .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; + ingress.clusterIssuer is not needed. ingress.domain still is — see below. + + kubectl -n {{ .Release.Namespace }} port-forward svc/{{ $service }} 3000:80 + + Then open http://127.0.0.1:3000/. That is a health check, not a working + instance: the app still builds every link and the OIDC redirect from + NEXT_PUBLIC_BASE_URL, which is "https://{{ .Values.ingress.domain }}" regardless of how + you reached the port. A sign-in started over the forward leaves for that host. +{{- end }} + + ingress.domain is not only the Ingress host. The chart passes it to the app as + NEXT_PUBLIC_BASE_URL — upstream's "fully qualified public URL of your + instance" — which is why it stays required with the Ingress switched off. + +SIGN-IN GOES THROUGH THE PROVIDER, AND ONLY THROUGH THE PROVIDER + This chart sets EMAIL_LOGIN_ENABLED and REGISTRATION_ENABLED to "false" in the + template, not through a value: there is no magic-link login and no + self-registration to fall back on. Every account arrives through the OIDC + provider, shown on the login page as "{{ .Values.rallly.oidc.name }}". + A wrong OIDC item therefore does not degrade the login, it removes it — + including your own way back in. + + The redirect URI to register at the provider is + + https://{{ .Values.ingress.domain }}/api/auth/callback/oidc + + and the claim paths the app reads out of the token are + name="{{ .Values.rallly.oidc.nameClaimPath }}", email="{{ .Values.rallly.oidc.emailClaimPath }}", picture="{{ .Values.rallly.oidc.pictureClaimPath }}". + A provider that nests them somewhere else answers with a user who has no name + or no address; that is these three values, not the provider. +{{ if eq (printf "%v" .Values.rallly.allowedEmails) "*" }} + ALLOWED_EMAILS is "*", so every account the provider authenticates may sign in + and the only gate in front of this instance is the provider's own audience. + With a broad IdP that is an open door. Narrow it with a comma-separated list + in rallly.allowedEmails; wildcards are supported (e.g. *@example.com). +{{- else }} + ALLOWED_EMAILS is "{{ .Values.rallly.allowedEmails }}". + An address outside that list is rejected after a successful login at the + provider, so the symptom arrives one step away from its cause: the provider + reports success and Rallly still refuses. Comma-separated, wildcards + supported. +{{- end }} + + Admin rights are claimed by INITIAL_ADMIN_EMAIL from the secret below. It has + to match the address the provider returns in the email claim exactly — nothing + reconciles the two, and a mismatch simply leaves the instance without an admin. + +SECRETS — THE FIELDS EACH 1PASSWORD ITEM MUST CARRY + The CRs carry an item path only; the field names below are decided in this + chart and appear nowhere in 1Password, so they are what you would otherwise + read out of the templates. Every one of them is wired as a plain secretKeyRef + without "optional: true": a field missing from the item does not fail this + install — the operator writes a Secret without that key and the pod stays in + CreateContainerConfigError. The key has to exist; an empty value is accepted, + but the app then sees an empty string rather than its own default. + + {{ .Values.secrets.ralllySecretRef }} + → Secret {{ .Release.Name }}-{{ .Chart.Name }}-secret + secret-key SECRET_PASSWORD. Upstream requires at least 32 + characters. It encrypts the sessions, so changing + it signs everyone out. + initial-admin-email INITIAL_ADMIN_EMAIL, see above. + + {{ .Values.secrets.databaseSecretRef }} + → Secret {{ .Release.Name }}-{{ .Chart.Name }}-database-secret + database-url postgres://USER:PASSWORD@HOST:5432/DATABASE + + The chart brings no database. Postgres has to exist and be reachable from + this namespace before the release is of any use. + + {{ .Values.secrets.oidcSecretRef }} + → Secret {{ .Release.Name }}-{{ .Chart.Name }}-oidc-secret + discovery-url the provider's .well-known/openid-configuration + client-id + client-secret + + The discovery URL is fetched by the pod, not by your browser: an IdP that + only resolves outside the cluster, or one behind a policy this namespace + cannot pass, fails here while looking perfectly reachable from your desk. +{{- if .Values.rallly.smtp.enabled }} + + {{ .Values.secrets.smtpSecretRef }} + → Secret {{ .Release.Name }}-{{ .Chart.Name }}-smtp-secret + host port secure user password + no-reply-email no-reply-email-name tls-servername + + All eight are required keys here, including the three that are optional + upstream (port, secure, tls-servername) and no-reply-email, which upstream + would default to SUPPORT_EMAIL. An item carrying only host, user and + password wedges the pod before it starts. +{{- if not .Values.rallly.smtp.rejectUnauthorized }} + + rallly.smtp.rejectUnauthorized is false: the mail server's TLS certificate + is not verified and the SMTP session is open to a man-in-the-middle. That is + the setting for a self-signed certificate only — prefer trusting the CA, and + set it back to true once the server has a certificate that chains. +{{- end }} +{{- else }} + + MAIL IS OFF + rallly.smtp.enabled is false, so no SMTP item is created and the container + receives none of the SMTP_* variables — upstream lists SMTP_HOST among the + required ones. Nothing the app tries to send leaves this pod: invitations + and notification mails fail where they are sent, with a green rollout and a + healthy pod either way. Sign-in is unaffected, because email login is off. +{{- end }} + +ROTATING AN ITEM IN 1PASSWORD RESTARTS NOTHING + Helm never sees the content of a OnePasswordItem, so no checksum annotation + can cover these Secrets and this chart carries none. Change a field in + 1Password and the operator updates the Kubernetes Secret, while the running + pod keeps what it read at start — environment variables are read once. Roll it + yourself: + + kubectl -n {{ .Release.Namespace }} rollout restart deployment/{{ $deployment }} + + This bites hardest on database-url and the SMTP password: the app goes on + using the old credentials, and nothing in the cluster says so. + +WHAT A GREEN ROLLOUT DOES NOT PROVE + All three probes are the same plain HTTP GET "/" against the container. They + establish that the web process answers, and nothing beyond it: the OIDC + provider, the mail server and the addresses in ALLOWED_EMAILS are exercised by + the first real sign-in and the first real mail, never by a probe. + The startup budget before the container is restarted is + {{ $startup.periodSeconds }}s x {{ $startup.failureThreshold }} failures = {{ mul $startup.periodSeconds $startup.failureThreshold }}s, and every misconfiguration listed + above outlives it untouched. + +THE CACHE VOLUME COUNTS AGAINST THE EPHEMERAL-STORAGE LIMIT + Next.js writes prerendered pages and the optimized images of /_next/image to + {{ .Values.rallly.cache.path }}. + /app belongs to the image's own user while the container runs as uid 20000 + under a read-only root filesystem, so the chart mounts a writable emptyDir + over that path. +{{ if .Values.rallly.cache.sizeLimit }} + The volume is capped at {{ .Values.rallly.cache.sizeLimit }}{{ if $ephLimit }}, and its usage counts a second time against + the container's ephemeral-storage limit of {{ $ephLimit }}{{ end }}. Whichever bound is reached + first, the kubelet evicts the pod — which reads as a restart with no + application error anywhere in the log. +{{- else }} + The volume has no size limit of its own{{ if $ephLimit }}, so it is bounded only by the + container's ephemeral-storage limit of {{ $ephLimit }} — emptyDir usage counts against + that limit, and exceeding it evicts the pod, which reads as a restart with no + application error anywhere in the log{{ end }}. rallly.cache.sizeLimit sets an + explicit bound. +{{- end }} + The cache is volatile on purpose: after every restart it starts out empty and + is rebuilt on demand, at the cost of a slower first request. + +CHANGING THE DOMAIN LATER IS A CHANGE ON TWO SIDES + ingress.domain is the Ingress host, the app's NEXT_PUBLIC_BASE_URL and + therefore the OIDC redirect URI in one value. Editing it here alone breaks the + login: the provider refuses a callback it was never told about, and it is the + provider that reports it — Kubernetes reports a healthy pod{{ if .Values.ingress.enabled }} and a valid + certificate for the new name{{ end }}. Register the new redirect URI first, then + upgrade. +{{- if $envNames }} + +rallly.env ADDS {{ len $envNames }} VARIABLE(S): {{ join ", " $envNames }} + The block is additive — it is appended after the variables the chart sets + itself, and it cannot remove any of them. A name the chart already sets ends + up in the container spec twice; do not settle that by ordering, use the + chart's own value for it instead ({{ .Chart.Name }}.smtp, {{ .Chart.Name }}.oidc, {{ .Chart.Name }}.allowedEmails). +{{- end }} +{{- if eq $replicas "0" }} + +SCALED TO ZERO + {{ if .Values.scaleDown }}scaleDown: true{{ else }}rallly.replicas: 0{{ end }} — the Deployment runs with replicas: 0, so nothing answers + until you set it back and upgrade again. Secrets and Service{{ if .Values.ingress.enabled }}, and the Ingress + with its certificate,{{ end }} stay in place. +{{- end }} diff --git a/rallly/templates/_helpers.tpl b/rallly/templates/_helpers.tpl index e94a9c9..af5d7d5 100644 --- a/rallly/templates/_helpers.tpl +++ b/rallly/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 "rallly.multiplyQuantity" -}} -{{- $quantity := printf "%v" .quantity -}} +{{- $quantity := include "rallly.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 "rallly.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 "rallly.formatNumber" -}} +{{- if kindIs "float64" . -}} +{{- regexReplaceAll "\\.?0+$" (printf "%.10f" .) "" -}} +{{- else -}} +{{- printf "%v" . -}} {{- end -}} {{- end -}}