Add reusable ImmichFrame Helm chart
Validate, publish and promote Helm chart / Validate chart (push) Successful in 23s
Validate, publish and promote Helm chart / Publish and promote chart (push) Successful in 29s

This commit is contained in:
2026-10-18 13:41:43 +02:00
commit e9af5ed974
15 changed files with 819 additions and 0 deletions
+135
View File
@@ -0,0 +1,135 @@
name: Validate, publish and promote Helm chart
on:
push:
pull_request:
env:
HELM_VERSION: v4.2.2
KUBECONFORM_VERSION: v0.7.0
GITOPS_REPOSITORY: feedc0de/homelab-gitops
jobs:
validate:
name: Validate chart
runs-on: ubuntu-latest
steps:
- name: Check out source
uses: actions/checkout@v4
- name: Install Helm
uses: azure/setup-helm@v4
with:
version: ${{ env.HELM_VERSION }}
- name: Install Ruby
uses: ruby/setup-ruby@v1
with:
ruby-version: "3.3"
- name: Lint, render and test relationships
run: ./test.sh
- name: Validate rendered Kubernetes resources
run: |
helm template immichframe chart/immichframe \
--namespace default \
--set ingress.enabled=true \
--set ingress.hosts[0].host=frame.example.com \
--set ingress.hosts[0].paths[0].path=/ \
--set ingress.hosts[0].paths[0].pathType=Prefix \
--set podDisruptionBudget.enabled=true \
--set prometheusRule.enabled=true \
| docker run --rm --interactive \
"ghcr.io/yannh/kubeconform:${KUBECONFORM_VERSION}" \
-strict -ignore-missing-schemas -summary
publish:
name: Publish and promote chart
if: gitea.ref == 'refs/heads/main'
needs: validate
runs-on: ubuntu-latest
steps:
- name: Check out source
uses: actions/checkout@v4
- name: Install Helm
uses: azure/setup-helm@v4
with:
version: ${{ env.HELM_VERSION }}
- name: Select immutable chart version
run: |
set -euo pipefail
run_attempt="${GITHUB_RUN_ATTEMPT:-1}"
test -n "${GITHUB_RUN_NUMBER}"
printf 'CHART_VERSION=0.%s.%s\n' \
"${GITHUB_RUN_NUMBER}" "${run_attempt}" >> "${GITHUB_ENV}"
- name: Package and publish chart
env:
PACKAGE_USERNAME: ${{ secrets.PACKAGE_USERNAME }}
PACKAGE_TOKEN: ${{ secrets.PACKAGE_TOKEN }}
run: |
set -euo pipefail
test -n "${PACKAGE_USERNAME}"
test -n "${PACKAGE_TOKEN}"
install -d .chart-packages
helm package chart/immichframe \
--destination .chart-packages \
--version "${CHART_VERSION}"
chart_file=".chart-packages/immichframe-${CHART_VERSION}.tgz"
test -f "${chart_file}"
curl --fail --silent --show-error \
--user "${PACKAGE_USERNAME}:${PACKAGE_TOKEN}" \
--request POST \
--upload-file "${chart_file}" \
https://code.brunner.ninja/api/packages/feedc0de/helm/api/charts
- name: Check that this is still current main
id: current-main
env:
COMMIT_SHA: ${{ gitea.sha }}
run: |
git fetch --no-tags origin main
if [ "$(git rev-parse FETCH_HEAD)" = "${COMMIT_SHA}" ]; then
echo "promote=true" >> "${GITEA_OUTPUT}"
else
echo "This build is no longer current main; skipping promotion"
echo "promote=false" >> "${GITEA_OUTPUT}"
fi
- name: Check out GitOps repository
if: steps.current-main.outputs.promote == 'true'
uses: actions/checkout@v4
with:
repository: ${{ env.GITOPS_REPOSITORY }}
ref: main
token: ${{ secrets.GITOPS_TOKEN }}
path: homelab-gitops
- name: Commit chart promotion
if: steps.current-main.outputs.promote == 'true'
working-directory: homelab-gitops
run: |
set -euo pipefail
./scripts/update-immichframe-chart.sh "${CHART_VERSION}"
git config user.name "immichframe chart CI"
git config user.email "immichframe-chart-ci@brunner.ninja"
git add apps/immichframe/Chart.yaml
if git diff --cached --quiet; then
echo "GitOps already selects ${CHART_VERSION}"
exit 0
fi
git commit -m "Deploy immichframe chart ${CHART_VERSION}"
for attempt in 1 2 3; do
if git push origin HEAD:main; then
exit 0
fi
if [ "${attempt}" = 3 ]; then
echo "GitOps promotion failed after three attempts" >&2
exit 1
fi
git fetch origin main
git rebase origin/main
done
+2
View File
@@ -0,0 +1,2 @@
.chart-packages/
rendered.yaml
+75
View File
@@ -0,0 +1,75 @@
# ImmichFrame Helm chart
This repository packages the upstream
[ImmichFrame](https://github.com/immichFrame/ImmichFrame) container for
Kubernetes. It is an independent community chart and is not maintained by the
ImmichFrame project.
The chart creates a Deployment, ConfigMap, Service, optional Ingress, optional
PVC, optional PodDisruptionBudget, and optional PrometheusRule. ImmichFrame does
not currently expose a Prometheus metrics endpoint, so the chart deliberately
does not create a ServiceMonitor. Its alerts use metrics supplied by
kube-state-metrics.
## Install
Add Daniel's public chart repository and choose an immutable chart version:
```sh
helm repo add brunner https://brunner.ninja/charts
helm repo update brunner
helm upgrade --install immichframe brunner/immichframe \
--version CHART_VERSION \
--namespace immichframe --create-namespace \
--values values.yaml
```
The chart version has no `latest` alias. Every successful main-branch workflow
publishes a unique SemVer version, `0.<run-number>.<attempt>`, so GitOps can pin
and roll back an exact package. The default upstream image is the chart's
`appVersion`; `image.registry`, `image.repository`, and `image.tag` can all be
overridden independently.
Each configured Immich account references an existing Kubernetes Secret. The
chart mounts its key as a file and writes only `ApiKeyFile` into
`Settings.yml`; API keys never enter Helm values or rendered ConfigMaps.
```yaml
config:
accounts:
- immichServerUrl: https://immich.example.com
apiKeySecret:
name: immichframe-api
key: api-key
options:
Albums:
- 00000000-0000-0000-0000-000000000000
```
## Deployment and persistence model
ImmichFrame `v1.0.38.0` and newer imports `Settings.yml` once into a SQLite
database under `/app/Config`; afterward the database is its source of truth.
With `persistence.enabled: false` (the default), each Deployment replica has an
ephemeral database and reimports the declarative settings whenever its Pod is
recreated. This is suitable when Git remains the configuration owner and admin
UI edits are intentionally disposable.
Enable persistence and normally use one replica if admin UI changes must
survive. With a persistent database, later `Settings.yml` changes are ignored
by ImmichFrame and must instead be applied through its admin UI. Multiple
replicas require storage that supports the chosen access mode and careful
application-level consideration; they must not share one SQLite database.
## CI publication
Gitea Actions lints, renders, schema-validates, and publishes the chart to
`https://brunner.ninja/charts`. The repository needs these Actions secrets:
- `PACKAGE_USERNAME` and `PACKAGE_TOKEN` for the Gitea Helm package registry.
- `GITOPS_TOKEN` with write access to `feedc0de/homelab-gitops` so CI can
promote the exact published chart version.
The live homelab values are intentionally kept in the public
[homelab-gitops application](https://code.brunner.ninja/feedc0de/homelab-gitops/src/branch/main/apps/immichframe),
where the API key is stored only as Sealed Secrets ciphertext.
+16
View File
@@ -0,0 +1,16 @@
apiVersion: v2
name: immichframe
description: Community Kubernetes deployment for the upstream ImmichFrame container
type: application
version: 0.1.0
appVersion: v1.0.39.0
home: https://github.com/immichFrame/ImmichFrame
sources:
- https://github.com/immichFrame/ImmichFrame
- https://code.brunner.ninja/feedc0de/immichframe-helm
keywords:
- immich
- digital-photo-frame
- photos
maintainers:
- name: feedc0de
+34
View File
@@ -0,0 +1,34 @@
{{- define "immichframe.name" -}}
{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }}
{{- end }}
{{- define "immichframe.fullname" -}}
{{- if .Values.fullnameOverride }}
{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }}
{{- else }}
{{- $name := default .Chart.Name .Values.nameOverride }}
{{- if contains $name .Release.Name }}
{{- .Release.Name | trunc 63 | trimSuffix "-" }}
{{- else }}
{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }}
{{- end }}
{{- end }}
{{- end }}
{{- define "immichframe.labels" -}}
helm.sh/chart: {{ printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" }}
app.kubernetes.io/name: {{ include "immichframe.name" . }}
app.kubernetes.io/instance: {{ .Release.Name }}
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
app.kubernetes.io/managed-by: {{ .Release.Service }}
app: {{ include "immichframe.fullname" . }}
{{- end }}
{{- define "immichframe.selectorLabels" -}}
app: {{ include "immichframe.fullname" . }}
{{- end }}
{{- define "immichframe.image" -}}
{{- $tag := default .Chart.AppVersion .Values.image.tag -}}
{{- printf "%s/%s:%s" .Values.image.registry .Values.image.repository $tag -}}
{{- end }}
@@ -0,0 +1,18 @@
apiVersion: v1
kind: ConfigMap
metadata:
name: {{ include "immichframe.fullname" . }}-config
labels:
{{- include "immichframe.labels" . | nindent 4 }}
data:
Settings.yml: |
General:
{{- toYaml .Values.config.general | nindent 6 }}
Accounts:
{{- range $index, $account := .Values.config.accounts }}
- ImmichServerUrl: {{ $account.immichServerUrl | quote }}
ApiKeyFile: {{ printf "/app/Secrets/account-%d-api-key" $index | quote }}
{{- with $account.options }}
{{- toYaml . | nindent 8 }}
{{- end }}
{{- end }}
+120
View File
@@ -0,0 +1,120 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ include "immichframe.fullname" . }}
labels:
{{- include "immichframe.labels" . | nindent 4 }}
spec:
replicas: {{ .Values.replicaCount }}
strategy:
type: RollingUpdate
rollingUpdate:
maxUnavailable: 0
maxSurge: 1
selector:
matchLabels:
{{- include "immichframe.selectorLabels" . | nindent 6 }}
template:
metadata:
annotations:
checksum/config: {{ include (print $.Template.BasePath "/configmap.yaml") . | sha256sum }}
checksum/external-secret-revision: {{ .Values.config.secretRevision | quote }}
{{- with .Values.podAnnotations }}
{{- toYaml . | nindent 8 }}
{{- end }}
labels:
{{- include "immichframe.labels" . | nindent 8 }}
{{- with .Values.podLabels }}
{{- toYaml . | nindent 8 }}
{{- end }}
spec:
automountServiceAccountToken: false
{{- with .Values.imagePullSecrets }}
imagePullSecrets:
{{- range . }}
- name: {{ if kindIs "string" . }}{{ . }}{{ else }}{{ .name }}{{ end }}
{{- end }}
{{- end }}
securityContext:
{{- toYaml .Values.podSecurityContext | nindent 8 }}
{{- if .Values.affinity }}
affinity:
{{- toYaml .Values.affinity | nindent 8 }}
{{- else if .Values.podAntiAffinity.enabled }}
affinity:
podAntiAffinity:
preferredDuringSchedulingIgnoredDuringExecution:
- weight: 100
podAffinityTerm:
labelSelector:
matchLabels:
{{- include "immichframe.selectorLabels" . | nindent 18 }}
topologyKey: kubernetes.io/hostname
{{- end }}
containers:
- name: immichframe
image: {{ include "immichframe.image" . | quote }}
imagePullPolicy: {{ .Values.image.pullPolicy }}
ports:
- name: http
containerPort: 8080
protocol: TCP
env:
- name: TZ
value: {{ .Values.timezone | quote }}
startupProbe:
tcpSocket:
port: http
{{- toYaml .Values.startupProbe | nindent 10 }}
readinessProbe:
tcpSocket:
port: http
{{- toYaml .Values.readinessProbe | nindent 10 }}
livenessProbe:
tcpSocket:
port: http
{{- toYaml .Values.livenessProbe | nindent 10 }}
resources:
{{- toYaml .Values.resources | nindent 10 }}
securityContext:
{{- toYaml .Values.securityContext | nindent 10 }}
volumeMounts:
{{- if .Values.persistence.enabled }}
- name: config-data
mountPath: /app/Config
{{- end }}
- name: settings
mountPath: /app/Config/Settings.yml
subPath: Settings.yml
readOnly: true
- name: api-keys
mountPath: /app/Secrets
readOnly: true
volumes:
{{- if .Values.persistence.enabled }}
- name: config-data
persistentVolumeClaim:
claimName: {{ default (printf "%s-config" (include "immichframe.fullname" .)) .Values.persistence.existingClaim }}
{{- end }}
- name: settings
configMap:
name: {{ include "immichframe.fullname" . }}-config
- name: api-keys
projected:
defaultMode: 0440
sources:
{{- range $index, $account := .Values.config.accounts }}
- secret:
name: {{ $account.apiKeySecret.name }}
items:
- key: {{ $account.apiKeySecret.key }}
path: {{ printf "account-%d-api-key" $index }}
{{- end }}
{{- with .Values.nodeSelector }}
nodeSelector:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.tolerations }}
tolerations:
{{- toYaml . | nindent 8 }}
{{- end }}
+35
View File
@@ -0,0 +1,35 @@
{{- if .Values.ingress.enabled }}
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: {{ include "immichframe.fullname" . }}
labels:
{{- include "immichframe.labels" . | nindent 4 }}
{{- with .Values.ingress.annotations }}
annotations:
{{- toYaml . | nindent 4 }}
{{- end }}
spec:
{{- with .Values.ingress.className }}
ingressClassName: {{ . }}
{{- end }}
{{- with .Values.ingress.tls }}
tls:
{{- toYaml . | nindent 4 }}
{{- end }}
rules:
{{- range .Values.ingress.hosts }}
- host: {{ .host | quote }}
http:
paths:
{{- range .paths }}
- path: {{ .path }}
pathType: {{ .pathType }}
backend:
service:
name: {{ include "immichframe.fullname" $ }}
port:
name: http
{{- end }}
{{- end }}
{{- end }}
+13
View File
@@ -0,0 +1,13 @@
{{- if .Values.podDisruptionBudget.enabled }}
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
name: {{ include "immichframe.fullname" . }}
labels:
{{- include "immichframe.labels" . | nindent 4 }}
spec:
minAvailable: {{ .Values.podDisruptionBudget.minAvailable }}
selector:
matchLabels:
{{- include "immichframe.selectorLabels" . | nindent 6 }}
{{- end }}
@@ -0,0 +1,28 @@
{{- if .Values.prometheusRule.enabled }}
apiVersion: monitoring.coreos.com/v1
kind: PrometheusRule
metadata:
name: {{ include "immichframe.fullname" . }}
labels:
{{- include "immichframe.labels" . | nindent 4 }}
{{- with .Values.prometheusRule.labels }}
{{- toYaml . | nindent 4 }}
{{- end }}
spec:
groups:
- name: {{ include "immichframe.fullname" . }}
rules:
- alert: ImmichFrameUnavailable
expr: kube_deployment_status_replicas_available{namespace={{ .Release.Namespace | quote }}, deployment={{ include "immichframe.fullname" . | quote }}} < 1
for: 5m
labels:
severity: critical
annotations:
summary: ImmichFrame has no available replicas
- alert: ImmichFrameContainerRestarting
expr: increase(kube_pod_container_status_restarts_total{namespace={{ .Release.Namespace | quote }}, container="immichframe"}[15m]) > 2
labels:
severity: warning
annotations:
summary: ImmichFrame is repeatedly restarting
{{- end }}
+17
View File
@@ -0,0 +1,17 @@
{{- if and .Values.persistence.enabled (not .Values.persistence.existingClaim) }}
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: {{ include "immichframe.fullname" . }}-config
labels:
{{- include "immichframe.labels" . | nindent 4 }}
spec:
accessModes:
{{- toYaml .Values.persistence.accessModes | nindent 4 }}
{{- with .Values.persistence.storageClass }}
storageClassName: {{ . }}
{{- end }}
resources:
requests:
storage: {{ .Values.persistence.size }}
{{- end }}
+15
View File
@@ -0,0 +1,15 @@
apiVersion: v1
kind: Service
metadata:
name: {{ include "immichframe.fullname" . }}
labels:
{{- include "immichframe.labels" . | nindent 4 }}
spec:
type: {{ .Values.service.type }}
ports:
- name: http
port: {{ .Values.service.port }}
protocol: TCP
targetPort: http
selector:
{{- include "immichframe.selectorLabels" . | nindent 4 }}
+178
View File
@@ -0,0 +1,178 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object",
"additionalProperties": false,
"properties": {
"replicaCount": { "type": "integer", "minimum": 1 },
"image": {
"type": "object",
"additionalProperties": false,
"required": ["registry", "repository", "tag", "pullPolicy"],
"properties": {
"registry": { "type": "string", "minLength": 1 },
"repository": { "type": "string", "minLength": 1 },
"tag": { "type": "string" },
"pullPolicy": { "enum": ["Always", "IfNotPresent", "Never"] }
}
},
"imagePullSecrets": {
"type": "array",
"items": {
"oneOf": [
{ "type": "string", "minLength": 1 },
{
"type": "object",
"additionalProperties": false,
"required": ["name"],
"properties": { "name": { "type": "string", "minLength": 1 } }
}
]
}
},
"nameOverride": { "type": "string" },
"fullnameOverride": { "type": "string" },
"timezone": { "type": "string", "minLength": 1 },
"config": {
"type": "object",
"additionalProperties": false,
"required": ["general", "accounts", "secretRevision"],
"properties": {
"secretRevision": { "type": "string" },
"general": { "type": "object" },
"accounts": {
"type": "array",
"minItems": 1,
"items": {
"type": "object",
"additionalProperties": false,
"required": ["immichServerUrl", "apiKeySecret", "options"],
"properties": {
"immichServerUrl": { "type": "string", "format": "uri" },
"apiKeySecret": {
"type": "object",
"additionalProperties": false,
"required": ["name", "key"],
"properties": {
"name": { "type": "string", "minLength": 1 },
"key": { "type": "string", "minLength": 1 }
}
},
"options": { "type": "object" }
}
}
}
}
},
"service": {
"type": "object",
"additionalProperties": false,
"required": ["type", "port"],
"properties": {
"type": { "enum": ["ClusterIP", "NodePort", "LoadBalancer"] },
"port": { "type": "integer", "minimum": 1, "maximum": 65535 }
}
},
"ingress": {
"type": "object",
"additionalProperties": false,
"required": ["enabled", "className", "annotations", "hosts", "tls"],
"properties": {
"enabled": { "type": "boolean" },
"className": { "type": "string" },
"annotations": { "type": "object", "additionalProperties": { "type": "string" } },
"hosts": {
"type": "array",
"items": {
"type": "object",
"additionalProperties": false,
"required": ["host", "paths"],
"properties": {
"host": { "type": "string", "minLength": 1 },
"paths": {
"type": "array",
"minItems": 1,
"items": {
"type": "object",
"additionalProperties": false,
"required": ["path", "pathType"],
"properties": {
"path": { "type": "string", "minLength": 1 },
"pathType": { "enum": ["Exact", "Prefix", "ImplementationSpecific"] }
}
}
}
}
}
},
"tls": { "type": "array", "items": { "type": "object" } }
}
},
"persistence": {
"type": "object",
"additionalProperties": false,
"required": ["enabled", "existingClaim", "storageClass", "accessModes", "size"],
"properties": {
"enabled": { "type": "boolean" },
"existingClaim": { "type": "string" },
"storageClass": { "type": "string" },
"accessModes": { "type": "array", "minItems": 1, "items": { "type": "string" } },
"size": { "type": "string", "minLength": 1 }
}
},
"podDisruptionBudget": {
"type": "object",
"additionalProperties": false,
"required": ["enabled", "minAvailable"],
"properties": {
"enabled": { "type": "boolean" },
"minAvailable": { "oneOf": [{ "type": "integer", "minimum": 0 }, { "type": "string" }] }
}
},
"prometheusRule": {
"type": "object",
"additionalProperties": false,
"required": ["enabled", "labels"],
"properties": {
"enabled": { "type": "boolean" },
"labels": { "type": "object", "additionalProperties": { "type": "string" } }
}
},
"podAntiAffinity": {
"type": "object",
"additionalProperties": false,
"required": ["enabled"],
"properties": { "enabled": { "type": "boolean" } }
},
"podAnnotations": { "type": "object", "additionalProperties": { "type": "string" } },
"podLabels": { "type": "object", "additionalProperties": { "type": "string" } },
"podSecurityContext": { "type": "object" },
"securityContext": { "type": "object" },
"resources": { "type": "object" },
"startupProbe": { "$ref": "#/$defs/probe" },
"readinessProbe": { "$ref": "#/$defs/probe" },
"livenessProbe": { "$ref": "#/$defs/probe" },
"nodeSelector": { "type": "object" },
"tolerations": { "type": "array", "items": { "type": "object" } },
"affinity": { "type": "object" }
},
"required": [
"replicaCount", "image", "imagePullSecrets", "nameOverride",
"fullnameOverride", "timezone", "config", "service", "ingress",
"persistence", "podDisruptionBudget", "prometheusRule",
"podAntiAffinity", "podAnnotations", "podLabels", "podSecurityContext",
"securityContext", "resources", "startupProbe", "readinessProbe",
"livenessProbe", "nodeSelector", "tolerations", "affinity"
],
"$defs": {
"probe": {
"type": "object",
"additionalProperties": false,
"properties": {
"failureThreshold": { "type": "integer", "minimum": 1 },
"initialDelaySeconds": { "type": "integer", "minimum": 0 },
"periodSeconds": { "type": "integer", "minimum": 1 },
"timeoutSeconds": { "type": "integer", "minimum": 1 }
}
}
}
}
+92
View File
@@ -0,0 +1,92 @@
replicaCount: 1
image:
registry: ghcr.io
repository: immichframe/immichframe
tag: ""
pullPolicy: IfNotPresent
imagePullSecrets: []
nameOverride: ""
fullnameOverride: ""
timezone: Etc/UTC
config:
secretRevision: ""
general:
Interval: 45
ShowClock: true
accounts:
- immichServerUrl: https://immich.example.com
apiKeySecret:
name: immichframe-api
key: api-key
options: {}
service:
type: ClusterIP
port: 80
ingress:
enabled: false
className: ""
annotations: {}
hosts: []
tls: []
persistence:
enabled: false
existingClaim: ""
storageClass: ""
accessModes:
- ReadWriteOnce
size: 1Gi
podDisruptionBudget:
enabled: false
minAvailable: 1
prometheusRule:
enabled: false
labels: {}
podAntiAffinity:
enabled: true
podAnnotations: {}
podLabels: {}
podSecurityContext:
seccompProfile:
type: RuntimeDefault
securityContext:
allowPrivilegeEscalation: false
capabilities:
drop:
- ALL
readOnlyRootFilesystem: false
runAsNonRoot: true
resources: {}
startupProbe:
failureThreshold: 30
periodSeconds: 2
timeoutSeconds: 1
readinessProbe:
failureThreshold: 3
periodSeconds: 10
timeoutSeconds: 1
livenessProbe:
failureThreshold: 3
initialDelaySeconds: 10
periodSeconds: 30
timeoutSeconds: 1
nodeSelector: {}
tolerations: []
affinity: {}
Executable
+41
View File
@@ -0,0 +1,41 @@
#!/bin/sh
set -eu
project_dir=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
chart_dir="$project_dir/chart/immichframe"
rendered_file="${TMPDIR:-/tmp}/immichframe-chart-rendered.yaml"
helm lint "$chart_dir"
helm template immichframe "$chart_dir" \
--namespace test-namespace \
--set replicaCount=2 \
--set ingress.enabled=true \
--set ingress.className=example \
--set ingress.hosts[0].host=frame.example.com \
--set ingress.hosts[0].paths[0].path=/ \
--set ingress.hosts[0].paths[0].pathType=Prefix \
--set podDisruptionBudget.enabled=true \
--set prometheusRule.enabled=true \
--set persistence.enabled=true \
> "$rendered_file"
ruby -ryaml -e '
documents = YAML.load_stream(File.read(ARGV.fetch(0))).compact
find = ->(kind) { documents.fetch(documents.index { |doc| doc["kind"] == kind }) }
deployment = find.call("Deployment")
service = find.call("Service")
ingress = find.call("Ingress")
config = find.call("ConfigMap")
settings = YAML.safe_load(config.fetch("data").fetch("Settings.yml"))
container = deployment.dig("spec", "template", "spec", "containers").fetch(0)
selector = deployment.dig("spec", "selector", "matchLabels")
raise "Service selector drift" unless service.dig("spec", "selector") == selector
raise "Ingress service drift" unless ingress.dig("spec", "rules", 0, "http", "paths", 0, "backend", "service", "name") == service.dig("metadata", "name")
raise "Probe port drift" unless %w[startupProbe readinessProbe livenessProbe].all? { |probe| container.dig(probe, "tcpSocket", "port") == "http" }
account = settings.fetch("Accounts").fetch(0)
raise "API key leaked into ConfigMap" if account.key?("ApiKey")
raise "API key file missing" unless account.fetch("ApiKeyFile").start_with?("/app/Secrets/")
raise "PVC not rendered" unless documents.any? { |doc| doc["kind"] == "PersistentVolumeClaim" }
raise "PDB not rendered" unless documents.any? { |doc| doc["kind"] == "PodDisruptionBudget" }
raise "PrometheusRule not rendered" unless documents.any? { |doc| doc["kind"] == "PrometheusRule" }
' "$rendered_file"