sure: new chart for the we-promise Sure app (1.0.0+up0.7.4) #36

Merged
jklein merged 3 commits from feat/sure-chart into main 2026-09-26 23:08:56 +02:00
3 changed files with 89 additions and 6 deletions
Showing only changes of commit e92286b420 - Show all commits

No files matched your search

+7
View File
@@ -336,6 +336,13 @@ probe on "/" reach the database at all (see values.yaml).
value: "true"
- name: RAILS_ENV
value: "production"
# Not cosmetic and not optional: /tmp is an emptyDir, which Kubernetes creates
# mode 0777 without a sticky bit, and Ruby skips exactly that shape when it
# looks for a temporary directory. Without this the app aborts during boot with
# "could not find a temporary directory". The directory is created by the
# init container in the StatefulSet, which explains the whole case.
- name: TMPDIR
value: "/tmp/ruby"
- name: PORT
value: "3000"
# Sure's production.rb defaults BOTH of these to true. The chart renders them
+70 -2
View File
@@ -34,6 +34,71 @@ spec:
automountServiceAccountToken: false
securityContext:
{{- include "sure.podSecurityContext" .Values.sure.securityContext | nindent 8 }}
initContainers:
# Ruby refuses a world-writable directory that has no sticky bit as its
# temporary directory (ruby 3.4, tmpdir.rb: a candidate is skipped when
# "world_writable? && !sticky?"), and a Kubernetes emptyDir is created
# mode 0777 WITHOUT the sticky bit. With /tmp mounted as an emptyDir and
# the image's root filesystem read-only, Ruby then finds no candidate at
# all and both containers die during boot - measured in this chart's
# first install-test, which logged
#
# system temporary path is world-writable: /tmp
# /tmp is world-writable: /tmp
# . is not writable: /rails
# ArgumentError: could not find a temporary directory
#
# raised from MiniMagick, which Active Storage loads while Rails boots.
# Note where it does NOT come from: nothing in the app asked for a temp
# file yet.
#
# The fix is a directory that is writable without being world-writable.
# This init container creates one per app container inside the same
# emptyDirs (mode 0700, owned by the pod's uid) and both containers get
# TMPDIR pointed at it. It runs as the same non-root uid as the app and
# needs no capability at all: the emptyDir root is 0777, so creating an
# entry in it is allowed, and chmod on a directory one has just created
# needs no privilege. That is deliberately weaker than the chown-as-root
# init container paperless-ngx needs.
#
# The image is the app's own, so there is no second image to pin, no
# Renovate block to maintain for it and nothing extra to pull.
- name: {{ .Release.Name }}-{{ .Chart.Name }}-init-tmp
image: "ghcr.io/we-promise/sure:{{ .Chart.AppVersion }}"
imagePullPolicy: IfNotPresent
# command, not args: the image entrypoint would start the application.
command:
- /bin/bash
- -ec
- |
for dir in /web-tmp/ruby /worker-tmp/ruby; do
mkdir -p "$dir"
chmod 700 "$dir"
done
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
runAsNonRoot: true
capabilities:
drop:
- ALL
volumeMounts:
- name: web-tmp
mountPath: /web-tmp
- name: worker-tmp
mountPath: /worker-tmp
# Fixed, deliberately small: this container runs two mkdirs and two
# chmods. Not values-configurable, for the same reason as the init
# container in paperless-ngx - there is nothing here to tune.
resources:
requests:
cpu: 10m
memory: 32Mi
ephemeral-storage: 1Mi
limits:
cpu: 100m
memory: 64Mi
ephemeral-storage: 10Mi
containers:
# The web process. Its command is the image's default
# (CMD ["./bin/rails", "server"]), which is exactly what makes the
@@ -74,8 +139,11 @@ spec:
# between the two processes, and a shared scratch directory would
# only create coupling nobody asked for.
#
# /tmp: Rack writes multipart uploads (CSV imports, attachments) to
# Dir.tmpdir before Active Storage ever sees them.
# /tmp: Rack writes multipart uploads (CSV imports, attachments)
# here before Active Storage ever sees them. The app does not use
# this path directly, though - TMPDIR points at the private
# subdirectory the init container creates inside it, because Ruby
# rejects a world-writable emptyDir as a temporary directory.
- name: web-tmp
mountPath: /tmp
# /rails/tmp: Rails' own scratch directory - the Bootsnap cache,
+12 -4
View File
@@ -36,10 +36,18 @@ sure:
# need a ReadWriteMany volume. Above 1 the extra pods normally stay Pending on
# "Multi-Attach error for volume" - see NOTES.txt.
replicas: 1
# Pod-level hardening, shared by both containers. The upstream image declares
# USER 1000:1000 (group and user "rails", created in the Dockerfile), so the
# chart needs no deviation from the repo hardening standard; fsGroup 1000
# makes the storage PVC writable for that uid.
# Pod-level hardening, shared by both containers and by the init container.
# The upstream image declares USER 1000:1000 (group and user "rails", created
# in the Dockerfile), so the chart needs no deviation from the repo hardening
# standard; fsGroup 1000 makes the storage PVC writable for that uid.
#
# readOnlyRootFilesystem: true holds, and the writable paths are emptyDirs:
# /rails/tmp, /rails/log and /tmp, one set per container. /tmp needs one extra
# step that is easy to mistake for decoration - a tiny init container creates
# a private subdirectory in it and the containers run with TMPDIR pointing
# there, because Ruby refuses a world-writable directory without a sticky bit
# as its temporary directory and an emptyDir is exactly that. Without it the
# app aborts during boot; the StatefulSet carries the measured evidence.
securityContext:
pod:
runAsNonRoot: true