Add install-test stage to the PR pipeline (k3d)

New install-test job in validate-charts.yml: for exactly the charts a PR
changes, install them into an ephemeral k3d cluster (pinned k3d v5.9.0,
k3s v1.35.5-k3s1) and require every Deployment/StatefulSet to become
Ready. Runs non-blocking (continue-on-error) during the stabilization
phase.

Fixtures live at the repo root under ci/ (not inside chart directories,
so they neither trigger publishes on merge nor get packaged): a stub
OnePasswordItem CRD, per-chart test values and fixtures (dummy secrets
with in-cluster DSNs, throwaway postgres/redis/dex, synapse-generate
seed job, static hostPath PVs where charts pin storage classes or need
RWX), plus a shared ci/run-install-test.sh harness that also runs
locally.

Excluded with documented reasons (ci/excluded-charts.txt): satisfactory
(multi-GB image), teamspeak-server (TS6 beta license expired, image
crashes on start), template-chart (placeholder image).

All 14 non-excluded charts verified locally in k3d using only the
committed fixtures.
This commit is contained in:
jklein committed 2026-08-21 23:12:38 +02:00
1 parent 9f258e2440
commit cc6084ca58
35 files changed
+1716 -1

No files matched your search

+3
View File
@@ -0,0 +1,3 @@
# apache-tika needs no extra values for a CI install: no secrets, no ingress,
# no persistence.
{}
+82
View File
@@ -0,0 +1,82 @@
# Fixtures for the calibre-web-automated install-test: the cwa secret the
# 1Password operator would otherwise materialize and static hostPath PVs
# (the consume PVC is ReadWriteMany, which local-path cannot provision).
# This chart names resources <chart>-<release>-..., release name is ci.
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: ci-static
provisioner: kubernetes.io/no-provisioner
---
apiVersion: batch/v1
kind: Job
metadata:
name: node-setup
spec:
backoffLimit: 2
template:
spec:
restartPolicy: Never
containers:
- name: setup
image: public.ecr.aws/docker/library/busybox:1.36
command:
- sh
- -c
- >
mkdir -p /pv/cwa/data /pv/cwa/consume &&
chmod -R 0777 /pv/cwa
volumeMounts:
- name: pv
mountPath: /pv
volumes:
- name: pv
hostPath:
path: /var/lib/ci-pv
type: DirectoryOrCreate
---
apiVersion: v1
kind: PersistentVolume
metadata:
name: ci-cwa-data
spec:
capacity:
storage: 2Gi
accessModes:
- ReadWriteOnce
persistentVolumeReclaimPolicy: Retain
storageClassName: ci-static
claimRef:
namespace: default
name: calibre-web-automated-ci-cwa-pvc
hostPath:
path: /var/lib/ci-pv/cwa/data
type: DirectoryOrCreate
---
apiVersion: v1
kind: PersistentVolume
metadata:
name: ci-cwa-consume
spec:
capacity:
storage: 1Gi
accessModes:
- ReadWriteMany
persistentVolumeReclaimPolicy: Retain
storageClassName: ci-static
claimRef:
namespace: default
name: calibre-web-automated-ci-cwa-consume-pvc
hostPath:
path: /var/lib/ci-pv/cwa/consume
type: DirectoryOrCreate
---
apiVersion: v1
kind: Secret
metadata:
name: calibre-web-automated-ci-cwa-secret
stringData:
hardcover-api-token: ci-dummy-token
# Mounted as /config/.config/calibre/customize.py.json (calibre plugin
# registry); an empty JSON object is a valid file.
calibre-plugins: "{}"
+2
View File
@@ -0,0 +1,2 @@
# First start unpacks the calibre library scaffold onto the volume.
INSTALL_TIMEOUT=600
+12
View File
@@ -0,0 +1,12 @@
secrets:
cwaSecretRef: op://ci/cwa
cwa:
# Both PVCs share this class and the consume PVC is RWX, which k3d's
# local-path provisioner cannot serve - the fixtures pre-bind static
# hostPath PVs of class ci-static instead.
storageClass: ci-static
storage: 2Gi
ingress:
domain: cwa.ci.local
+24
View File
@@ -0,0 +1,24 @@
# Schema-less stub CRD for the 1Password operator's OnePasswordItem CR.
# The charts render OnePasswordItem resources; in CI there is no operator, so
# this stub lets them apply cleanly while the actual Kubernetes Secrets are
# hand-created per chart in ci/<chart>/fixtures.yaml.
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
name: onepassworditems.onepassword.com
spec:
group: onepassword.com
names:
kind: OnePasswordItem
listKind: OnePasswordItemList
plural: onepassworditems
singular: onepassworditem
scope: Namespaced
versions:
- name: v1
served: true
storage: true
schema:
openAPIV3Schema:
type: object
x-kubernetes-preserve-unknown-fields: true
+5
View File
@@ -0,0 +1,5 @@
# cyberchef: stateless, no secrets. Only the ingress host is required
# (clusterIssuer already has a default). The Ingress just has to apply -
# there is no ingress-nginx/cert-manager in the CI cluster.
ingress:
domain: cyberchef.ci.local
+5
View File
@@ -0,0 +1,5 @@
# Charts excluded from the PR install-test, one per line: <chart> <reason>.
# ci/run-install-test.sh skips these with the given reason.
satisfactory the game-server image is multiple GB and Steam downloads 10GB+ game files on first start - impractical on a CI runner
teamspeak-server the TS6 v6.0.0-beta8 image crashes on start ("The default license has expired. Please use the latest server version.", re-verified 2026-08-21); nothing to test until the appVersion is bumped
template-chart scaffold with placeholder image "registry/image:xxx", never published
+3
View File
@@ -0,0 +1,3 @@
# gotenberg needs no extra values for a CI install: no secrets, no ingress,
# no persistence.
{}
+19
View File
@@ -0,0 +1,19 @@
# Fixtures for the homeassistant install-test: the config secret the
# 1Password operator would otherwise materialize (release name: ci). The
# chart mounts the three keys as configuration.yaml, http-config.yaml and
# recorder-config.yaml under /config.
apiVersion: v1
kind: Secret
metadata:
name: ci-homeassistant-config-secret
stringData:
config: |
default_config:
http: !include http-config.yaml
recorder: !include recorder-config.yaml
http-config: |
use_x_forwarded_for: true
trusted_proxies:
- 10.42.0.0/16
recorder-config: |
db_url: sqlite:////config/home-assistant_v2.db
+2
View File
@@ -0,0 +1,2 @@
# Home Assistant's first boot installs/initializes integrations.
INSTALL_TIMEOUT=600
+9
View File
@@ -0,0 +1,9 @@
secrets:
configSecretRef: op://ci/homeassistant-config
core:
storage:
# k3d's default dynamic provisioner (the values default "default" does
# not exist there). Home Assistant starts as root, so writability of the
# dynamically provisioned volume is not an issue.
storageClass: local-path
+95
View File
@@ -0,0 +1,95 @@
# Fixtures for the matrix-synapse install-test. Synapse needs a signing key
# and a log config on its PVC before it starts, so the PVC is backed by a
# pre-bound hostPath PV and a seed Job runs "synapse generate" into that
# directory first (same pattern as the k3d verification for PRs #21/#26).
# The chart mounts homeserver.yaml from the config secret; the static config
# below points at the generated key/log-config paths. Release name: ci.
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: ci-static
provisioner: kubernetes.io/no-provisioner
---
apiVersion: v1
kind: PersistentVolume
metadata:
name: ci-matrix-synapse-data
spec:
capacity:
storage: 2Gi
accessModes:
- ReadWriteOnce
persistentVolumeReclaimPolicy: Retain
storageClassName: ci-static
claimRef:
namespace: default
name: ci-matrix-synapse-pvc
hostPath:
path: /var/lib/ci-pv/matrix-synapse
type: DirectoryOrCreate
---
apiVersion: batch/v1
kind: Job
metadata:
name: synapse-seed
spec:
backoffLimit: 2
template:
spec:
restartPolicy: Never
containers:
- name: generate
# Same image the chart deploys (appVersion of matrix-synapse).
image: ghcr.io/element-hq/synapse:v1.149.1
args: ["generate"]
env:
- name: SYNAPSE_SERVER_NAME
value: synapse.ci.local
- name: SYNAPSE_REPORT_STATS
value: "no"
# Match the UID/GID the chart runs synapse with, so the generated
# signing key (mode 0600) is readable by the server container.
- name: UID
value: "1000"
- name: GID
value: "1000"
volumeMounts:
- name: pv
mountPath: /data
subPath: matrix-synapse-data
volumes:
- name: pv
hostPath:
path: /var/lib/ci-pv/matrix-synapse
type: DirectoryOrCreate
---
apiVersion: v1
kind: Secret
metadata:
name: ci-matrix-synapse-config-secret
stringData:
homeserver.yaml: |
server_name: "synapse.ci.local"
pid_file: /data/homeserver.pid
listeners:
- port: 8008
tls: false
type: http
x_forwarded: true
resources:
- names: [client, federation]
compress: false
database:
name: sqlite3
args:
database: /data/homeserver.db
log_config: "/data/synapse.ci.local.log.config"
media_store_path: /media_store
signing_key_path: "/data/synapse.ci.local.signing.key"
registration_shared_secret: "ci-registration-shared-secret"
macaroon_secret_key: "ci-macaroon-secret-key"
form_secret: "ci-form-secret"
report_stats: false
trusted_key_servers:
- server_name: "matrix.org"
suppress_key_server_warning: true
+2
View File
@@ -0,0 +1,2 @@
# Synapse first start (SQLite schema setup) plus the config seed job.
INSTALL_TIMEOUT=600
+14
View File
@@ -0,0 +1,14 @@
matrix:
serverName: synapse.ci.local
configSecretRef: op://ci/matrix-synapse-config
delegation:
enabled: true
serverUrl: ci.local
storage:
amount: 2Gi
storageClass: ci-static
ingress:
clusterIssuer: ci
domain: matrix.ci.local
+184
View File
@@ -0,0 +1,184 @@
# Fixtures for the outline install-test: throwaway postgres + redis, a dex
# OIDC provider (outline fetches the issuer's discovery document at boot),
# and the secrets the 1Password operator would otherwise materialize
# (release name: ci). Outline's /_health probe checks Postgres AND Redis.
apiVersion: v1
kind: Secret
metadata:
name: ci-outline-secret
stringData:
# Outline requires 64-char hex secrets.
outline-secret-key: 5f4dcc3b5aa765d61d8327deb882cf995f4dcc3b5aa765d61d8327deb882cf99
outline-utils-secret: 9c1185a5c5e9fc54612808977ee8f5489c1185a5c5e9fc54612808977ee8f548
discovery-url: http://dex.default.svc.cluster.local:5556/dex
client-id: ci-client
client-secret: ci-client-secret
---
apiVersion: v1
kind: Secret
metadata:
name: ci-outline-db-secret
stringData:
connection-url: postgres://ci:ci@postgres:5432/outline
---
apiVersion: v1
kind: Secret
metadata:
name: ci-outline-redis-secret
stringData:
connection-url: redis://:cipass@redis:6379
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: postgres
labels:
app: postgres
spec:
replicas: 1
selector:
matchLabels:
app: postgres
template:
metadata:
labels:
app: postgres
spec:
containers:
- name: postgres
image: public.ecr.aws/docker/library/postgres:16-alpine
env:
- name: POSTGRES_USER
value: ci
- name: POSTGRES_PASSWORD
value: ci
- name: POSTGRES_DB
value: outline
ports:
- containerPort: 5432
readinessProbe:
exec:
command: ["pg_isready", "-U", "ci"]
periodSeconds: 5
volumeMounts:
- name: data
mountPath: /var/lib/postgresql/data
volumes:
- name: data
emptyDir: {}
---
apiVersion: v1
kind: Service
metadata:
name: postgres
spec:
selector:
app: postgres
ports:
- port: 5432
targetPort: 5432
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: redis
labels:
app: redis
spec:
replicas: 1
selector:
matchLabels:
app: redis
template:
metadata:
labels:
app: redis
spec:
containers:
- name: redis
image: public.ecr.aws/docker/library/redis:7-alpine
args: ["redis-server", "--requirepass", "cipass"]
ports:
- containerPort: 6379
readinessProbe:
tcpSocket:
port: 6379
periodSeconds: 5
---
apiVersion: v1
kind: Service
metadata:
name: redis
spec:
selector:
app: redis
ports:
- port: 6379
targetPort: 6379
---
apiVersion: v1
kind: ConfigMap
metadata:
name: dex-config
data:
config.yaml: |
issuer: http://dex.default.svc.cluster.local:5556/dex
storage:
type: memory
web:
http: 0.0.0.0:5556
oauth2:
skipApprovalScreen: true
staticClients:
- id: ci-client
secret: ci-client-secret
name: CI Client
redirectURIs:
- https://outline.ci.local/auth/oidc.callback
enablePasswordDB: true
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: dex
labels:
app: dex
spec:
replicas: 1
selector:
matchLabels:
app: dex
template:
metadata:
labels:
app: dex
spec:
containers:
- name: dex
image: ghcr.io/dexidp/dex:v2.41.1
command: ["dex", "serve", "/etc/dex/cfg/config.yaml"]
ports:
- containerPort: 5556
readinessProbe:
httpGet:
path: /dex/healthz
port: 5556
periodSeconds: 5
volumeMounts:
- name: config
mountPath: /etc/dex/cfg
volumes:
- name: config
configMap:
name: dex-config
---
apiVersion: v1
kind: Service
metadata:
name: dex
spec:
selector:
app: dex
ports:
- port: 5556
targetPort: 5556
+7
View File
@@ -0,0 +1,7 @@
secrets:
configSecretRef: op://ci/outline-config
redisSecretRef: op://ci/outline-redis
dbSecretRef: op://ci/outline-db
ingress:
domain: outline.ci.local
+193
View File
@@ -0,0 +1,193 @@
# Fixtures for the paperless-ngx install-test: throwaway postgres + redis,
# the secrets the 1Password operator would otherwise materialize (release
# name: ci), and storage plumbing. The chart hardcodes storageClassName
# "longhorn" (RWX for the consume PVC), which does not exist in k3d: a
# no-provisioner StorageClass named "longhorn" plus pre-bound hostPath PVs
# stand in for it. Paperless runs as uid 1000 with a read-only rootfs and
# hostPath volumes do not support fsGroup, so a node-setup Job pre-creates
# the exact subPath directories world-writable.
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: longhorn
provisioner: kubernetes.io/no-provisioner
---
apiVersion: batch/v1
kind: Job
metadata:
name: node-setup
spec:
backoffLimit: 2
template:
spec:
restartPolicy: Never
containers:
- name: setup
image: public.ecr.aws/docker/library/busybox:1.36
command:
- sh
- -c
- >
mkdir -p /pv/paperless-ngx/data/data /pv/paperless-ngx/data/media
/pv/paperless-ngx/data/export /pv/paperless-ngx/consume &&
chmod -R 0777 /pv/paperless-ngx
volumeMounts:
- name: pv
mountPath: /pv
volumes:
- name: pv
hostPath:
path: /var/lib/ci-pv
type: DirectoryOrCreate
---
apiVersion: v1
kind: PersistentVolume
metadata:
name: ci-paperless-data
spec:
capacity:
storage: 20Gi
accessModes:
- ReadWriteOnce
persistentVolumeReclaimPolicy: Retain
storageClassName: longhorn
claimRef:
namespace: default
name: ci-paperless-ngx-volume-claim
hostPath:
path: /var/lib/ci-pv/paperless-ngx/data
type: DirectoryOrCreate
---
apiVersion: v1
kind: PersistentVolume
metadata:
name: ci-paperless-consume
spec:
capacity:
storage: 1Gi
accessModes:
- ReadWriteMany
persistentVolumeReclaimPolicy: Retain
storageClassName: longhorn
claimRef:
namespace: default
name: ci-paperless-ngx-consume-volume-claim
hostPath:
path: /var/lib/ci-pv/paperless-ngx/consume
type: DirectoryOrCreate
---
apiVersion: v1
kind: Secret
metadata:
name: ci-paperless-ngx-redis-secret
stringData:
redis-connection-url: redis://:cipass@redis:6379
key-prefix: ci
---
apiVersion: v1
kind: Secret
metadata:
name: ci-paperless-ngx-db-secret
stringData:
server: postgres
port: "5432"
database: paperless
username: ci
password: ci
---
apiVersion: v1
kind: Secret
metadata:
name: ci-paperless-ngx-secret
stringData:
username: admin
password: ci-admin-password
paperless-secret-key: paperless-ci-secret-key-0123456789abcdef0123456789abcdef
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: postgres
labels:
app: postgres
spec:
replicas: 1
selector:
matchLabels:
app: postgres
template:
metadata:
labels:
app: postgres
spec:
containers:
- name: postgres
image: public.ecr.aws/docker/library/postgres:16-alpine
env:
- name: POSTGRES_USER
value: ci
- name: POSTGRES_PASSWORD
value: ci
- name: POSTGRES_DB
value: paperless
ports:
- containerPort: 5432
readinessProbe:
exec:
command: ["pg_isready", "-U", "ci"]
periodSeconds: 5
volumeMounts:
- name: data
mountPath: /var/lib/postgresql/data
volumes:
- name: data
emptyDir: {}
---
apiVersion: v1
kind: Service
metadata:
name: postgres
spec:
selector:
app: postgres
ports:
- port: 5432
targetPort: 5432
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: redis
labels:
app: redis
spec:
replicas: 1
selector:
matchLabels:
app: redis
template:
metadata:
labels:
app: redis
spec:
containers:
- name: redis
image: public.ecr.aws/docker/library/redis:7-alpine
args: ["redis-server", "--requirepass", "cipass"]
ports:
- containerPort: 6379
readinessProbe:
tcpSocket:
port: 6379
periodSeconds: 5
---
apiVersion: v1
kind: Service
metadata:
name: redis
spec:
selector:
app: redis
ports:
- port: 6379
targetPort: 6379
+2
View File
@@ -0,0 +1,2 @@
# Paperless runs database migrations and an index check on first start.
INSTALL_TIMEOUT=600
+10
View File
@@ -0,0 +1,10 @@
secrets:
redisRef: op://ci/paperless-redis
databaseRef: op://ci/paperless-db
paperlessRef: op://ci/paperless
ingress:
domain: paperless.ci.local
# smbConnection and oidc stay disabled (defaults): the SMB CSI driver and a
# real OIDC login flow are out of scope for the CI install-test.
+269
View File
@@ -0,0 +1,269 @@
# Fixtures for the patchmon install-test: throwaway postgres + redis, a dex
# OIDC provider, the secrets the 1Password operator would otherwise
# materialize (release name: ci), a pre-bound world-writable hostPath PV for
# the assets volume (the non-root backend writes agent assets at boot;
# hostPath does not support fsGroup), and a Service alias "backend": the
# frontend image's nginx template resolves an upstream host literally named
# "backend" at startup (found during the probes verification of PR #21).
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: ci-static
provisioner: kubernetes.io/no-provisioner
---
apiVersion: batch/v1
kind: Job
metadata:
name: node-setup
spec:
backoffLimit: 2
template:
spec:
restartPolicy: Never
containers:
- name: setup
image: public.ecr.aws/docker/library/busybox:1.36
command:
- sh
- -c
- >
mkdir -p /pv/patchmon/patchmon-assets /pv/patchmon/patchmon-agents &&
chmod -R 0777 /pv/patchmon
volumeMounts:
- name: pv
mountPath: /pv
volumes:
- name: pv
hostPath:
path: /var/lib/ci-pv
type: DirectoryOrCreate
---
apiVersion: v1
kind: PersistentVolume
metadata:
name: ci-patchmon-data
spec:
capacity:
storage: 10Gi
accessModes:
# The assets PVC is shared by backend and frontend as ReadWriteMany.
- ReadWriteMany
persistentVolumeReclaimPolicy: Retain
storageClassName: ci-static
claimRef:
namespace: default
name: ci-patchmon-pvc
hostPath:
path: /var/lib/ci-pv/patchmon
type: DirectoryOrCreate
---
apiVersion: v1
kind: Service
metadata:
name: backend
spec:
selector:
app: ci-patchmon-backend
ports:
- name: http
port: 80
targetPort: 3001
- name: direct
port: 3001
targetPort: 3001
---
apiVersion: v1
kind: Secret
metadata:
name: ci-patchmon-postgres-secret
stringData:
database-url: postgresql://ci:ci@postgres:5432/patchmon
---
apiVersion: v1
kind: Secret
metadata:
name: ci-patchmon-redis-secret
stringData:
host: redis
port: "6379"
username: default
password: cipass
db: "0"
---
apiVersion: v1
kind: Secret
metadata:
name: ci-patchmon-patchmon-secret
stringData:
jwt-secret: patchmon-ci-jwt-secret-0123456789abcdef0123456789abcdef
encryption-key: 0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef
session-secret: patchmon-ci-session-secret-0123456789abcdef0123456789abcdef
---
apiVersion: v1
kind: Secret
metadata:
name: ci-patchmon-oidc-secret
stringData:
issuer-url: http://dex.default.svc.cluster.local:5556/dex
client-id: ci-client
client-secret: ci-client-secret
redirect-uri: https://patchmon.ci.local/auth/callback
requested-scopes: openid profile email
button-text: CI SSO
user-group-name: users
admin-group-name: admins
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: postgres
labels:
app: postgres
spec:
replicas: 1
selector:
matchLabels:
app: postgres
template:
metadata:
labels:
app: postgres
spec:
containers:
- name: postgres
image: public.ecr.aws/docker/library/postgres:16-alpine
env:
- name: POSTGRES_USER
value: ci
- name: POSTGRES_PASSWORD
value: ci
- name: POSTGRES_DB
value: patchmon
ports:
- containerPort: 5432
readinessProbe:
exec:
command: ["pg_isready", "-U", "ci"]
periodSeconds: 5
volumeMounts:
- name: data
mountPath: /var/lib/postgresql/data
volumes:
- name: data
emptyDir: {}
---
apiVersion: v1
kind: Service
metadata:
name: postgres
spec:
selector:
app: postgres
ports:
- port: 5432
targetPort: 5432
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: redis
labels:
app: redis
spec:
replicas: 1
selector:
matchLabels:
app: redis
template:
metadata:
labels:
app: redis
spec:
containers:
- name: redis
image: public.ecr.aws/docker/library/redis:7-alpine
args: ["redis-server", "--requirepass", "cipass"]
ports:
- containerPort: 6379
readinessProbe:
tcpSocket:
port: 6379
periodSeconds: 5
---
apiVersion: v1
kind: Service
metadata:
name: redis
spec:
selector:
app: redis
ports:
- port: 6379
targetPort: 6379
---
apiVersion: v1
kind: ConfigMap
metadata:
name: dex-config
data:
config.yaml: |
issuer: http://dex.default.svc.cluster.local:5556/dex
storage:
type: memory
web:
http: 0.0.0.0:5556
oauth2:
skipApprovalScreen: true
staticClients:
- id: ci-client
secret: ci-client-secret
name: CI Client
redirectURIs:
- https://patchmon.ci.local/auth/callback
enablePasswordDB: true
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: dex
labels:
app: dex
spec:
replicas: 1
selector:
matchLabels:
app: dex
template:
metadata:
labels:
app: dex
spec:
containers:
- name: dex
image: ghcr.io/dexidp/dex:v2.41.1
command: ["dex", "serve", "/etc/dex/cfg/config.yaml"]
ports:
- containerPort: 5556
readinessProbe:
httpGet:
path: /dex/healthz
port: 5556
periodSeconds: 5
volumeMounts:
- name: config
mountPath: /etc/dex/cfg
volumes:
- name: config
configMap:
name: dex-config
---
apiVersion: v1
kind: Service
metadata:
name: dex
spec:
selector:
app: dex
ports:
- port: 5556
targetPort: 5556
+19
View File
@@ -0,0 +1,19 @@
secrets:
redisSecretRef: op://ci/patchmon-redis
postgresSecretRef: op://ci/patchmon-postgres
oidcSecretRef: op://ci/patchmon-oidc
patchmonSecretRef: op://ci/patchmon
ingress:
clusterIssuer: ci
domain: patchmon.ci.local
patchmon:
storage:
# Point the RWX assets PVC at the fixture StorageClass so the pre-created
# world-writable hostPath PV is used (local-path cannot provision RWX, and
# the non-root backend must write its assets dir at boot). Note: the PVC
# template guards on .storage.storageClass but renders .storage.class,
# so both keys must be set.
storageClass: ci-static
class: ci-static
+143
View File
@@ -0,0 +1,143 @@
# Fixtures for the rallly install-test: throwaway postgres, a dex OIDC
# provider (the OIDC discovery URL must answer) and the secrets the
# 1Password operator would otherwise materialize (release name: ci).
apiVersion: v1
kind: Secret
metadata:
name: ci-rallly-secret
stringData:
secret-key: rallly-ci-secret-key-0123456789abcdef0123456789abcdef
initial-admin-email: ci@example.org
---
apiVersion: v1
kind: Secret
metadata:
name: ci-rallly-database-secret
stringData:
database-url: postgres://ci:ci@postgres:5432/rallly
---
apiVersion: v1
kind: Secret
metadata:
name: ci-rallly-oidc-secret
stringData:
discovery-url: http://dex.default.svc.cluster.local:5556/dex/.well-known/openid-configuration
client-id: ci-client
client-secret: ci-client-secret
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: postgres
labels:
app: postgres
spec:
replicas: 1
selector:
matchLabels:
app: postgres
template:
metadata:
labels:
app: postgres
spec:
containers:
- name: postgres
image: public.ecr.aws/docker/library/postgres:16-alpine
env:
- name: POSTGRES_USER
value: ci
- name: POSTGRES_PASSWORD
value: ci
- name: POSTGRES_DB
value: rallly
ports:
- containerPort: 5432
readinessProbe:
exec:
command: ["pg_isready", "-U", "ci"]
periodSeconds: 5
volumeMounts:
- name: data
mountPath: /var/lib/postgresql/data
volumes:
- name: data
emptyDir: {}
---
apiVersion: v1
kind: Service
metadata:
name: postgres
spec:
selector:
app: postgres
ports:
- port: 5432
targetPort: 5432
---
apiVersion: v1
kind: ConfigMap
metadata:
name: dex-config
data:
config.yaml: |
issuer: http://dex.default.svc.cluster.local:5556/dex
storage:
type: memory
web:
http: 0.0.0.0:5556
oauth2:
skipApprovalScreen: true
staticClients:
- id: ci-client
secret: ci-client-secret
name: CI Client
redirectURIs:
- https://rallly.ci.local/api/auth/callback/oidc
enablePasswordDB: true
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: dex
labels:
app: dex
spec:
replicas: 1
selector:
matchLabels:
app: dex
template:
metadata:
labels:
app: dex
spec:
containers:
- name: dex
image: ghcr.io/dexidp/dex:v2.41.1
command: ["dex", "serve", "/etc/dex/cfg/config.yaml"]
ports:
- containerPort: 5556
readinessProbe:
httpGet:
path: /dex/healthz
port: 5556
periodSeconds: 5
volumeMounts:
- name: config
mountPath: /etc/dex/cfg
volumes:
- name: config
configMap:
name: dex-config
---
apiVersion: v1
kind: Service
metadata:
name: dex
spec:
selector:
app: dex
ports:
- port: 5556
targetPort: 5556
+12
View File
@@ -0,0 +1,12 @@
secrets:
ralllySecretRef: op://ci/rallly
oidcSecretRef: op://ci/rallly-oidc
databaseSecretRef: op://ci/rallly-db
# smtp stays disabled (default), so no smtpSecretRef is needed.
rallly:
supportEmail: ci@example.org
ingress:
clusterIssuer: ci
domain: rallly.ci.local
+112
View File
@@ -0,0 +1,112 @@
#!/usr/bin/env bash
# Install-test harness: installs one chart into the cluster of the current
# kubectl context (an ephemeral k3d cluster in CI) using only the committed
# fixtures under ci/<chart>/ and requires every workload to become Ready.
#
# Usage: ci/run-install-test.sh <chart-name>
#
# Layout (repo root):
# ci/common/ fixtures applied for every chart (OnePasswordItem stub CRD)
# ci/<chart>/test-values.yaml values for the CI install (required)
# ci/<chart>/fixtures.yaml dummy secrets / throwaway infra / static PVs (optional)
# ci/<chart>/settings.env optional overrides, e.g. INSTALL_TIMEOUT=600
# ci/excluded-charts.txt charts that cannot run in CI, with reasons
set -euo pipefail
CHART="${1:?usage: $0 <chart-name>}"
NAMESPACE="default"
RELEASE="ci"
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
CI_DIR="$ROOT/ci"
# Per-chart override via ci/<chart>/settings.env (INSTALL_TIMEOUT in seconds).
INSTALL_TIMEOUT=300
FIXTURE_JOB_TIMEOUT=600
if [ -f "$CI_DIR/$CHART/settings.env" ]; then
# shellcheck disable=SC1090
source "$CI_DIR/$CHART/settings.env"
fi
# --- Exclusion list ---------------------------------------------------------
if [ -f "$CI_DIR/excluded-charts.txt" ]; then
REASON="$(grep -E "^$CHART([[:space:]]|$)" "$CI_DIR/excluded-charts.txt" | sed -E "s/^$CHART[[:space:]]*//" || true)"
if [ -n "$REASON" ]; then
echo "SKIP: chart '$CHART' is excluded from the install-test: $REASON"
exit 0
fi
fi
if [ ! -d "$ROOT/$CHART" ] || [ ! -f "$ROOT/$CHART/Chart.yaml" ]; then
echo "ERROR: '$CHART' is not a chart directory" >&2
exit 1
fi
if [ ! -f "$CI_DIR/$CHART/test-values.yaml" ]; then
echo "ERROR: missing ci/$CHART/test-values.yaml — every installable chart needs CI fixtures." >&2
echo "Add test values (and fixtures.yaml if the app needs secrets/infra), or add the chart" >&2
echo "to ci/excluded-charts.txt with a justification." >&2
exit 1
fi
# --- Failure diagnostics ----------------------------------------------------
diagnose() {
echo "=================== INSTALL-TEST FAILED: DIAGNOSTICS ==================="
echo "--- pods ---"
kubectl -n "$NAMESPACE" get pods -o wide || true
echo "--- events (most recent last) ---"
kubectl -n "$NAMESPACE" get events --sort-by=.metadata.creationTimestamp | tail -60 || true
echo "--- describe pods ---"
kubectl -n "$NAMESPACE" describe pods || true
echo "--- container logs ---"
for POD in $(kubectl -n "$NAMESPACE" get pods -o name); do
echo "--- logs $POD ---"
kubectl -n "$NAMESPACE" logs "$POD" --all-containers --tail=150 --prefix || true
kubectl -n "$NAMESPACE" logs "$POD" --all-containers --tail=50 --previous --prefix 2>/dev/null || true
done
echo "========================================================================"
}
trap 'RC=$?; if [ $RC -ne 0 ]; then diagnose; fi; exit $RC' EXIT
# --- 1. Apply fixtures ------------------------------------------------------
echo "::group::Apply fixtures"
kubectl apply -f "$CI_DIR/common/"
if [ -f "$CI_DIR/$CHART/fixtures.yaml" ]; then
kubectl apply -f "$CI_DIR/$CHART/fixtures.yaml"
fi
echo "::endgroup::"
# --- 2. Wait for fixture jobs (node setup, data seeding) and infra ----------
echo "::group::Wait for fixture jobs and infrastructure"
JOBS="$(kubectl -n "$NAMESPACE" get jobs -o name 2>/dev/null || true)"
for JOB in $JOBS; do
echo "Waiting for $JOB to complete..."
kubectl -n "$NAMESPACE" wait --for=condition=complete "$JOB" --timeout="${FIXTURE_JOB_TIMEOUT}s"
done
for RES in $(kubectl -n "$NAMESPACE" get deploy,sts -o name 2>/dev/null || true); do
echo "Waiting for fixture $RES..."
kubectl -n "$NAMESPACE" rollout status "$RES" --timeout="${FIXTURE_JOB_TIMEOUT}s"
done
echo "::endgroup::"
# --- 3. Install the chart ---------------------------------------------------
echo "::group::helm install"
helm install "$RELEASE" "$ROOT/$CHART" \
--namespace "$NAMESPACE" \
-f "$CI_DIR/$CHART/test-values.yaml"
echo "::endgroup::"
# --- 4. Require every workload to become Ready ------------------------------
echo "::group::Wait for workloads (timeout ${INSTALL_TIMEOUT}s each)"
WORKLOADS="$(kubectl -n "$NAMESPACE" get deploy,sts -o name)"
if [ -z "$WORKLOADS" ]; then
echo "ERROR: the release created no Deployments/StatefulSets" >&2
exit 1
fi
for RES in $WORKLOADS; do
echo "Waiting for $RES..."
kubectl -n "$NAMESPACE" rollout status "$RES" --timeout="${INSTALL_TIMEOUT}s"
done
echo "::endgroup::"
echo "--- final state ---"
kubectl -n "$NAMESPACE" get pods -o wide
echo "OK: chart '$CHART' installed and all workloads are Ready."
+10
View File
@@ -0,0 +1,10 @@
# Fixtures for the samba install-test: the users secret the 1Password
# operator would otherwise materialize (release name: ci). users.conf uses
# the dockur/samba "user:password" format.
apiVersion: v1
kind: Secret
metadata:
name: ci-samba-users-secret
stringData:
users.conf: |
ciuser:ci-password
+10
View File
@@ -0,0 +1,10 @@
secrets:
sambaUsersRef: op://ci/samba-users
samba:
storageClass: local-path
shares:
- name: ci-share
validUsers: ciuser
description: CI test share
size: 1Gi
+141
View File
@@ -0,0 +1,141 @@
# Fixtures for the urlaubsverwaltung install-test: throwaway postgres, a dex
# OIDC provider (Spring performs OIDC discovery against the issuer at boot)
# and the secrets the 1Password operator would otherwise materialize.
# Note: this chart names secrets <chart>-<release>-..., release name is ci.
apiVersion: v1
kind: Secret
metadata:
name: urlaubsverwaltung-ci-uv-db-secret
stringData:
jdbc-url: jdbc:postgresql://postgres:5432/urlaubsverwaltung
username: ci
password: ci
---
apiVersion: v1
kind: Secret
metadata:
name: urlaubsverwaltung-ci-uv-oidc-secret
stringData:
client-id: ci-client
client-secret: ci-client-secret
issuer-name: CI
redirect-uri: https://uv.ci.local/login/oauth2/code/default
issuer-uri: http://dex.default.svc.cluster.local:5556/dex
jwt-issuer-uri: http://dex.default.svc.cluster.local:5556/dex
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: postgres
labels:
app: postgres
spec:
replicas: 1
selector:
matchLabels:
app: postgres
template:
metadata:
labels:
app: postgres
spec:
containers:
- name: postgres
image: public.ecr.aws/docker/library/postgres:16-alpine
env:
- name: POSTGRES_USER
value: ci
- name: POSTGRES_PASSWORD
value: ci
- name: POSTGRES_DB
value: urlaubsverwaltung
ports:
- containerPort: 5432
readinessProbe:
exec:
command: ["pg_isready", "-U", "ci"]
periodSeconds: 5
volumeMounts:
- name: data
mountPath: /var/lib/postgresql/data
volumes:
- name: data
emptyDir: {}
---
apiVersion: v1
kind: Service
metadata:
name: postgres
spec:
selector:
app: postgres
ports:
- port: 5432
targetPort: 5432
---
apiVersion: v1
kind: ConfigMap
metadata:
name: dex-config
data:
config.yaml: |
issuer: http://dex.default.svc.cluster.local:5556/dex
storage:
type: memory
web:
http: 0.0.0.0:5556
oauth2:
skipApprovalScreen: true
staticClients:
- id: ci-client
secret: ci-client-secret
name: CI Client
redirectURIs:
- https://uv.ci.local/login/oauth2/code/default
enablePasswordDB: true
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: dex
labels:
app: dex
spec:
replicas: 1
selector:
matchLabels:
app: dex
template:
metadata:
labels:
app: dex
spec:
containers:
- name: dex
image: ghcr.io/dexidp/dex:v2.41.1
command: ["dex", "serve", "/etc/dex/cfg/config.yaml"]
ports:
- containerPort: 5556
readinessProbe:
httpGet:
path: /dex/healthz
port: 5556
periodSeconds: 5
volumeMounts:
- name: config
mountPath: /etc/dex/cfg
volumes:
- name: config
configMap:
name: dex-config
---
apiVersion: v1
kind: Service
metadata:
name: dex
spec:
selector:
app: dex
ports:
- port: 5556
targetPort: 5556
+2
View File
@@ -0,0 +1,2 @@
# Spring Boot startup (JVM + Liquibase migrations) can take a while.
INSTALL_TIMEOUT=600
+6
View File
@@ -0,0 +1,6 @@
secrets:
databaseSecretRef: op://ci/uv-db
oidcSecretRef: op://ci/uv-oidc
ingress:
domain: uv.ci.local
+188
View File
@@ -0,0 +1,188 @@
# Fixtures for the voucher-vault install-test: throwaway postgres + redis
# (Celery broker), a dex OIDC provider and the secrets the 1Password
# operator would otherwise materialize (release name: ci).
apiVersion: v1
kind: Secret
metadata:
name: ci-voucher-vault-secret
stringData:
secretKey: voucher-vault-ci-django-secret-0123456789abcdef0123456789abcdef
redis-url: redis://:cipass@redis:6379/0
---
apiVersion: v1
kind: Secret
metadata:
name: ci-voucher-vault-db-secret
stringData:
host: postgres
port: "5432"
user: ci
password: ci
database: vouchervault
---
apiVersion: v1
kind: Secret
metadata:
name: ci-voucher-vault-oidc-secret
stringData:
oidc-jwks-endpoint: http://dex.default.svc.cluster.local:5556/dex/keys
client-id: ci-client
client-secret: ci-client-secret
authorization-endpoint: http://dex.default.svc.cluster.local:5556/dex/auth
token-endpoint: http://dex.default.svc.cluster.local:5556/dex/token
user-endpoint: http://dex.default.svc.cluster.local:5556/dex/userinfo
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: postgres
labels:
app: postgres
spec:
replicas: 1
selector:
matchLabels:
app: postgres
template:
metadata:
labels:
app: postgres
spec:
containers:
- name: postgres
image: public.ecr.aws/docker/library/postgres:16-alpine
env:
- name: POSTGRES_USER
value: ci
- name: POSTGRES_PASSWORD
value: ci
- name: POSTGRES_DB
value: vouchervault
ports:
- containerPort: 5432
readinessProbe:
exec:
command: ["pg_isready", "-U", "ci"]
periodSeconds: 5
volumeMounts:
- name: data
mountPath: /var/lib/postgresql/data
volumes:
- name: data
emptyDir: {}
---
apiVersion: v1
kind: Service
metadata:
name: postgres
spec:
selector:
app: postgres
ports:
- port: 5432
targetPort: 5432
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: redis
labels:
app: redis
spec:
replicas: 1
selector:
matchLabels:
app: redis
template:
metadata:
labels:
app: redis
spec:
containers:
- name: redis
image: public.ecr.aws/docker/library/redis:7-alpine
args: ["redis-server", "--requirepass", "cipass"]
ports:
- containerPort: 6379
readinessProbe:
tcpSocket:
port: 6379
periodSeconds: 5
---
apiVersion: v1
kind: Service
metadata:
name: redis
spec:
selector:
app: redis
ports:
- port: 6379
targetPort: 6379
---
apiVersion: v1
kind: ConfigMap
metadata:
name: dex-config
data:
config.yaml: |
issuer: http://dex.default.svc.cluster.local:5556/dex
storage:
type: memory
web:
http: 0.0.0.0:5556
oauth2:
skipApprovalScreen: true
staticClients:
- id: ci-client
secret: ci-client-secret
name: CI Client
redirectURIs:
- https://vv.ci.local/oidc/callback
enablePasswordDB: true
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: dex
labels:
app: dex
spec:
replicas: 1
selector:
matchLabels:
app: dex
template:
metadata:
labels:
app: dex
spec:
containers:
- name: dex
image: ghcr.io/dexidp/dex:v2.41.1
command: ["dex", "serve", "/etc/dex/cfg/config.yaml"]
ports:
- containerPort: 5556
readinessProbe:
httpGet:
path: /dex/healthz
port: 5556
periodSeconds: 5
volumeMounts:
- name: config
mountPath: /etc/dex/cfg
volumes:
- name: config
configMap:
name: dex-config
---
apiVersion: v1
kind: Service
metadata:
name: dex
spec:
selector:
app: dex
ports:
- port: 5556
targetPort: 5556
+8
View File
@@ -0,0 +1,8 @@
secrets:
voucherVaultSecretRef: op://ci/voucher-vault
oidcSecretRef: op://ci/voucher-vault-oidc
databaseSecretRef: op://ci/voucher-vault-db
ingress:
clusterIssuer: ci
domain: vv.ci.local
+65
View File
@@ -0,0 +1,65 @@
# Fixtures for the zipline install-test: throwaway postgres and the secrets
# the 1Password operator would otherwise materialize (release name: ci).
apiVersion: v1
kind: Secret
metadata:
name: ci-zipline-db-secret
stringData:
connection-url: postgresql://ci:ci@postgres:5432/zipline
---
apiVersion: v1
kind: Secret
metadata:
name: ci-zipline-core-secret
stringData:
server-secret: zipline-ci-core-secret-0123456789abcdef0123456789abcdef0123456789
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: postgres
labels:
app: postgres
spec:
replicas: 1
selector:
matchLabels:
app: postgres
template:
metadata:
labels:
app: postgres
spec:
containers:
- name: postgres
image: public.ecr.aws/docker/library/postgres:16-alpine
env:
- name: POSTGRES_USER
value: ci
- name: POSTGRES_PASSWORD
value: ci
- name: POSTGRES_DB
value: zipline
ports:
- containerPort: 5432
readinessProbe:
exec:
command: ["pg_isready", "-U", "ci"]
periodSeconds: 5
volumeMounts:
- name: data
mountPath: /var/lib/postgresql/data
volumes:
- name: data
emptyDir: {}
---
apiVersion: v1
kind: Service
metadata:
name: postgres
spec:
selector:
app: postgres
ports:
- port: 5432
targetPort: 5432
+9
View File
@@ -0,0 +1,9 @@
config:
storage: 1Gi
storageClass: local-path
domain: zipline.ci.local
# The OnePasswordItem CRs only need non-null item paths; the actual
# Kubernetes Secrets come from ci/zipline/fixtures.yaml.
secretRef:
db: op://ci/zipline-db
zipline: op://ci/zipline-core