{{- $replicas := include "gotenberg.replicas" (dict "scaleDown" .Values.scaleDown "replicas" .Values.gotenberg.replicas) -}} {{- /* effectiveResources, not resources: an erased "resources:" block falls back to the chart defaults in the Deployment (#99), so the notes must report the limits the release actually gets, not "(none set)". */ -}} {{- $limits := (include "gotenberg.effectiveResources" .Values.gotenberg.resources | fromYaml).limits | default dict -}} {{- $svc := printf "%s-%s-service" .Release.Name .Chart.Name -}} Gotenberg {{ .Chart.AppVersion }} — release {{ .Release.Name }} in namespace {{ .Release.Namespace }}. ACCESS This chart creates no Ingress. The API is cluster-internal: http://{{ $svc }}.{{ .Release.Namespace }}.svc.cluster.local Mind the port. The Service listens on 80 and forwards to the container's 3000, so consumers are configured without a port — e.g. paperless-ngx: PAPERLESS_TIKA_GOTENBERG_ENDPOINT: http://{{ $svc }}.{{ .Release.Namespace }}.svc.cluster.local To reach it from your machine: kubectl -n {{ .Release.Namespace }} port-forward svc/{{ $svc }} 3000:80 curl http://localhost:3000/health The health route reports Chromium and LibreOffice separately — it is the quickest way to tell a broken converter from a broken caller. This chart has no secrets and no volumes. {{ if eq $replicas "0" }} SCALED TO ZERO {{ if .Values.scaleDown }}scaleDown: true{{ else }}gotenberg.replicas: 0{{ end }} — the deployment runs with replicas: 0. The Service keeps its ClusterIP but has no endpoints, so callers fail with "connection refused" rather than a timeout. Set it back and upgrade to return. {{ end }} NO AUTHENTICATION, AND THIS CHART CANNOT ADD ANY Gotenberg's basic auth and OIDC, its SSRF guards for the downloadFrom feature, and its request timeout are all command-line flags. This chart passes no flags — it only offers gotenberg.env — so none of them can be switched on through values. Everything that reaches the Service may convert, and the URL routes make the server fetch whatever URL they are handed. Keep it cluster-internal; do not put an Ingress in front of it without auth. BIG CONVERSIONS RUN OUT OF TIME BEFORE THEY RUN OUT OF MEMORY Chromium and LibreOffice hold the whole document in memory, so a conversion is a short, memory-hungry burst. This release limits the container to memory: {{ index $limits "memory" | default "(none set)" }} ephemeral-storage: {{ index $limits "ephemeral-storage" | default "(none set)" }} against deliberately smaller requests: the pod reserves little while it idles and grows into the limits only while converting. Those limits are set so the 30-second request timeout below is what a too-large document hits first. An 18 MB HTML converts in about 22 seconds and stays well inside them; a document heavy enough to exhaust the memory limit would have run past the 30 seconds anyway, so raising memory further does not turn a failing conversion into a successful one — splitting the document does. If you do need to change them, set gotenberg.resources.limits.memory and .limits.ephemeralStorage. Both are set explicitly now, so raising the matching request no longer drags the limit along at limitFactor x request. Should a conversion still hit a limit, the two failures look different and neither of them reports an HTTP error to the caller: - Out of memory: the connection is dropped mid-request and the pod's RESTARTS counter goes up. Confirm with kubectl -n {{ .Release.Namespace }} get pod -l app={{ .Release.Name }}-{{ .Chart.Name }}-deployment -o jsonpath='{.items[*].status.containerStatuses[*].lastState.terminated.reason}' which then prints OOMKilled. - Out of ephemeral storage: /tmp and /home/gotenberg are emptyDirs and count against the ephemeral-storage limit — Chromium's profile alone takes about 4 MB, and the uploaded file plus the rendered PDF come on top. The pod does not restart, it disappears: a fresh one takes its place and the old one is left in status Evicted, with the message naming ephemeral-storage. REQUESTS TIME OUT AFTER 30 SECONDS That is Gotenberg's own default, and since the chart passes no flags it cannot be changed here. This is the ceiling large conversions actually meet: an 18 MB HTML needs about 22 seconds, and a 25 MB one does not finish inside the 30. Callers that need longer must split the document instead. {{- if .Values.networkPolicy.enabled }} NETWORKPOLICY IS ACTIVE — WHAT IT CAPS, AND WHAT IT DOES NOT {{ .Release.Name }}-{{ .Chart.Name }}-networkpolicy selects this workload's pods. Inbound: TCP 3000 only. Outbound: DNS{{ with .Values.networkPolicy.egress }}{{ with .extraPorts }} plus TCP {{ join ", " . }}{{ end }}{{ end }}. {{- /* Null-safe on purpose: networkPolicy.egress may be erased to null (the schema permits it), and a bare .egress.remoteAssets then fails the whole render with "nil pointer evaluating interface {}.remoteAssets" - an error naming this file while the operator's value appears nowhere in it. Same rule as issue #99: an erased block means "the default applies", not a broken render. */}} {{- if (.Values.networkPolicy.egress | default dict).remoteAssets }} REMOTE ASSETS ARE ALLOWED IN THIS RELEASE (networkPolicy.egress.remoteAssets: true), so Chromium may fetch over TCP 80 and 443 to any destination. Know what that opens: this service is reachable from the whole cluster by default (3000 is the probe port), so anything that can POST here can now have Chromium fetch an arbitrary URL — including one inside the cluster. Switch it back off if nothing in this release renders HTML that references remote images. {{- else }} EGRESS IS DNS ONLY HERE, AND IT COSTS SOMETHING SILENTLY. Gotenberg drives a headless Chromium, so an egress rule for 80/443 would turn a service the whole cluster can POST to into a way of fetching arbitrary URLs — which is why the default is off. What it costs: in a rendered HTML document, EMBEDDED images are MIME parts inside the file and convert fine, but REMOTE images () are fetched by Chromium at render time and are SILENTLY DROPPED. The PDF is produced and is simply missing them. Nothing fails, nothing is logged here, no probe notices. If this release needs them — a paperless-ngx rendering mail bodies that reference remote pictures is the case — set networkPolicy: egress: remoteAssets: true and not extraPorts: [80, 443]. The named value says why the ports are open; a port list cannot. {{- end }} THIS CHART HAS NO INGRESS AND ITS CONSUMERS ARE OTHER PODS. While probeSources is empty the 3000 rule has no source selector; narrowing it means listing the consumers' namespaces in allowedNamespaces in the same change, or every conversion stops. Three limits belong in the same breath, because none of them is visible in the object itself: - EGRESS IS BY PORT, NEVER BY DESTINATION. A NetworkPolicy matches CIDRs and selectors and cannot express a hostname, so every egress rule permits its port to ANY address. That is a limit of the object, not a shortcut — this cluster's CNI (Canal: Flannel plus Calico) has no FQDN rule either. An outbound port the chart does not know about is BLOCKED: add it to networkPolicy.egress.extraPorts, and expect to find out through an application-level connection error rather than through this object. {{- if .Values.networkPolicy.probeSources }} - THE PROBE PATH IS RESTRICTED in this release, because networkPolicy.probeSources is set to {{ join ", " .Values.networkPolicy.probeSources }} Probes come from the IP of the WORKER NODE running the pod. If none of those CIDRs covers the node this pod lands on, the kubelet's probes are dropped and THE POD NEVER BECOMES READY — the rollout then times out and the symptom looks like a broken application. Compare kubectl -n {{ .Release.Namespace }} describe pod -l app={{ .Release.Name }}-{{ .Chart.Name }}-deployment against `kubectl get node -o wide`. {{- else }} - THE PROBED PORT IS OPEN INSIDE THE CLUSTER. networkPolicy.probeSources is empty, so that rule carries no source selector at all and any pod the CNI routes to can reach it. Not from the internet — but this is NOT pod-level isolation. Narrowing it means listing the worker node IPs in networkPolicy.probeSources, because the kubelet probes from the node's own address and the chart cannot know it; a wrong guess there leaves the pod permanently un-Ready. {{- end }} - IT IS ONLY A BOUNDARY WHERE THE CNI ENFORCES POLICIES. Where it does not, this object is accepted and filters nothing, so everything reads as configured while nothing is filtered. `kubectl -n {{ .Release.Namespace }} get networkpolicy` shows the object exists; only a connection attempt from an unrelated namespace shows that it bites. Switch it off with networkPolicy.enabled: false. {{- end }}