diff --git a/outline/Chart.yaml b/outline/Chart.yaml index 89280ac..bdd6ddd 100644 --- a/outline/Chart.yaml +++ b/outline/Chart.yaml @@ -5,7 +5,7 @@ icon: https://raw.githubusercontent.com/outline/outline/main/public/images/icon- type: application -version: 7.0.0+up1.9.2 +version: 7.1.0+up1.9.2 appVersion: 1.9.2 maintainers: diff --git a/outline/templates/NOTES.txt b/outline/templates/NOTES.txt new file mode 100644 index 0000000..be884da --- /dev/null +++ b/outline/templates/NOTES.txt @@ -0,0 +1,164 @@ +{{- $replicas := include "outline.replicas" (dict "scaleDown" .Values.scaleDown "replicas" .Values.outline.replicas) -}} +{{- $startup := fromYaml (include "outline.startupProbe" .Values.outline.startupProbe) -}} +{{- $upload := .Values.outline.maxUploadSize | toString -}} +Outline {{ .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.domain is still required: it becomes Outline's URL variable, which + builds every generated link, every share URL and the OIDC redirect URI. + Reach the instance inside the cluster with + + kubectl -n {{ .Release.Namespace }} port-forward svc/{{ .Release.Name }}-{{ .Chart.Name }}-service 8080:80 + + and open http://localhost:8080. Links on the page will still point at + https://{{ .Values.ingress.domain }}/, and an OIDC login will only complete if + that host actually resolves for the browser. +{{- end }} +{{ if eq $replicas "0" }} +SCALED TO ZERO + {{ if .Values.scaleDown }}scaleDown: true{{ else }}outline.replicas: 0{{ end }} — the StatefulSet runs with replicas: 0, so the endpoint + above does not answer until you set it back and upgrade again. PVC, Secrets, + Service{{ if .Values.ingress.enabled }} and Ingress stay in place, so neither the uploads nor the + certificate are touched.{{ else }} and the uploads on the PVC stay in place.{{ end }} +{{ end }} +WHAT THE THREE 1PASSWORD ITEMS MUST CONTAIN + The chart creates OnePasswordItem CRs; the operator turns each into a + Kubernetes Secret whose keys are the FIELD NAMES of the 1Password item. A + missing field does not fail the install — the release goes through and the + pod then hangs in CreateContainerConfigError naming the key it could not + find (kubectl -n {{ .Release.Namespace }} describe pod). + + secrets.configSecretRef → {{ .Values.secrets.configSecretRef }} + Secret {{ .Release.Name }}-{{ .Chart.Name }}-secret, fields: + outline-secret-key SECRET_KEY, 32 random bytes hex (openssl rand -hex 32) + outline-utils-secret UTILS_SECRET, a second, independent value + discovery-url OIDC issuer URL (the .well-known discovery endpoint) + client-id OIDC client id + client-secret OIDC client secret + + secrets.dbSecretRef → {{ .Values.secrets.dbSecretRef }} + Secret {{ .Release.Name }}-{{ .Chart.Name }}-db-secret, field: + connection-url postgres://user:password@host:5432/database + + secrets.redisSecretRef → {{ .Values.secrets.redisSecretRef }} + Secret {{ .Release.Name }}-{{ .Chart.Name }}-redis-secret, field: + connection-url redis://host:6379 + + On the identity provider's side the client needs this redirect URI: + + https://{{ .Values.ingress.domain }}/auth/oidc.callback + + The login button is labelled "{{ .Values.outline.oidc.displayName }}" (outline.oidc.displayName). + Nothing about OIDC is exercised before someone actually signs in, so a wrong + issuer, secret or redirect URI surfaces as a failed login on a healthy pod. + The first account to sign in creates the workspace and becomes its admin — + on a fresh install, sign in yourself before handing the URL to anyone. + +CHANGING A SECRET'S CONTENT RESTARTS NOTHING + Helm never sees the content of a OnePasswordItem, so it cannot hash it, and + this chart carries no checksum annotation for secrets (it has no ConfigMap + either). Pointing a *SecretRef at a DIFFERENT item rolls the pods; editing a + value INSIDE the same item does not — the operator updates the Kubernetes + Secret while the running container keeps the old value in its environment. + After such a rotation: + + kubectl -n {{ .Release.Namespace }} rollout restart statefulset/{{ .Release.Name }}-{{ .Chart.Name }}-sfs + + Rotating outline-secret-key invalidates existing sessions; rotating it + without keeping the old value also makes previously encrypted data + unreadable. Rotate the database or Redis URL and the pod restarts into + whatever that new endpoint contains, empty or not. + +POSTGRES AND REDIS ARE NOT PART OF THIS CHART + Both have to exist and be reachable before the first start. /_health checks + them, and all three probes use it, so a missing database does not produce a + useful error: the pod simply never turns Ready and the startup probe kills + it after about {{ mul $startup.failureThreshold $startup.periodSeconds }}s. Read the container log, not the pod events. + + The chart pins PGSSLMODE=disable next to the connection URL. That fits a + Postgres reached over the cluster network; a database anywhere less trusted + needs TLS asked for explicitly, and this chart has no value for it — the + outline.env block below is the place to set it. + +THE FIRST START MIGRATES, AND THE STARTUP PROBE IS THE CLOCK + Outline runs its database migrations when the container starts. The startup + probe grants {{ $startup.failureThreshold }} × {{ $startup.periodSeconds }}s = {{ mul $startup.failureThreshold $startup.periodSeconds }}s before the kubelet restarts the + container — and a restart lands in the middle of a migration. Raise + outline.startupProbe.failureThreshold BEFORE a large app-version jump or a + first import, not after the crash loop has started. + + Back up the database before any app-version bump. Migrations are one-way; a + helm rollback returns the old image to a database the new one has already + rewritten. + +ONE REPLICA, ONE VOLUME + Uploads are stored locally (FILE_STORAGE=local), on the ReadWriteOnce PVC + {{ .Release.Name }}-{{ .Chart.Name }}-pvc under /outline-data/data — there is no object storage in + this chart. That PVC is a plain chart object, not a StatefulSet + volumeClaimTemplate, so all replicas would share the single RWO volume and + the second pod usually never schedules. +{{- if gt (int $replicas) 1 }} + + outline.replicas is {{ $replicas }}. Expect the extra pods to stay Pending on + "Multi-Attach error for volume" unless the storage class is genuinely RWX. +{{- end }} + +BACKUP IS TWO THINGS AT THE SAME TIME + The database holds the documents and one row per attachment; the PVC holds + the attachment bytes. Restoring one without the other gives a wiki with + broken images or files nobody references. Snapshot them together. + + Because the PVC is rendered by the chart, helm uninstall deletes it and the + uploads with it, unless the storage class reclaim policy says otherwise. The + external database survives that — which makes an accidental uninstall look + recoverable while the attachments are already gone. +{{- if .Values.ingress.enabled }} + +TWO UPLOAD CEILINGS, THE SMALLER ONE WINS + outline.maxUploadSize {{ $upload }}{{ if regexMatch "^[0-9]+$" $upload }} bytes (~{{ div (int64 $upload) 1048576 }} MiB){{ else }} — Outline reads this as a plain byte count{{ end }} + ingress.maxUploadFileSize {{ .Values.ingress.maxUploadFileSize }} + + The smaller of the two decides, and the Ingress limit fails the more + confusingly: a file above it never reaches Outline, so the user gets the + ingress controller's 413 page instead of a message from the app. With the + chart defaults the Ingress is the tighter of the two by a factor of ten. + Raise both, or neither. +{{- else }} + +UPLOAD CEILING + outline.maxUploadSize {{ $upload }}{{ if regexMatch "^[0-9]+$" $upload }} bytes (~{{ div (int64 $upload) 1048576 }} MiB){{ else }} — Outline reads this as a plain byte count{{ end }} + + Without an Ingress this is the only limit the chart sets — whatever proxy + you put in front adds its own, usually much smaller, body-size cap. +{{- end }} + +THE CHART CONFIGURES NO MAIL + Outline sends invitations, document notifications and digest mails over + SMTP. This chart sets no SMTP_* variable at all, so those mails are quietly + never sent and nothing in the UI says so. The place to add them is + outline.env, which also supports the secretKeyRef form for the password. +{{- if .Values.outline.env }} + + outline.env currently adds {{ len .Values.outline.env }} variable(s), rendered after the ones the chart + sets itself. +{{- end }} +{{- if has (.Values.outline.forceHttps | toString) (list "true" "1") }} + +FORCE_HTTPS IS ON + outline.forceHttps is set. The chart defaults it to "false" because the + Ingress already terminates TLS and Outline trusts the forwarded protocol + header. Turned on, Outline redirects to https itself — which loops + endlessly if that header does not reach the pod. +{{- end }} diff --git a/outline/templates/_helpers.tpl b/outline/templates/_helpers.tpl index 6efd2e8..ee8a0cf 100644 --- a/outline/templates/_helpers.tpl +++ b/outline/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 "outline.multiplyQuantity" -}} -{{- $quantity := printf "%v" .quantity -}} +{{- $quantity := include "outline.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 "outline.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 "outline.formatNumber" -}} +{{- if kindIs "float64" . -}} +{{- regexReplaceAll "\\.?0+$" (printf "%.10f" .) "" -}} +{{- else -}} +{{- printf "%v" . -}} {{- end -}} {{- end -}}