From 65cb33e5f2884cdf2aad8fa0202e54d467300905 Mon Sep 17 00:00:00 2001 From: jklein Date: Sat, 29 Aug 2026 00:19:21 +0200 Subject: [PATCH] calibre-web-automated: add a NOTES.txt worth reading twice (6.1.0+upv4.0.6) Prints what an operator would otherwise dig out of the templates: the rendered access URL or the concrete port-forward, the fields the 1Password item has to carry, and the traps of this chart in particular - the public default login, the ingest folder that deletes what it imports, the fixed 1Gi ingest claim, the one shared library claim, and why a change inside the 1Password item rolls nothing (subPath mounts included). Conditional on the values that actually decide something: ingress on/off, scaled to zero, an unset secret ref, and extra env entries. Part of #43. --- calibre-web-automated/Chart.yaml | 2 +- calibre-web-automated/templates/NOTES.txt | 168 ++++++++++++++++++++++ 2 files changed, 169 insertions(+), 1 deletion(-) create mode 100644 calibre-web-automated/templates/NOTES.txt diff --git a/calibre-web-automated/Chart.yaml b/calibre-web-automated/Chart.yaml index 9d9df6a..836f7bf 100644 --- a/calibre-web-automated/Chart.yaml +++ b/calibre-web-automated/Chart.yaml @@ -5,7 +5,7 @@ name: calibre-web-automated description: A Helm chart to deploy calibere-web-automated icon: https://raw.githubusercontent.com/crocodilestick/Calibre-Web-Automated/main/README_images/cwa-logo-round-dark.png -version: 6.0.0+upv4.0.6 +version: 6.1.0+upv4.0.6 appVersion: v4.0.6 maintainers: diff --git a/calibre-web-automated/templates/NOTES.txt b/calibre-web-automated/templates/NOTES.txt new file mode 100644 index 0000000..3543977 --- /dev/null +++ b/calibre-web-automated/templates/NOTES.txt @@ -0,0 +1,168 @@ +{{- $replicas := include "calibre-web-automated.replicas" (dict "scaleDown" .Values.scaleDown "replicas" .Values.cwa.replicas) -}} +{{- $startup := fromYaml (include "calibre-web-automated.startupProbe" .Values.cwa.startupProbe) -}} +{{- $sfs := printf "%s-%s-sfs" .Release.Name .Chart.Name -}} +{{- $pod := printf "%s-0" $sfs -}} +Calibre-Web Automated {{ .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 + + Uploads are capped at {{ .Values.cwa.maxUploadFileSize }} by the Ingress, not by the application: the + chart renders that value into nginx.ingress.kubernetes.io/proxy-body-size and + nginx.org/client-max-body-size. A bigger book is rejected with 413 by the + controller before CWA ever sees it, so nothing about it reaches the app log. + Both annotations are understood by the nginx-family controllers only, and this + release uses ingressClassName "{{ .Values.ingress.className }}". +{{- else }} + +ACCESS — NO INGRESS + ingress.enabled is false, so this release has no Ingress and no certificate. + Neither ingress.domain nor ingress.clusterIssuer is needed: this chart reads + ingress.domain in the Ingress template and nowhere else. The app is reachable + inside the cluster as {{ .Release.Name }}-{{ .Chart.Name }}-service:80. To open it locally: + + kubectl -n {{ .Release.Namespace }} port-forward svc/{{ .Release.Name }}-{{ .Chart.Name }}-service 8083:80 + + Then browse to http://localhost:8083. cwa.maxUploadFileSize ({{ .Values.cwa.maxUploadFileSize }}) does + nothing in this mode — it exists only as an Ingress annotation, so an upload + through the port-forward is limited by the volume and by nothing else. +{{- end }} +{{ if eq $replicas "0" }} +SCALED TO ZERO + {{ if .Values.scaleDown }}scaleDown: true{{ else }}cwa.replicas: 0{{ end }} — the StatefulSet runs with replicas: 0, so nothing + answers until you set it back and upgrade again. Both PVCs, the Secret and the + Service{{ if .Values.ingress.enabled }} and Ingress stay in place, so no data is lost and the certificate + is not re-issued.{{ else }} stay in place, so no data is lost.{{ end }} +{{ end }} +FIRST LOGIN — THE DEFAULT PASSWORD IS PUBLIC + Username "admin", password "admin123". Those are the upstream defaults, they + stand in CWA's README, and this chart does not change them — the 1Password item + below carries no admin credentials. Until you change it under Admin -> Users, + everyone who reaches the address above is one guess away from being + administrator. + +THE FIRST START BUILDS A LIBRARY, AND THE STARTUP PROBE IS TIMING IT + Against an empty volume CWA notices that there is no Calibre library under + /calibre-library and creates one itself, while the s6 init fixes ownership of + /config. That first boot takes minutes where every later one takes seconds. + + The startup probe grants it {{ $startup.periodSeconds }}s x {{ $startup.failureThreshold }} failures = {{ mul $startup.periodSeconds $startup.failureThreshold }}s. On a slow volume + the container is killed and restarted before it is finished, which presents as + CrashLoopBackOff and is really a budget that is too small: raise + cwa.startupProbe.failureThreshold, and watch the boot instead of guessing. + + kubectl -n {{ .Release.Namespace }} logs {{ $pod }} -f + +SECRETS — THE FIELDS THE 1PASSWORD ITEM MUST CARRY + {{ if .Values.secrets.cwaSecretRef }}{{ .Values.secrets.cwaSecretRef }}{{ else }}(secrets.cwaSecretRef is unset — see below){{ end }} + → Secret {{ .Release.Name }}-{{ .Chart.Name }}-secret + hardcover-api-token becomes the HARDCOVER_TOKEN environment variable + calibre-plugins mounted as /config/.config/calibre/customize.py.json, + Calibre's plugin registry; "{}" is a valid empty value + + These field names are decided by this chart and appear nowhere in 1Password, so + they are exactly what you would otherwise go and read in the templates. Both + are required even if you never use Hardcover: the OnePasswordItem is created + unconditionally, and so are the environment reference and the volume. A missing + hardcover-api-token parks the pod in CreateContainerConfigError; a missing + calibre-plugins fails the mount instead ("couldn't find key calibre-plugins"), + and the container is never created at all. +{{- if not .Values.secrets.cwaSecretRef }} + + secrets.cwaSecretRef IS UNSET. The OnePasswordItem has been created with an + empty itemPath, the operator has nothing to resolve, and the Secret above will + never appear — the pod cannot start. Set it to the item's op:// path and + upgrade. +{{- end }} + +CHANGING THE ITEM'S CONTENT RESTARTS NOTHING + Helm never sees what is inside a OnePasswordItem, so it cannot hash it into the + pod template: after you edit the item, "helm upgrade" finds no change and the + running pod keeps the old values. Pointing secrets.cwaSecretRef at a DIFFERENT + item does not help either — the Secret's name is built from the release and + chart name, so the pod spec stays byte-identical. Roll it by hand: + + kubectl -n {{ .Release.Namespace }} rollout restart statefulset/{{ $sfs }} + + For the plugin file that is the only way: it is mounted with subPath, and a + subPath mount is never refreshed by the kubelet, not even when the Secret + itself has been updated. HARDCOVER_TOKEN is an environment variable and is + likewise read once, at start. + +THE INGEST FOLDER DELETES WHAT YOU PUT INTO IT + Books copied into /cwa-book-ingest are imported, converted, and then removed. + Upstream is blunt about it: "All files within this folder will be DELETED after + being processed." It is a drop box, never a second copy of the library — put + nothing there that does not exist somewhere else. + + Downloading straight into it is unsupported for the same reason it sounds + convenient: a half-written file gets picked up, which upstream warns "can cause + duplicate imports and potentially a corrupt database". Copy finished files in. + + The folder is the ReadWriteMany claim {{ .Chart.Name }}-{{ .Release.Name }}-cwa-consume-pvc, + and its size is fixed at 1Gi inside the chart — there is no value for it. An + import batch larger than that fills the volume, and raising it means changing + the chart, not the values file. + +EVERYTHING ELSE SHARES ONE CLAIM + {{ .Chart.Name }}-{{ .Release.Name }}-cwa-pvc ({{ .Values.cwa.storage }} on {{ if .Values.cwa.storageClass }}storage class "{{ .Values.cwa.storageClass }}"{{ else }}the cluster's default storage class{{ end }}) + is mounted three times by subPath: config/ holds the application state, + calibre-library/ the books and metadata.db, calibre-plugins/ the Calibre + plugins. A growing library and the application's own database therefore draw on + one quota, and a full volume does not merely stop imports — it takes the app + down. That claim is also the entire backup: metadata.db without the book files + is an index of nothing. + +THIS RELEASE IS SINGLE-INSTANCE BY CONSTRUCTION + The StatefulSet mounts both claims by fixed name instead of using + volumeClaimTemplates, so every replica would mount the same volumes — and the + library claim is ReadWriteOnce. A second pod stays Pending unless it happens to + land on the node holding the volume, and where it does start, two CWA processes + write one SQLite metadata.db. Raising cwa.replicas above 1 buys nothing. + +THE HARDENING HERE IS DELIBERATELY NOT THE REPO STANDARD + The image boots through the s6 init as root and only then drops the application + to PUID/PGID 1000. Hence no runAsNonRoot/runAsUser, a writable root filesystem, + and a capabilities.add that is not empty. Two of those look like oversights: + + readOnlyRootFilesystem: true switches the image's PUID/PGID remapping off. + The application then runs as the built-in uid 911 and no longer has access to + what uid 1000 wrote, so an existing library goes quietly unreadable. + + Without KILL the root supervisor cannot signal its own uid-1000 services. The + pod stops shutting down cleanly and is SIGKILLed when the grace period ends. + + PUID and PGID are pinned to 1000 in the template, not in the values, and the + pod's fsGroup default of 1000 is what matches them. Changing + securityContext.pod.fsGroup on its own takes write access away from the app. + +WHAT A READY POD DOES NOT PROVE + All three probes GET "/", and CWA answers that with its login page no matter + what else is wrong. Ready therefore says the web server is up — not that the + library was found, not that one import has ever succeeded, and not that the + Hardcover token is accepted. Those show in the log, and only there. + +TIME, AND THE ENVIRONMENT THE CHART ALREADY OCCUPIES + TZ is "{{ .Values.cwa.timezone }}", which decides the log timestamps and when CWA's scheduled + tasks run. Besides TZ the chart sets PUID, PGID, HARDCOVER_TOKEN, + CALIBRE_CONFIG_DIR and NETWORK_SHARE_MODE itself, and anything in cwa.env is + appended after those. +{{- if .Values.cwa.env }} + There {{ if eq (len .Values.cwa.env) 1 }}is 1 such entry{{ else }}are {{ len .Values.cwa.env }} such entries{{ end }} in this release, and the chart does not check + whether one of them repeats a name it has already used — nor does Kubernetes + reject the repetition. +{{- end }} + + NETWORK_SHARE_MODE is pinned to "false", which leaves SQLite write-ahead + logging switched on. Upstream's instruction for the other case is explicit — + "If you are deploying on a network share, set the following environment + variable to disable WAL: NETWORK_SHARE_MODE=true" — so a library claim backed + by NFS or SMB, which is what cwa.storageClass decides, runs into exactly the + locking problems that setting exists to avoid.