Introduce helm chart and CI and CD
Validate, publish and deploy Gramps Web / validate (push) Failing after 29m16s
Validate, publish and deploy Gramps Web / publish (push) Skipped
Validate, publish and deploy Gramps Web / deploy (push) Skipped

This commit is contained in:
2026-09-06 11:26:19 +02:00
parent c8c2807e31
commit 9933f9904a
30 changed files with 1877 additions and 1 deletions
+137
View File
@@ -0,0 +1,137 @@
name: Validate, publish and deploy Gramps Web
on:
push:
pull_request:
env:
CHART_REGISTRY: oci://registry.brunner.ninja/feedc0de/charts
POSTGRES_IMAGE: registry.brunner.ninja/feedc0de/postgres-with-locales
POSTGRES_IMAGE_TAG: 18-locales
HELM_VERSION: v4.2.2
HELM_SHA256: 9adafecab4d406853bba163a70e9f104f47dbbf65ce24b7653bae7e36150bcb6
KUBECTL_VERSION: v1.36.3
KUBERNETES_API: https://host.containers.internal:6443
KUBERNETES_TLS_SERVER_NAME: 192.168.0.2
jobs:
validate:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install pinned Helm
run: |
curl -fsSL -o "${RUNNER_TEMP}/helm.tgz" "https://get.helm.sh/helm-${HELM_VERSION}-linux-amd64.tar.gz"
printf '%s %s\n' "${HELM_SHA256}" "${RUNNER_TEMP}/helm.tgz" | sha256sum --check
tar -xzf "${RUNNER_TEMP}/helm.tgz" -C "${RUNNER_TEMP}" --strip-components=1 linux-amd64/helm
echo "${RUNNER_TEMP}" >> "${GITHUB_PATH}"
- name: Validate chart and secret hygiene
run: bash test.sh
- name: Validate Kubernetes schemas
run: |
helm template gramps . --namespace gramps --values values-arschrock.yaml \
| docker run --rm --interactive \
ghcr.io/yannh/kubeconform:v0.7.0@sha256:85dbef6b4b312b99133decc9c6fc9495e9fc5f92293d4ff3b7e1b30f5611823c \
-strict -exit-on-error -ignore-missing-schemas -summary
- name: Build and test locale-enabled PostgreSQL
run: |
set -euo pipefail
base_version=$(docker run --rm docker.io/bitnami/postgresql:latest postgres --version)
[[ "${base_version}" == *"PostgreSQL) 18."* ]]
docker build --pull --tag "${POSTGRES_IMAGE}:ci" postgresql-image
docker run --rm --entrypoint bash "${POSTGRES_IMAGE}:ci" -ec \
'postgres --version | grep -E "PostgreSQL\) 18\."; locale -a | grep -Fx de_AT.utf8; locale -a | grep -Fx de_DE.utf8; locale -a | grep -Fx en_GB.utf8; locale -a | grep -Fx en_US.utf8'
publish:
if: gitea.event_name == 'push' && (gitea.ref == 'refs/heads/main' || startsWith(gitea.ref, 'refs/tags/v'))
needs: validate
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install pinned Helm
run: |
curl -fsSL -o "${RUNNER_TEMP}/helm.tgz" "https://get.helm.sh/helm-${HELM_VERSION}-linux-amd64.tar.gz"
printf '%s %s\n' "${HELM_SHA256}" "${RUNNER_TEMP}/helm.tgz" | sha256sum --check
tar -xzf "${RUNNER_TEMP}/helm.tgz" -C "${RUNNER_TEMP}" --strip-components=1 linux-amd64/helm
echo "${RUNNER_TEMP}" >> "${GITHUB_PATH}"
- name: Build and publish image and chart
env:
COMMIT_SHA: ${{ gitea.sha }}
GIT_REF: ${{ gitea.ref }}
QUAY_USERNAME: ${{ secrets.QUAY_USERNAME }}
QUAY_TOKEN: ${{ secrets.QUAY_TOKEN }}
run: |
set -euo pipefail
test -n "${QUAY_USERNAME}"
test -n "${QUAY_TOKEN}"
if [[ "${GIT_REF}" == refs/tags/v* ]]; then
chart_version="${GIT_REF#refs/tags/v}"
else
chart_version="0.0.0-dev.sha${COMMIT_SHA:0:12}"
fi
base_version=$(docker run --rm docker.io/bitnami/postgresql:latest postgres --version)
[[ "${base_version}" == *"PostgreSQL) 18."* ]]
docker build --pull \
--label "org.opencontainers.image.revision=${COMMIT_SHA}" \
--label "org.opencontainers.image.source=https://code.brunner.ninja/feedc0de/gramps-web-deployment" \
--tag "${POSTGRES_IMAGE}:${POSTGRES_IMAGE_TAG}" postgresql-image
helm dependency update .
mkdir -p "${RUNNER_TEMP}/packages"
helm package . --destination "${RUNNER_TEMP}/packages" \
--version "${chart_version}" --app-version latest
package="${RUNNER_TEMP}/packages/gramps-web-${chart_version}.tgz"
test -f "${package}"
helm template gramps "${package}" --namespace gramps \
--values values-arschrock.yaml > "${RUNNER_TEMP}/rendered.yaml"
printf '%s' "${QUAY_TOKEN}" | docker login registry.brunner.ninja \
--username "${QUAY_USERNAME}" --password-stdin
printf '%s' "${QUAY_TOKEN}" | helm registry login registry.brunner.ninja \
--username "${QUAY_USERNAME}" --password-stdin
docker push "${POSTGRES_IMAGE}:${POSTGRES_IMAGE_TAG}"
helm push "${package}" "${CHART_REGISTRY}"
deploy:
if: gitea.ref == 'refs/heads/main'
needs: publish
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install pinned clients
run: |
curl -fsSL -o "${RUNNER_TEMP}/helm.tgz" "https://get.helm.sh/helm-${HELM_VERSION}-linux-amd64.tar.gz"
printf '%s %s\n' "${HELM_SHA256}" "${RUNNER_TEMP}/helm.tgz" | sha256sum --check
tar -xzf "${RUNNER_TEMP}/helm.tgz" -C "${RUNNER_TEMP}" --strip-components=1 linux-amd64/helm
curl -fsSL -o "${RUNNER_TEMP}/kubectl" "https://dl.k8s.io/release/${KUBECTL_VERSION}/bin/linux/amd64/kubectl"
curl -fsSL -o "${RUNNER_TEMP}/kubectl.sha256" "https://dl.k8s.io/release/${KUBECTL_VERSION}/bin/linux/amd64/kubectl.sha256"
printf '%s %s\n' "$(cat "${RUNNER_TEMP}/kubectl.sha256")" "${RUNNER_TEMP}/kubectl" | sha256sum --check
chmod 0700 "${RUNNER_TEMP}/helm" "${RUNNER_TEMP}/kubectl"
echo "${RUNNER_TEMP}" >> "${GITHUB_PATH}"
- name: Configure registry and Kubernetes access
env:
KUBE_CONFIG_BASE64: ${{ secrets.KUBE_CONFIG_BASE64 }}
QUAY_USERNAME: ${{ secrets.QUAY_USERNAME }}
QUAY_TOKEN: ${{ secrets.QUAY_TOKEN }}
run: |
test -n "${KUBE_CONFIG_BASE64}"
printf '%s' "${KUBE_CONFIG_BASE64}" | base64 --decode > "${RUNNER_TEMP}/kubeconfig"
chmod 0600 "${RUNNER_TEMP}/kubeconfig"
export KUBECONFIG="${RUNNER_TEMP}/kubeconfig"
kubectl config set-cluster cluster --server="${KUBERNETES_API}" --tls-server-name="${KUBERNETES_TLS_SERVER_NAME}"
printf '%s' "${QUAY_TOKEN}" | helm registry login registry.brunner.ninja \
--username "${QUAY_USERNAME}" --password-stdin
- name: Deploy the published OCI chart
env:
COMMIT_SHA: ${{ gitea.sha }}
run: |
set -euo pipefail
export KUBECONFIG="${RUNNER_TEMP}/kubeconfig"
chart_version="0.0.0-dev.sha${COMMIT_SHA:0:12}"
CHART_VERSION="${chart_version}" ./deploy.sh
kubectl --namespace gramps rollout restart deployment/grampsweb deployment/grampsweb-celery
kubectl --namespace gramps rollout status deployment/grampsweb --timeout=10m
kubectl --namespace gramps rollout status deployment/grampsweb-celery --timeout=10m
kubectl --namespace gramps rollout status statefulset/postgresql --timeout=10m
kubectl --namespace gramps rollout status statefulset/valkey-primary --timeout=10m
+10
View File
@@ -0,0 +1,10 @@
charts/
*.tgz
.rendered/
# Local migration leftovers and the old nested Bitnami checkout are not chart sources.
postgresql/
valkey/
gramps.yaml
values-local.yaml
secrets*.yaml
+7
View File
@@ -0,0 +1,7 @@
.git/
.gitea/
.idea/
.vscode/
postgresql/
valkey/
*.tmp
+9
View File
@@ -0,0 +1,9 @@
dependencies:
- name: postgresql
repository: oci://registry-1.docker.io/bitnamicharts
version: 18.8.17
- name: valkey
repository: oci://registry-1.docker.io/bitnamicharts
version: 6.2.19
digest: sha256:d1e1f89668c809ac8c7df096f92bbc7439b3ee40ec57b1cde1f929e2d5ef2c9e
generated: "2026-09-06T08:59:30.91876833+02:00"
+19
View File
@@ -0,0 +1,19 @@
apiVersion: v2
name: gramps-web
description: A reusable Helm chart for Gramps Web with optional PostgreSQL and Valkey
type: application
version: 0.1.0
appVersion: latest
home: https://www.grampsweb.org/
sources:
- https://github.com/gramps-project/gramps-web
dependencies:
- name: postgresql
repository: oci://registry-1.docker.io/bitnamicharts
version: ^18.0.0
condition: postgresql.enabled
- name: valkey
repository: oci://registry-1.docker.io/bitnamicharts
version: ^6.0.0
condition: valkey.enabled
+21
View File
@@ -0,0 +1,21 @@
# Existing deployment migration
The live PostgreSQL and Valkey instances are already Helm releases. The goal is
to adopt their resources into one `gramps` Helm release without replacing the
PostgreSQL PVC.
1. Run `./backup.sh` and verify the gzip file.
2. Run `./bootstrap-secrets.sh`. This separates live credentials into runtime,
PostgreSQL, and Valkey Secrets without printing them.
3. Run `./preflight.sh`. It verifies the required Secret keys, bound PVCs,
StatefulSet immutable fields, and a server-side Helm dry run.
4. Install the published OCI package with `./deploy.sh`. The script uses
`--take-ownership`; the rendered StatefulSets deliberately retain the old
names, selectors, services, and volume-claim-template names.
5. Verify PostgreSQL, Gramps Web, Celery, OIDC, media, and the public URL.
6. Back up the old Helm release Secrets, then delete only the release metadata
Secrets named `sh.helm.release.v1.postgresql.*` and
`sh.helm.release.v1.valkey.*`. Do not uninstall the old releases.
PostgreSQL 18 to a newer 18.x image is a minor upgrade. PostgreSQL 18 to 19 is
a major upgrade and requires a separately planned `pg_upgrade` or dump/restore.
+220 -1
View File
@@ -1,2 +1,221 @@
# gramps-kubernetes
# Gramps Web Helm chart
This repository packages Gramps Web as a reusable Helm chart. PostgreSQL and
Valkey are enabled by default through the Bitnami subcharts, but either service
can be disabled and replaced independently. OIDC, S3 media storage, SMTP,
workers, persistence, ingress, images, resources, and scheduling are optional
or configurable.
## Quick start
```sh
helm install family oci://registry.brunner.ninja/feedc0de/charts/gramps-web \
--namespace gramps-web --create-namespace
```
The default installation creates random PostgreSQL, Valkey, and application
secrets. Helm and the subcharts preserve those values across normal upgrades.
Do not use `--set ...password=...` in production: command history and Helm
release values can retain it. Create a Secret and use an `existingSecret`
setting instead.
## Data service choices
Bundled PostgreSQL and Valkey are independent:
```yaml
postgresql:
enabled: true
valkey:
enabled: true
```
For external services:
```yaml
postgresql:
enabled: false
database:
host: postgres.database.svc
port: 5432
username: gramps
auth:
existingSecret: gramps-database
existingSecretKey: password
valkey:
enabled: false
cache:
host: valkey.cache.svc
port: 6379
auth:
existingSecret: gramps-valkey
existingSecretKey: valkey-password
```
The bundled charts remain fully configurable under `postgresql.*` and
`valkey.*`. For example, a locale-capable PostgreSQL image can be selected
without changing this chart:
```yaml
global:
security:
allowInsecureImages: true
postgresql:
image:
registry: registry.example.com
repository: platform/postgres-with-locales
tag: 18-locales
```
Locales are supplied by the container image, not generated by Helm. The
included arschrock image recipe generates `de_AT.UTF-8`, `de_DE.UTF-8`,
`en_GB.UTF-8`, and `en_US.UTF-8`; its `LOCALES` build argument can be extended
for other browser locales.
PostgreSQL chart upgrades update Kubernetes resources and the image. They do
not convert a data directory between PostgreSQL major versions. Stay on the
same PostgreSQL major for routine upgrades; use `pg_upgrade` or dump/restore
for a major upgrade.
## OIDC / Authentik
OIDC is off by default. A provider such as Authentik can be enabled with:
```yaml
oidc:
enabled: true
issuer: https://auth.example.com/application/o/gramps/
clientId: gramps-web
clientSecret:
existingSecret: gramps-oidc
existingSecretKey: client-secret
name: Company login
disableLocalAuth: true
autoRedirect: true
groups:
admin: Gramps Admins
editor: Gramps Editors
```
Keep local authentication enabled until OIDC login and role mapping have been
tested. Gramps supports custom OIDC providers, including Authentik, and can map
OIDC groups to its roles.
## Replicas
`replicaCount` and `worker.replicaCount` are configurable. Multiple web pods
should only be attempted when tree data, user data, the search index, media,
cache/broker, and application secret are all shared. The safer first scaling
step is setting `GUNICORN_NUM_WORKERS` through `config.extraEnv`; upstream
documents multiple Gunicorn workers, while it does not document Kubernetes
web replicas as a supported topology. The chart therefore defaults to one web
pod.
## Health checks and metrics
Gramps Web exposes `/ready`, but upstream currently implements it as a shallow
HTTP response rather than a dependency check. The chart therefore uses:
- `/ready` for startup and liveness, so a temporary dependency outage does not
create a web-pod restart loop;
- a deep web readiness probe that checks `/ready`, runs an authenticated
PostgreSQL `SELECT 1`, and performs an authenticated Valkey `PING`;
- a targeted Celery self-ping for worker startup, readiness, and liveness;
- the PostgreSQL and Valkey subcharts' native startup, readiness, and liveness
probes.
The probe blocks traffic when the core API/database/cache path is broken, but
it deliberately does not make pod health depend on Authentik, SMTP, or S3.
Those should be monitored with external synthetic checks.
Gramps Web does not currently expose Prometheus application metrics. The
bundled data-service charts do have optional exporters and ServiceMonitors:
```yaml
postgresql:
metrics:
enabled: true
serviceMonitor:
enabled: true
valkey:
metrics:
enabled: true
serviceMonitor:
enabled: true
```
They remain disabled by default to avoid imposing exporter CPU/memory requests
or Prometheus Operator CRDs on chart users.
## Persistence and upgrades
The application PVC has a `helm.sh/resource-policy: keep` annotation by
default. PostgreSQL and Valkey persistence are controlled by their subchart
values. Back up PostgreSQL before any image or chart upgrade:
```sh
./backup.sh
```
The application image defaults to `ghcr.io/gramps-project/grampsweb:latest`
with `imagePullPolicy: Always`. Set `image.tag` to a release tag if you prefer
reproducible rollouts.
## arschrock deployment
[`values-arschrock.yaml`](values-arschrock.yaml) contains only non-secret
cluster configuration. Existing credentials are moved by
`bootstrap-secrets.sh` into `gramps-runtime`, `gramps-postgresql`, and
`gramps-valkey`; none is rendered or committed.
Initial migration:
```sh
./backup.sh
./bootstrap-secrets.sh
./preflight.sh
kubectl apply -f ci-deployer.yaml
./create-ci-kubeconfig.sh # save its output as Gitea KUBE_CONFIG_BASE64
```
Also configure `QUAY_USERNAME` and `QUAY_TOKEN` in Gitea. CI validates the
chart, builds and tests the locale-enabled PostgreSQL 18 image, publishes the
OCI chart to `registry.brunner.ninja/feedc0de/charts/gramps-web`, and deploys
that exact chart package to the `gramps` namespace from the registry.
This deployment stores Helm release metadata in ConfigMaps. The CI identity
cannot list Secrets or read `gramps-runtime` (OIDC, S3, database URIs, and app
key). Bitnami's templates require direct lookup access to their configured
password Secrets, so CI can read only `gramps-postgresql`, `gramps-valkey`,
and the legacy `postgresql` Secret that its create-or-reuse helper checks.
The initial Helm deployment uses `--take-ownership` and preserves the existing
StatefulSet/PVC names. Old `postgresql` and `valkey` Helm release records must
only be removed after the new `gramps` release is healthy; never run
`helm uninstall postgresql` or `helm uninstall valkey` after adoption because
their historical manifests describe the live resources.
Automatic Helm rollback is intentionally disabled for the first ownership
transfer because an install rollback could delete adopted resources. It is
enabled automatically for every later upgrade.
## Development
```sh
./test.sh
./render.sh
```
`Chart.lock` records the last-tested dependency packages. `Chart.yaml` uses
compatible major-version ranges, and CI runs `helm dependency update` when it
packages a release. This allows current subchart releases while avoiding an
unreviewed breaking subchart-major jump.
Upstream references:
- [Gramps Web configuration](https://github.com/gramps-project/gramps-web-docs/blob/main/docs/en/install_setup/configuration.md)
- [Gramps Web OIDC configuration](https://github.com/gramps-project/gramps-web-docs/blob/main/docs/en/install_setup/oidc.md)
- [Gramps Web PostgreSQL and locale guidance](https://github.com/gramps-project/gramps-web-docs/blob/main/docs/en/install_setup/postgres.md)
- [Bitnami PostgreSQL chart](https://github.com/bitnami/charts/tree/main/bitnami/postgresql)
- [Bitnami Valkey chart](https://github.com/bitnami/charts/tree/main/bitnami/valkey)
Executable
+16
View File
@@ -0,0 +1,16 @@
#!/usr/bin/env bash
set -euo pipefail
namespace=${NAMESPACE:-gramps}
pod=${POSTGRES_POD:-postgresql-0}
destination=${1:-"/tmp/gramps-postgresql-$(date +%Y%m%d-%H%M%S).sql.gz"}
umask 077
kubectl --namespace "${namespace}" exec "${pod}" -- bash -ec \
'PGPASSWORD="$(cat "${POSTGRES_PASSWORD_FILE}")" pg_dumpall --username=postgres' \
| gzip -9 > "${destination}"
gzip --test "${destination}"
test -s "${destination}"
echo "PostgreSQL backup written to ${destination} (mode 0600)."
+59
View File
@@ -0,0 +1,59 @@
#!/usr/bin/env bash
# One-time helper for the existing arschrock deployment. It copies credentials
# from live resources into one Kubernetes Secret without printing them.
set -euo pipefail
namespace=${NAMESPACE:-gramps}
workdir=$(mktemp --directory)
trap 'rm -rf "${workdir}"' EXIT HUP INT TERM
chmod 0700 "${workdir}"
extract_env() {
local deployment=$1 name=$2 destination=$3
kubectl --namespace "${namespace}" get deployment "${deployment}" --output=json \
| jq --raw-output --join-output --arg name "${name}" \
'.spec.template.spec.containers[0].env[] | select(.name == $name) | .value' \
> "${destination}"
test -s "${destination}"
}
extract_secret() {
local source_secret=$1 key=$2 destination=$3
kubectl --namespace "${namespace}" get secret "${source_secret}" --output=json \
| jq --raw-output --join-output --arg key "${key}" '.data[$key]' \
| base64 --decode > "${destination}"
test -s "${destination}"
}
extract_env grampsweb-celery GRAMPSWEB_SECRET_KEY "${workdir}/app-secret-key" \
|| openssl rand -hex 32 > "${workdir}/app-secret-key"
extract_env grampsweb GRAMPSWEB_POSTGRES_PASSWORD "${workdir}/password"
extract_env grampsweb GRAMPSWEB_USER_DB_URI "${workdir}/user-database-uri"
extract_env grampsweb GRAMPSWEB_SEARCH_INDEX_DB_URI "${workdir}/search-index-database-uri"
extract_env grampsweb GRAMPSWEB_OIDC_CLIENT_SECRET "${workdir}/oidc-client-secret"
extract_env grampsweb AWS_ACCESS_KEY_ID "${workdir}/aws-access-key-id"
extract_env grampsweb AWS_SECRET_ACCESS_KEY "${workdir}/aws-secret-access-key"
extract_secret postgresql postgres-password "${workdir}/postgres-password"
extract_secret valkey valkey-password "${workdir}/valkey-password"
kubectl --namespace "${namespace}" create secret generic gramps-runtime \
--from-file="${workdir}/app-secret-key" \
--from-file="${workdir}/user-database-uri" \
--from-file="${workdir}/search-index-database-uri" \
--from-file="${workdir}/oidc-client-secret" \
--from-file="${workdir}/aws-access-key-id" \
--from-file="${workdir}/aws-secret-access-key" \
--dry-run=client --output=yaml \
| kubectl apply --filename=-
kubectl --namespace "${namespace}" create secret generic gramps-postgresql \
--from-file="${workdir}/postgres-password" \
--from-file="${workdir}/password" \
--dry-run=client --output=yaml \
| kubectl apply --filename=-
kubectl --namespace "${namespace}" create secret generic gramps-valkey \
--from-file="${workdir}/valkey-password" \
--dry-run=client --output=yaml \
| kubectl apply --filename=-
echo "Runtime, PostgreSQL, and Valkey Secrets are ready; no values were printed."
+56
View File
@@ -0,0 +1,56 @@
apiVersion: v1
kind: ServiceAccount
metadata:
name: gramps-deployer
namespace: gramps
---
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: gramps-deployer
namespace: gramps
rules:
- apiGroups: [""]
resources: ["configmaps", "endpoints", "persistentvolumeclaims", "pods", "serviceaccounts", "services"]
verbs: ["get", "list", "watch", "create", "update", "patch", "delete"]
# The upstream subcharts look up these two existing Secrets while rendering.
# CI cannot list Secrets or read gramps-runtime (OIDC, S3, and app credentials).
- apiGroups: [""]
resources: ["secrets"]
resourceNames: ["gramps-postgresql", "gramps-valkey", "postgresql"]
verbs: ["get"]
- apiGroups: ["apps"]
resources: ["deployments", "replicasets", "statefulsets"]
verbs: ["get", "list", "watch", "create", "update", "patch", "delete"]
- apiGroups: ["batch"]
resources: ["jobs"]
verbs: ["get", "list", "watch", "create", "update", "patch", "delete"]
- apiGroups: ["networking.k8s.io"]
resources: ["ingresses", "networkpolicies"]
verbs: ["get", "list", "watch", "create", "update", "patch", "delete"]
- apiGroups: ["policy"]
resources: ["poddisruptionbudgets"]
verbs: ["get", "list", "watch", "create", "update", "patch", "delete"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: gramps-deployer
namespace: gramps
subjects:
- kind: ServiceAccount
name: gramps-deployer
namespace: gramps
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: Role
name: gramps-deployer
---
apiVersion: v1
kind: Secret
metadata:
name: gramps-deployer-token
namespace: gramps
annotations:
kubernetes.io/service-account.name: gramps-deployer
type: kubernetes.io/service-account-token
+37
View File
@@ -0,0 +1,37 @@
#!/usr/bin/env bash
set -euo pipefail
project_dir=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)
namespace=gramps
account=gramps-deployer
secret=gramps-deployer-token
kubectl apply --filename "${project_dir}/ci-deployer.yaml" >&2
for attempt in {1..30}; do
token=$(kubectl --namespace "${namespace}" get secret "${secret}" \
--output=jsonpath='{.data.token}' 2>/dev/null || true)
[[ -n "${token}" ]] && break
sleep 1
done
[[ -n "${token:-}" ]] || { echo "Token was not issued" >&2; exit 1; }
workdir=$(mktemp --directory)
trap 'rm -rf "${workdir}"' EXIT HUP INT TERM
server=$(kubectl config view --minify --output=jsonpath='{.clusters[0].cluster.server}')
kubectl --namespace "${namespace}" get secret "${secret}" \
--output=jsonpath='{.data.ca\.crt}' | base64 --decode > "${workdir}/ca.crt"
export KUBECONFIG="${workdir}/config"
kubectl config set-cluster cluster --server="${server}" \
--certificate-authority="${workdir}/ca.crt" --embed-certs=true >/dev/null
kubectl config set-credentials "${account}" \
--token="$(printf '%s' "${token}" | base64 --decode)" >/dev/null
kubectl config set-context gramps --cluster=cluster --user="${account}" \
--namespace="${namespace}" >/dev/null
kubectl config use-context gramps >/dev/null
echo "Store the next line as the Gitea Actions secret KUBE_CONFIG_BASE64; do not commit it." >&2
base64 --wrap=0 "${KUBECONFIG}"
printf '\n'
Executable
+27
View File
@@ -0,0 +1,27 @@
#!/usr/bin/env bash
set -euo pipefail
export HELM_DRIVER=${HELM_DRIVER:-configmap}
registry=${CHART_REGISTRY:-oci://registry.brunner.ninja/feedc0de/charts}
version=${CHART_VERSION:?Set CHART_VERSION to the published chart version}
namespace=${NAMESPACE:-gramps}
release=${RELEASE:-gramps}
project_dir=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)
helm_args=(
upgrade --install "${release}" "${registry}/gramps-web"
--version "${version}"
--namespace "${namespace}" --create-namespace
--values "${project_dir}/values-arschrock.yaml"
--take-ownership --wait --timeout 20m
)
# Never auto-uninstall resources during the initial ownership transfer. Once
# the parent release exists, normal upgrades may safely roll back to it.
if helm status "${release}" --namespace "${namespace}" 2>/dev/null \
| grep -q '^STATUS: deployed$'; then
helm_args+=(--rollback-on-failure)
fi
helm "${helm_args[@]}"
Executable
+17
View File
@@ -0,0 +1,17 @@
#!/usr/bin/env bash
set -euo pipefail
project_dir=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)
release=${RELEASE:-gramps-web}
namespace=${NAMESPACE:-gramps-web}
values=${VALUES_FILE:-"${project_dir}/values.yaml"}
dependencies=("${project_dir}"/charts/*.tgz)
if (( ${#dependencies[@]} < 2 )); then
helm dependency build --skip-refresh "${project_dir}"
fi
helm upgrade --install "${release}" "${project_dir}" \
--namespace "${namespace}" --create-namespace \
--values "${values}" \
--rollback-on-failure --wait --timeout 15m
+14
View File
@@ -0,0 +1,14 @@
ARG BASE_IMAGE=docker.io/bitnami/postgresql:latest
FROM ${BASE_IMAGE}
USER 0
ARG LOCALES="de_AT de_DE en_GB en_US"
RUN for locale_name in ${LOCALES}; do \
localedef --force --inputfile="${locale_name}" --charmap=UTF-8 "${locale_name}.UTF-8"; \
done \
&& tdnf clean all
ENV LANG=en_US.UTF-8 \
LANGUAGE=en_US:en
USER 1001
Executable
+78
View File
@@ -0,0 +1,78 @@
#!/usr/bin/env bash
set -euo pipefail
export HELM_DRIVER=${HELM_DRIVER:-configmap}
project_dir=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)
workdir=$(mktemp --directory)
trap 'rm -rf "${workdir}"' EXIT HUP INT TERM
namespace=${NAMESPACE:-gramps}
for key in app-secret-key user-database-uri search-index-database-uri \
oidc-client-secret aws-access-key-id aws-secret-access-key; do
kubectl --namespace "${namespace}" get secret gramps-runtime \
--output="jsonpath={.data.${key}}" | grep -q . \
|| { echo "gramps-runtime is missing ${key}" >&2; exit 1; }
done
for item in gramps-postgresql:postgres-password gramps-postgresql:password \
gramps-valkey:valkey-password; do
secret=${item%%:*}
key=${item#*:}
kubectl --namespace "${namespace}" get secret "${secret}" \
--output="jsonpath={.data.${key}}" | grep -q . \
|| { echo "${secret} is missing ${key}" >&2; exit 1; }
done
for claim in data-postgresql-0 grampsweb-data; do
phase=$(kubectl --namespace "${namespace}" get pvc "${claim}" \
--output=jsonpath='{.status.phase}')
[[ "${phase}" == Bound ]] || { echo "PVC ${claim} is ${phase}" >&2; exit 1; }
done
dependencies=("${project_dir}"/charts/*.tgz)
if (( ${#dependencies[@]} < 2 )); then
helm dependency build --skip-refresh "${project_dir}"
fi
helm template gramps "${project_dir}" --namespace "${namespace}" \
--values "${project_dir}/values-arschrock.yaml" > "${workdir}/rendered.yaml"
kubectl --namespace "${namespace}" get statefulset postgresql valkey-primary valkey-replicas \
--output=json > "${workdir}/live.json"
python3 - "${workdir}/rendered.yaml" "${workdir}/live.json" <<'PY'
import json
import sys
import yaml
rendered = {
item["metadata"]["name"]: item
for item in yaml.safe_load_all(open(sys.argv[1], encoding="utf-8"))
if item and item.get("kind") == "StatefulSet"
}
live = {
item["metadata"]["name"]: item
for item in json.load(open(sys.argv[2], encoding="utf-8"))["items"]
}
for name in ("postgresql", "valkey-primary", "valkey-replicas"):
expected = rendered[name]["spec"]
current = live[name]["spec"]
checks = {
"selector": (expected["selector"], current["selector"]),
"serviceName": (expected["serviceName"], current["serviceName"]),
"volumeClaimTemplates": (
[item["metadata"]["name"] for item in expected.get("volumeClaimTemplates", [])],
[item["metadata"]["name"] for item in current.get("volumeClaimTemplates", [])],
),
}
for field, (wanted, found) in checks.items():
if wanted != found:
raise SystemExit(f"{name}: immutable {field} differs: wanted={wanted!r} live={found!r}")
print("Live StatefulSet immutable fields match the parent chart.")
PY
helm upgrade --install gramps "${project_dir}" \
--namespace "${namespace}" \
--values "${project_dir}/values-arschrock.yaml" \
--take-ownership --dry-run=server --hide-secret
echo "Migration preflight passed."
Executable
+17
View File
@@ -0,0 +1,17 @@
#!/usr/bin/env bash
set -euo pipefail
project_dir=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)
output=${1:-"${project_dir}/.rendered/gramps-web.yaml"}
values=${2:-"${project_dir}/values.yaml"}
mkdir -p "$(dirname -- "${output}")"
dependencies=("${project_dir}"/charts/*.tgz)
if (( ${#dependencies[@]} < 2 )); then
helm dependency build --skip-refresh "${project_dir}" >/dev/null
fi
helm template gramps-web "${project_dir}" \
--namespace gramps-web \
--values "${values}" > "${output}"
echo "Rendered ${output}"
+15
View File
@@ -0,0 +1,15 @@
Gramps Web is installed as {{ include "gramps-web.fullname" . }}.
{{- if .Values.ingress.enabled }}
Open {{ (first .Values.ingress.hosts).host }} in your browser.
{{- else }}
Run:
kubectl -n {{ .Release.Namespace }} port-forward service/{{ include "gramps-web.fullname" . }} 5000:{{ .Values.service.port }}
Then open http://localhost:5000.
{{- end }}
{{- if or .Values.database.auth.password .Values.cache.auth.password .Values.oidc.clientSecret.value .Values.s3.auth.secretAccessKey .Values.email.auth.password }}
WARNING: one or more plaintext secret values were supplied to Helm. Prefer the
corresponding existingSecret settings so credentials are not retained in Helm values.
{{- end }}
+89
View File
@@ -0,0 +1,89 @@
{{- define "gramps-web.name" -}}
{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }}
{{- end }}
{{- define "gramps-web.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 "gramps-web.workerName" -}}
{{- default (printf "%s-worker" (include "gramps-web.fullname" .)) .Values.worker.nameOverride | trunc 63 | trimSuffix "-" }}
{{- end }}
{{- define "gramps-web.chart" -}}
{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }}
{{- end }}
{{- define "gramps-web.labels" -}}
helm.sh/chart: {{ include "gramps-web.chart" . }}
app.kubernetes.io/name: {{ include "gramps-web.name" . }}
app.kubernetes.io/instance: {{ .Release.Name }}
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
app.kubernetes.io/managed-by: {{ .Release.Service }}
{{- end }}
{{- define "gramps-web.serviceAccountName" -}}
{{- if .Values.serviceAccount.create }}
{{- default (include "gramps-web.fullname" .) .Values.serviceAccount.name }}
{{- else }}
{{- default "default" .Values.serviceAccount.name }}
{{- end }}
{{- end }}
{{- define "gramps-web.appSecretName" -}}
{{- default (include "gramps-web.fullname" .) .Values.appSecret.existingSecret }}
{{- end }}
{{- define "gramps-web.postgresqlHost" -}}
{{- if .Values.postgresql.enabled }}
{{- if .Values.postgresql.fullnameOverride }}{{ .Values.postgresql.fullnameOverride }}{{ else }}{{ printf "%s-postgresql" .Release.Name }}{{ end }}
{{- else }}
{{- required "database.host is required when postgresql.enabled=false" .Values.database.host }}
{{- end }}
{{- end }}
{{- define "gramps-web.postgresqlSecretName" -}}
{{- if .Values.postgresql.enabled }}
{{- if .Values.postgresql.auth.existingSecret }}{{ .Values.postgresql.auth.existingSecret }}{{ else if .Values.postgresql.fullnameOverride }}{{ .Values.postgresql.fullnameOverride }}{{ else }}{{ printf "%s-postgresql" .Release.Name }}{{ end }}
{{- else }}
{{- required "database.auth.existingSecret is required when an external database password is not supplied" .Values.database.auth.existingSecret }}
{{- end }}
{{- end }}
{{- define "gramps-web.postgresqlSecretKey" -}}
{{- if .Values.postgresql.enabled }}
{{- default "password" .Values.postgresql.auth.secretKeys.userPasswordKey }}
{{- else }}
{{- default "password" .Values.database.auth.existingSecretKey }}
{{- end }}
{{- end }}
{{- define "gramps-web.valkeyHost" -}}
{{- if .Values.valkey.enabled }}
{{- if .Values.valkey.fullnameOverride }}{{ .Values.valkey.fullnameOverride }}-primary{{ else }}{{ printf "%s-valkey-primary" .Release.Name }}{{ end }}
{{- else }}
{{- required "cache.host is required when valkey.enabled=false" .Values.cache.host }}
{{- end }}
{{- end }}
{{- define "gramps-web.valkeySecretName" -}}
{{- if .Values.valkey.enabled }}
{{- if .Values.valkey.auth.existingSecret }}{{ .Values.valkey.auth.existingSecret }}{{ else if .Values.valkey.fullnameOverride }}{{ .Values.valkey.fullnameOverride }}{{ else }}{{ printf "%s-valkey" .Release.Name }}{{ end }}
{{- else }}
{{- required "cache.auth.existingSecret is required when an external Valkey password is not supplied" .Values.cache.auth.existingSecret }}
{{- end }}
{{- end }}
{{- define "gramps-web.valkeySecretKey" -}}
{{- if .Values.valkey.enabled }}{{ default "valkey-password" .Values.valkey.auth.existingSecretPasswordKey }}{{ else }}{{ default "valkey-password" .Values.cache.auth.existingSecretKey }}{{ end }}
{{- end }}
+195
View File
@@ -0,0 +1,195 @@
{{- define "gramps-web.env" -}}
- name: TZ
value: {{ .Values.config.timezone | quote }}
- name: GRAMPSWEB_SECRET_KEY
valueFrom:
secretKeyRef:
name: {{ include "gramps-web.appSecretName" . }}
key: {{ ternary .Values.appSecret.existingSecretKey "secret-key" (ne .Values.appSecret.existingSecret "") }}
- name: GRAMPSWEB_TREE
value: {{ required "config.tree is required" .Values.config.tree | quote }}
- name: GRAMPSWEB_BASE_URL
value: {{ required "config.baseUrl is required" .Values.config.baseUrl | quote }}
- name: GRAMPSWEB_DB_BACKEND
value: {{ .Values.config.databaseBackend | quote }}
- name: GRAMPSWEB_POSTGRES_HOST
value: {{ include "gramps-web.postgresqlHost" . | quote }}
- name: GRAMPSWEB_POSTGRES_PORT
value: {{ .Values.database.port | quote }}
- name: GRAMPSWEB_POSTGRES_USER
value: {{ ternary .Values.postgresql.auth.username .Values.database.username .Values.postgresql.enabled | quote }}
- name: GRAMPSWEB_POSTGRES_PASSWORD
{{- if and (not .Values.postgresql.enabled) .Values.database.auth.password }}
value: {{ .Values.database.auth.password | quote }}
{{- else }}
valueFrom:
secretKeyRef:
name: {{ include "gramps-web.postgresqlSecretName" . }}
key: {{ include "gramps-web.postgresqlSecretKey" . }}
{{- end }}
- name: PROBE_POSTGRES_DATABASE
value: {{ ternary .Values.postgresql.auth.database .Values.database.database .Values.postgresql.enabled | quote }}
- name: PROBE_POSTGRES_ENABLED
value: {{ has .Values.config.databaseBackend (list "postgresql" "sharedpostgresql") | quote }}
- name: GRAMPSWEB_USER_DB_URI
{{- if .Values.config.userDatabaseUriSecret.existingSecret }}
valueFrom:
secretKeyRef:
name: {{ .Values.config.userDatabaseUriSecret.existingSecret }}
key: {{ .Values.config.userDatabaseUriSecret.key }}
{{- else }}
value: {{ .Values.config.userDatabaseUri | quote }}
{{- end }}
- name: GRAMPSWEB_SEARCH_INDEX_DB_URI
{{- if .Values.config.searchIndexDatabaseUriSecret.existingSecret }}
valueFrom:
secretKeyRef:
name: {{ .Values.config.searchIndexDatabaseUriSecret.existingSecret }}
key: {{ .Values.config.searchIndexDatabaseUriSecret.key }}
{{- else }}
value: {{ .Values.config.searchIndexDatabaseUri | quote }}
{{- end }}
- name: VALKEY_PASSWORD
{{- if and (not .Values.valkey.enabled) (not .Values.cache.auth.enabled) }}
value: ""
{{- else }}
{{- if and (not .Values.valkey.enabled) .Values.cache.auth.password }}
value: {{ .Values.cache.auth.password | quote }}
{{- else }}
valueFrom:
secretKeyRef:
name: {{ include "gramps-web.valkeySecretName" . }}
key: {{ include "gramps-web.valkeySecretKey" . }}
{{- end }}
{{- end }}
- name: GRAMPSWEB_CELERY_CONFIG__broker_url
value: {{ printf "%s%s:%v/%v" (ternary "redis://:$(VALKEY_PASSWORD)@" "redis://" (or .Values.valkey.enabled .Values.cache.auth.enabled)) (include "gramps-web.valkeyHost" .) .Values.cache.port .Values.cache.brokerDatabase | quote }}
- name: GRAMPSWEB_CELERY_CONFIG__result_backend
value: {{ printf "%s%s:%v/%v" (ternary "redis://:$(VALKEY_PASSWORD)@" "redis://" (or .Values.valkey.enabled .Values.cache.auth.enabled)) (include "gramps-web.valkeyHost" .) .Values.cache.port .Values.cache.resultDatabase | quote }}
- name: GRAMPSWEB_RATELIMIT_STORAGE_URI
value: {{ printf "%s%s:%v/%v" (ternary "redis://:$(VALKEY_PASSWORD)@" "redis://" (or .Values.valkey.enabled .Values.cache.auth.enabled)) (include "gramps-web.valkeyHost" .) .Values.cache.port .Values.cache.rateLimitDatabase | quote }}
{{- if .Values.oidc.enabled }}
- name: GRAMPSWEB_OIDC_ENABLED
value: "true"
- name: GRAMPSWEB_OIDC_ISSUER
value: {{ required "oidc.issuer is required when OIDC is enabled" .Values.oidc.issuer | quote }}
- name: GRAMPSWEB_OIDC_CLIENT_ID
value: {{ required "oidc.clientId is required when OIDC is enabled" .Values.oidc.clientId | quote }}
- name: GRAMPSWEB_OIDC_CLIENT_SECRET
{{- if .Values.oidc.clientSecret.existingSecret }}
valueFrom:
secretKeyRef:
name: {{ .Values.oidc.clientSecret.existingSecret }}
key: {{ .Values.oidc.clientSecret.existingSecretKey }}
{{- else }}
value: {{ required "oidc.clientSecret.existingSecret or oidc.clientSecret.value is required when OIDC is enabled" .Values.oidc.clientSecret.value | quote }}
{{- end }}
- name: GRAMPSWEB_OIDC_NAME
value: {{ .Values.oidc.name | quote }}
- name: GRAMPSWEB_OIDC_SCOPES
value: {{ .Values.oidc.scopes | quote }}
- name: GRAMPSWEB_OIDC_USERNAME_CLAIM
value: {{ .Values.oidc.usernameClaim | quote }}
- name: GRAMPSWEB_OIDC_DISABLE_LOCAL_AUTH
value: {{ .Values.oidc.disableLocalAuth | quote }}
- name: GRAMPSWEB_OIDC_AUTO_REDIRECT
value: {{ .Values.oidc.autoRedirect | quote }}
- name: GRAMPSWEB_OIDC_ROLE_CLAIM
value: {{ .Values.oidc.roleClaim | quote }}
{{- with .Values.oidc.openidConfigUrl }}
- name: GRAMPSWEB_OIDC_OPENID_CONFIG_URL
value: {{ . | quote }}
{{- end }}
{{- range $role, $group := .Values.oidc.groups }}
{{- if $group }}
- name: {{ printf "GRAMPSWEB_OIDC_GROUP_%s" ($role | upper) }}
value: {{ $group | quote }}
{{- end }}
{{- end }}
{{- else }}
- name: GRAMPSWEB_OIDC_ENABLED
value: "false"
{{- end }}
{{- if .Values.s3.enabled }}
- name: GRAMPSWEB_MEDIA_BASE_DIR
value: {{ required "s3.mediaBaseDir is required when S3 is enabled" .Values.s3.mediaBaseDir | quote }}
- name: AWS_DEFAULT_REGION
value: {{ required "s3.region is required when S3 is enabled" .Values.s3.region | quote }}
{{- with .Values.s3.endpointUrl }}
- name: AWS_ENDPOINT_URL
value: {{ . | quote }}
{{- end }}
- name: AWS_ACCESS_KEY_ID
{{- if .Values.s3.auth.existingSecret }}
valueFrom:
secretKeyRef:
name: {{ .Values.s3.auth.existingSecret }}
key: {{ .Values.s3.auth.accessKeyIdKey }}
{{- else }}
value: {{ required "s3.auth.existingSecret or s3.auth.accessKeyId is required when S3 is enabled" .Values.s3.auth.accessKeyId | quote }}
{{- end }}
- name: AWS_SECRET_ACCESS_KEY
{{- if .Values.s3.auth.existingSecret }}
valueFrom:
secretKeyRef:
name: {{ .Values.s3.auth.existingSecret }}
key: {{ .Values.s3.auth.secretAccessKeyKey }}
{{- else }}
value: {{ required "s3.auth.existingSecret or s3.auth.secretAccessKey is required when S3 is enabled" .Values.s3.auth.secretAccessKey | quote }}
{{- end }}
{{- end }}
{{- if .Values.email.enabled }}
- name: GRAMPSWEB_EMAIL_HOST
value: {{ required "email.host is required when email is enabled" .Values.email.host | quote }}
- name: GRAMPSWEB_EMAIL_PORT
value: {{ .Values.email.port | quote }}
- name: GRAMPSWEB_EMAIL_USE_TLS
value: {{ .Values.email.useTls | quote }}
- name: GRAMPSWEB_DEFAULT_FROM_EMAIL
value: {{ .Values.email.defaultFromEmail | quote }}
{{- with .Values.email.username }}
- name: GRAMPSWEB_EMAIL_HOST_USER
value: {{ . | quote }}
{{- end }}
{{- if or .Values.email.auth.existingSecret .Values.email.auth.password }}
- name: GRAMPSWEB_EMAIL_HOST_PASSWORD
{{- if .Values.email.auth.existingSecret }}
valueFrom:
secretKeyRef:
name: {{ .Values.email.auth.existingSecret }}
key: {{ .Values.email.auth.passwordKey }}
{{- else }}
value: {{ .Values.email.auth.password | quote }}
{{- end }}
{{- end }}
{{- end }}
{{- with .Values.config.extraEnv }}
{{ toYaml . }}
{{- end }}
{{- end }}
{{- define "gramps-web.volumeMounts" -}}
- name: data
mountPath: /app/users
subPath: users
- name: data
mountPath: /app/indexdir
subPath: indexdir
- name: data
mountPath: /app/thumbnail_cache
subPath: thumbnail_cache
- name: data
mountPath: /app/cache
subPath: cache
- name: data
mountPath: /app/secret
subPath: secret
- name: data
mountPath: /root/.gramps/grampsdb
subPath: db
- name: data
mountPath: /app/media
subPath: app/media
- name: tmp
mountPath: /tmp
{{- end }}
+114
View File
@@ -0,0 +1,114 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ include "gramps-web.fullname" . }}
labels:
{{- include "gramps-web.labels" . | nindent 4 }}
spec:
replicas: {{ .Values.replicaCount }}
selector:
matchLabels:
app: {{ include "gramps-web.fullname" . }}
template:
metadata:
labels:
app: {{ include "gramps-web.fullname" . }}
{{- include "gramps-web.labels" . | nindent 8 }}
{{- with .Values.podLabels }}
{{- toYaml . | nindent 8 }}
{{- end }}
annotations:
checksum/app-secret: {{ include (print $.Template.BasePath "/secret.yaml") . | sha256sum }}
{{- with .Values.podAnnotations }}
{{- toYaml . | nindent 8 }}
{{- end }}
spec:
serviceAccountName: {{ include "gramps-web.serviceAccountName" . }}
automountServiceAccountToken: {{ .Values.serviceAccount.automountServiceAccountToken }}
{{- with .Values.image.pullSecrets }}
imagePullSecrets:
{{- toYaml . | nindent 8 }}
{{- end }}
securityContext:
{{- toYaml .Values.podSecurityContext | nindent 8 }}
containers:
- name: gramps-web
image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"
imagePullPolicy: {{ .Values.image.pullPolicy }}
securityContext:
{{- toYaml .Values.securityContext | nindent 12 }}
env:
{{- include "gramps-web.env" . | nindent 12 }}
ports:
- name: http
containerPort: 5000
protocol: TCP
{{- if .Values.livenessProbe.enabled }}
livenessProbe:
{{- omit .Values.livenessProbe "enabled" | toYaml | nindent 12 }}
{{- end }}
{{- if .Values.readinessProbe.enabled }}
readinessProbe:
{{- if .Values.readinessProbe.deep }}
exec:
command:
- python3
- -c
- |
import os
import urllib.request
import psycopg2
import redis
with urllib.request.urlopen("http://127.0.0.1:5000/ready", timeout=3) as response:
assert response.status == 200
if os.environ["PROBE_POSTGRES_ENABLED"] == "true":
connection = psycopg2.connect(
host=os.environ["GRAMPSWEB_POSTGRES_HOST"],
port=os.environ["GRAMPSWEB_POSTGRES_PORT"],
user=os.environ["GRAMPSWEB_POSTGRES_USER"],
password=os.environ["GRAMPSWEB_POSTGRES_PASSWORD"],
dbname=os.environ["PROBE_POSTGRES_DATABASE"],
connect_timeout=3,
)
with connection.cursor() as cursor:
cursor.execute("SELECT 1")
assert cursor.fetchone() == (1,)
connection.close()
assert redis.Redis.from_url(
os.environ["GRAMPSWEB_RATELIMIT_STORAGE_URI"],
socket_connect_timeout=3,
socket_timeout=3,
).ping()
{{- end }}
{{- omit .Values.readinessProbe "enabled" "deep" | toYaml | nindent 12 }}
{{- end }}
{{- if .Values.startupProbe.enabled }}
startupProbe:
{{- omit .Values.startupProbe "enabled" | toYaml | nindent 12 }}
{{- end }}
resources:
{{- toYaml .Values.resources | nindent 12 }}
volumeMounts:
{{- include "gramps-web.volumeMounts" . | nindent 12 }}
volumes:
- name: data
{{- if .Values.persistence.enabled }}
persistentVolumeClaim:
claimName: {{ default (printf "%s-data" (include "gramps-web.fullname" .)) .Values.persistence.existingClaim }}
{{- else }}
emptyDir: {}
{{- end }}
- name: tmp
emptyDir: {}
{{- with .Values.nodeSelector }}
nodeSelector:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.affinity }}
affinity:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.tolerations }}
tolerations:
{{- toYaml . | nindent 8 }}
{{- end }}
+36
View File
@@ -0,0 +1,36 @@
{{- if .Values.ingress.enabled }}
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: {{ default (include "gramps-web.fullname" .) .Values.ingress.nameOverride }}
labels:
{{- include "gramps-web.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 "gramps-web.fullname" $ }}
port:
name: http
{{- end }}
{{- end }}
{{- end }}
+22
View File
@@ -0,0 +1,22 @@
{{- if and .Values.persistence.enabled (not .Values.persistence.existingClaim) }}
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: {{ include "gramps-web.fullname" . }}-data
labels:
{{- include "gramps-web.labels" . | nindent 4 }}
{{- with .Values.persistence.annotations }}
annotations:
{{- toYaml . | nindent 4 }}
{{- end }}
spec:
accessModes:
{{- toYaml .Values.persistence.accessModes | nindent 4 }}
{{- with .Values.persistence.storageClass }}
storageClassName: {{ . | quote }}
{{- end }}
resources:
requests:
storage: {{ .Values.persistence.size }}
{{- end }}
+22
View File
@@ -0,0 +1,22 @@
{{- if not .Values.appSecret.existingSecret }}
{{- $name := include "gramps-web.fullname" . -}}
{{- $secret := lookup "v1" "Secret" .Release.Namespace $name -}}
{{- $secretKey := "" -}}
{{- if and $secret $secret.data (hasKey $secret.data "secret-key") -}}
{{- $secretKey = index $secret.data "secret-key" | b64dec -}}
{{- else if .Values.appSecret.value -}}
{{- $secretKey = .Values.appSecret.value -}}
{{- else -}}
{{- $secretKey = randAlphaNum 64 -}}
{{- end }}
apiVersion: v1
kind: Secret
metadata:
name: {{ $name }}
labels:
{{- include "gramps-web.labels" . | nindent 4 }}
type: Opaque
stringData:
secret-key: {{ $secretKey | quote }}
{{- end }}
+16
View File
@@ -0,0 +1,16 @@
apiVersion: v1
kind: Service
metadata:
name: {{ include "gramps-web.fullname" . }}
labels:
{{- include "gramps-web.labels" . | nindent 4 }}
spec:
type: {{ .Values.service.type }}
ports:
- name: http
port: {{ .Values.service.port }}
targetPort: http
protocol: TCP
selector:
app: {{ include "gramps-web.fullname" . }}
+14
View File
@@ -0,0 +1,14 @@
{{- if .Values.serviceAccount.create }}
apiVersion: v1
kind: ServiceAccount
metadata:
name: {{ include "gramps-web.serviceAccountName" . }}
labels:
{{- include "gramps-web.labels" . | nindent 4 }}
{{- with .Values.serviceAccount.annotations }}
annotations:
{{- toYaml . | nindent 4 }}
{{- end }}
automountServiceAccountToken: {{ .Values.serviceAccount.automountServiceAccountToken }}
{{- end }}
+90
View File
@@ -0,0 +1,90 @@
{{- if .Values.worker.enabled }}
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ include "gramps-web.workerName" . }}
labels:
{{- include "gramps-web.labels" . | nindent 4 }}
app.kubernetes.io/component: worker
spec:
replicas: {{ .Values.worker.replicaCount }}
selector:
matchLabels:
app: {{ include "gramps-web.workerName" . }}
template:
metadata:
labels:
app: {{ include "gramps-web.workerName" . }}
{{- include "gramps-web.labels" . | nindent 8 }}
app.kubernetes.io/component: worker
{{- with .Values.podLabels }}
{{- toYaml . | nindent 8 }}
{{- end }}
annotations:
checksum/app-secret: {{ include (print $.Template.BasePath "/secret.yaml") . | sha256sum }}
{{- with .Values.podAnnotations }}
{{- toYaml . | nindent 8 }}
{{- end }}
spec:
serviceAccountName: {{ include "gramps-web.serviceAccountName" . }}
automountServiceAccountToken: {{ .Values.serviceAccount.automountServiceAccountToken }}
{{- with .Values.image.pullSecrets }}
imagePullSecrets:
{{- toYaml . | nindent 8 }}
{{- end }}
securityContext:
{{- toYaml .Values.podSecurityContext | nindent 8 }}
containers:
- name: worker
image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"
imagePullPolicy: {{ .Values.image.pullPolicy }}
command: [celery]
args:
- -A
- gramps_webapi.celery
- worker
- --loglevel=INFO
- --concurrency={{ .Values.worker.concurrency }}
securityContext:
{{- toYaml .Values.securityContext | nindent 12 }}
env:
{{- include "gramps-web.env" . | nindent 12 }}
{{- if .Values.worker.startupProbe.enabled }}
startupProbe:
{{- omit .Values.worker.startupProbe "enabled" | toYaml | nindent 12 }}
{{- end }}
{{- if .Values.worker.readinessProbe.enabled }}
readinessProbe:
{{- omit .Values.worker.readinessProbe "enabled" | toYaml | nindent 12 }}
{{- end }}
{{- if .Values.worker.livenessProbe.enabled }}
livenessProbe:
{{- omit .Values.worker.livenessProbe "enabled" | toYaml | nindent 12 }}
{{- end }}
resources:
{{- toYaml .Values.worker.resources | nindent 12 }}
volumeMounts:
{{- include "gramps-web.volumeMounts" . | nindent 12 }}
volumes:
- name: data
{{- if .Values.persistence.enabled }}
persistentVolumeClaim:
claimName: {{ default (printf "%s-data" (include "gramps-web.fullname" .)) .Values.persistence.existingClaim }}
{{- else }}
emptyDir: {}
{{- end }}
- name: tmp
emptyDir: {}
{{- with .Values.nodeSelector }}
nodeSelector:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.affinity }}
affinity:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.tolerations }}
tolerations:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- end }}
Executable
+61
View File
@@ -0,0 +1,61 @@
#!/usr/bin/env bash
set -euo pipefail
project_dir=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)
workdir=$(mktemp --directory)
trap 'rm -rf "${workdir}"' EXIT HUP INT TERM
bash -n \
"${project_dir}/bootstrap-secrets.sh" \
"${project_dir}/create-ci-kubeconfig.sh" \
"${project_dir}/deploy.sh" \
"${project_dir}/install.sh" \
"${project_dir}/preflight.sh" \
"${project_dir}/render.sh" \
"${project_dir}/test.sh"
dependencies=("${project_dir}"/charts/*.tgz)
if (( ${#dependencies[@]} < 2 )); then
helm dependency build --skip-refresh "${project_dir}"
fi
helm lint "${project_dir}"
helm lint "${project_dir}" --values "${project_dir}/values-arschrock.yaml"
helm template gramps-web "${project_dir}" --namespace gramps-web \
> "${workdir}/default.yaml"
helm template gramps "${project_dir}" --namespace gramps \
--values "${project_dir}/values-arschrock.yaml" \
> "${workdir}/arschrock.yaml"
helm template external "${project_dir}" --namespace gramps \
--set postgresql.enabled=false \
--set database.host=postgres.example.internal \
--set database.auth.existingSecret=external-postgresql \
--set valkey.enabled=false \
--set cache.host=valkey.example.internal \
--set cache.auth.existingSecret=external-valkey \
> "${workdir}/external.yaml"
grep -Fq 'ghcr.io/gramps-project/grampsweb:latest' "${workdir}/default.yaml"
grep -Fq 'name: GRAMPSWEB_OIDC_ENABLED' "${workdir}/default.yaml"
grep -Fq 'name: postgresql' "${workdir}/arschrock.yaml"
grep -Fq 'name: valkey-primary' "${workdir}/arschrock.yaml"
grep -Fq 'name: grampsweb-celery' "${workdir}/arschrock.yaml"
grep -Fq 'name: gramps-runtime' "${workdir}/arschrock.yaml"
grep -Fq 'path: /ready' "${workdir}/default.yaml"
grep -Fq 'psycopg2.connect' "${workdir}/default.yaml"
grep -Fq 'celery -A gramps_webapi.celery inspect ping' "${workdir}/default.yaml"
if grep -Eq '^kind: StatefulSet$' "${workdir}/external.yaml"; then
echo "External-service render unexpectedly creates a StatefulSet" >&2
exit 1
fi
# Block the credential patterns that existed in the legacy raw manifests.
if rg --hidden --glob '!charts/**' --glob '!postgresql/**' --glob '!valkey/**' \
--glob '!test.sh' \
'(GRAMPSWEB_POSTGRES_PASSWORD:[[:space:]]+[^{$]|OIDC_CLIENT_SECRET:[[:space:]]+[^{$]|AWS_SECRET_ACCESS_KEY:[[:space:]]+[^{$]|password:[[:space:]]+[A-Za-z0-9]{24,})' \
"${project_dir}"; then
echo "A likely plaintext credential is present in committable chart sources" >&2
exit 1
fi
echo "Chart renders successfully with bundled and external data services."
+137
View File
@@ -0,0 +1,137 @@
# Cluster-specific, non-secret desired state. Secret values live only in the
# Kubernetes Secret named gramps-runtime (created by bootstrap-secrets.sh).
fullnameOverride: grampsweb
image:
repository: ghcr.io/gramps-project/grampsweb
tag: latest
pullPolicy: Always
serviceAccount:
create: true
name: grampsweb
config:
tree: brunner.ninja Stammbaum
baseUrl: https://stammbaum.brunner.ninja
timezone: Europe/Vienna
databaseBackend: postgresql
userDatabaseUri: ""
userDatabaseUriSecret:
existingSecret: gramps-runtime
key: user-database-uri
searchIndexDatabaseUri: ""
searchIndexDatabaseUriSecret:
existingSecret: gramps-runtime
key: search-index-database-uri
appSecret:
existingSecret: gramps-runtime
existingSecretKey: app-secret-key
persistence:
existingClaim: grampsweb-data
worker:
nameOverride: grampsweb-celery
ingress:
enabled: true
nameOverride: stammbaum-brunner-ninja
className: traefik
annotations:
traefik.ingress.kubernetes.io/router.entrypoints: websecure
hosts:
- host: stammbaum.brunner.ninja
paths:
- path: /
pathType: Prefix
oidc:
enabled: true
issuer: https://auth.brunner.ninja/application/o/gramps/
clientId: VipNqgic7tcwE15c5Qdn2RF1BDjzsAWtJv5ZOgGS
clientSecret:
existingSecret: gramps-runtime
existingSecretKey: oidc-client-secret
name: brunner.ninja Auth
openidConfigUrl: https://auth.brunner.ninja/application/o/gramps/.well-known/openid-configuration
disableLocalAuth: true
autoRedirect: true
groups:
admin: Gramps Admins
editor: Gramps Editors
s3:
enabled: true
mediaBaseDir: s3://gramps
region: keller
endpointUrl: http://rook-ceph-rgw-my-store.rook-ceph.svc.cluster.local
auth:
existingSecret: gramps-runtime
accessKeyIdKey: aws-access-key-id
secretAccessKeyKey: aws-secret-access-key
email:
enabled: true
host: smtp-relay.gmail.com
port: 587
useTls: false
defaultFromEmail: stammbaum@brunner.ninja
global:
defaultStorageClass: rook-ceph-block
security:
# Required because this cluster uses its locale-enabled PostgreSQL build.
allowInsecureImages: true
postgresql:
enabled: true
fullnameOverride: postgresql
commonLabels:
app.kubernetes.io/instance: postgresql
image:
registry: registry.brunner.ninja
repository: feedc0de/postgres-with-locales
tag: 18-locales
pullPolicy: Always
pullSecrets:
- quay-pull-secret
auth:
username: gramps
database: gramps
existingSecret: gramps-postgresql
secretKeys:
adminPasswordKey: postgres-password
userPasswordKey: password
primary:
persistence:
enabled: true
size: 8Gi
valkey:
enabled: true
fullnameOverride: valkey
commonLabels:
app.kubernetes.io/instance: valkey
architecture: replication
auth:
enabled: true
existingSecret: gramps-valkey
existingSecretPasswordKey: valkey-password
primary:
persistence:
enabled: true
size: 4Gi
replica:
replicaCount: 3
persistence:
enabled: true
size: 4Gi
resources:
requests:
cpu: 25m
memory: 64Mi
limits:
cpu: 500m
memory: 512Mi
+54
View File
@@ -0,0 +1,54 @@
{
"$schema": "https://json-schema.org/draft-07/schema#",
"type": "object",
"properties": {
"replicaCount": { "type": "integer", "minimum": 1 },
"image": {
"type": "object",
"required": ["repository", "tag", "pullPolicy"],
"properties": {
"repository": { "type": "string", "minLength": 1 },
"tag": { "type": "string", "minLength": 1 },
"pullPolicy": { "enum": ["Always", "IfNotPresent", "Never"] }
}
},
"config": {
"type": "object",
"required": ["tree", "baseUrl", "databaseBackend"],
"properties": {
"tree": { "type": "string", "minLength": 1 },
"baseUrl": { "type": "string", "minLength": 1 },
"databaseBackend": { "type": "string", "minLength": 1 }
}
},
"postgresql": {
"type": "object",
"required": ["enabled"],
"properties": { "enabled": { "type": "boolean" } }
},
"valkey": {
"type": "object",
"required": ["enabled"],
"properties": { "enabled": { "type": "boolean" } }
},
"oidc": {
"type": "object",
"required": ["enabled"],
"properties": { "enabled": { "type": "boolean" } }
},
"s3": {
"type": "object",
"required": ["enabled"],
"properties": { "enabled": { "type": "boolean" } }
},
"persistence": {
"type": "object",
"required": ["enabled", "size"],
"properties": {
"enabled": { "type": "boolean" },
"size": { "type": "string", "minLength": 1 }
}
}
}
}
+268
View File
@@ -0,0 +1,268 @@
replicaCount: 1
image:
repository: ghcr.io/gramps-project/grampsweb
tag: latest
pullPolicy: Always
pullSecrets: []
nameOverride: ""
fullnameOverride: ""
serviceAccount:
create: true
name: ""
annotations: {}
automountServiceAccountToken: false
podAnnotations: {}
podLabels: {}
podSecurityContext: {}
securityContext: {}
service:
type: ClusterIP
port: 80
ingress:
enabled: false
nameOverride: ""
className: ""
annotations: {}
hosts:
- host: gramps.example.com
paths:
- path: /
pathType: Prefix
tls: []
config:
tree: "Family Tree"
baseUrl: http://localhost:5000
timezone: UTC
databaseBackend: postgresql
userDatabaseUri: sqlite:////app/users/users.sqlite
userDatabaseUriSecret:
existingSecret: ""
key: user-database-uri
searchIndexDatabaseUri: sqlite:////app/indexdir/search_index.db
searchIndexDatabaseUriSecret:
existingSecret: ""
key: search-index-database-uri
extraEnv: []
appSecret:
# The chart creates and preserves a random secret when existingSecret is empty.
existingSecret: ""
existingSecretKey: secret-key
value: ""
database:
# Used only when postgresql.enabled=false. Bundled credentials are configured
# below under postgresql.auth.
host: ""
port: 5432
username: gramps
database: gramps
auth:
existingSecret: ""
existingSecretKey: password
password: ""
cache:
# Used only when valkey.enabled=false. Bundled credentials are configured
# below under valkey.auth.
host: ""
port: 6379
brokerDatabase: 0
resultDatabase: 1
rateLimitDatabase: 2
auth:
enabled: true
existingSecret: ""
existingSecretKey: valkey-password
password: ""
oidc:
enabled: false
issuer: ""
clientId: ""
clientSecret:
existingSecret: ""
existingSecretKey: oidc-client-secret
value: ""
name: OIDC
scopes: openid email profile
usernameClaim: preferred_username
openidConfigUrl: ""
disableLocalAuth: false
autoRedirect: false
roleClaim: groups
groups:
admin: ""
owner: ""
editor: ""
contributor: ""
member: ""
guest: ""
s3:
enabled: false
mediaBaseDir: ""
region: ""
endpointUrl: ""
auth:
existingSecret: ""
accessKeyIdKey: aws-access-key-id
secretAccessKeyKey: aws-secret-access-key
accessKeyId: ""
secretAccessKey: ""
email:
enabled: false
host: ""
port: 587
useTls: true
defaultFromEmail: ""
username: ""
auth:
existingSecret: ""
passwordKey: smtp-password
password: ""
persistence:
enabled: true
existingClaim: ""
storageClass: ""
accessModes:
- ReadWriteMany
size: 10Gi
annotations:
helm.sh/resource-policy: keep
worker:
enabled: true
replicaCount: 1
nameOverride: ""
concurrency: 2
startupProbe:
enabled: true
exec:
command:
- sh
- -ec
- celery -A gramps_webapi.celery inspect ping --destination "celery@${HOSTNAME}" --timeout 5
periodSeconds: 10
timeoutSeconds: 10
failureThreshold: 30
readinessProbe:
enabled: true
exec:
command:
- sh
- -ec
- celery -A gramps_webapi.celery inspect ping --destination "celery@${HOSTNAME}" --timeout 5
periodSeconds: 15
timeoutSeconds: 10
failureThreshold: 2
livenessProbe:
enabled: true
exec:
command:
- sh
- -ec
- celery -A gramps_webapi.celery inspect ping --destination "celery@${HOSTNAME}" --timeout 5
periodSeconds: 30
timeoutSeconds: 10
failureThreshold: 6
resources:
requests:
cpu: 25m
memory: 256Mi
limits:
cpu: "2"
memory: 2Gi
resources:
requests:
cpu: 25m
memory: 256Mi
limits:
cpu: "1"
memory: 1Gi
livenessProbe:
enabled: true
httpGet:
path: /ready
port: http
initialDelaySeconds: 15
timeoutSeconds: 5
readinessProbe:
enabled: true
# Also verifies PostgreSQL authentication/querying and a Valkey PING.
deep: true
initialDelaySeconds: 5
periodSeconds: 15
timeoutSeconds: 10
failureThreshold: 2
startupProbe:
enabled: true
httpGet:
path: /ready
port: http
failureThreshold: 30
periodSeconds: 5
nodeSelector: {}
tolerations: []
affinity: {}
postgresql:
enabled: true
auth:
username: gramps
database: gramps
password: ""
existingSecret: ""
secretKeys:
adminPasswordKey: postgres-password
userPasswordKey: password
primary:
startupProbe:
enabled: true
timeoutSeconds: 5
failureThreshold: 30
persistence:
enabled: true
size: 8Gi
resources:
requests:
cpu: 50m
memory: 256Mi
limits:
cpu: "1"
memory: 2Gi
valkey:
enabled: true
architecture: standalone
auth:
enabled: true
password: ""
existingSecret: ""
existingSecretPasswordKey: valkey-password
primary:
startupProbe:
enabled: true
failureThreshold: 30
persistence:
enabled: true
size: 1Gi
resources:
requests:
cpu: 25m
memory: 64Mi
limits:
cpu: 500m
memory: 512Mi