diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml new file mode 100644 index 0000000..5eff0a1 --- /dev/null +++ b/.gitea/workflows/ci.yml @@ -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 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b6064f1 --- /dev/null +++ b/.gitignore @@ -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 + diff --git a/.helmignore b/.helmignore new file mode 100644 index 0000000..c2b6506 --- /dev/null +++ b/.helmignore @@ -0,0 +1,7 @@ +.git/ +.gitea/ +.idea/ +.vscode/ +postgresql/ +valkey/ +*.tmp diff --git a/Chart.lock b/Chart.lock new file mode 100644 index 0000000..c307032 --- /dev/null +++ b/Chart.lock @@ -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" diff --git a/Chart.yaml b/Chart.yaml new file mode 100644 index 0000000..1d4f79f --- /dev/null +++ b/Chart.yaml @@ -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 + diff --git a/MIGRATION.md b/MIGRATION.md new file mode 100644 index 0000000..5bb999e --- /dev/null +++ b/MIGRATION.md @@ -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. diff --git a/README.md b/README.md index edea3b2..a66ce14 100644 --- a/README.md +++ b/README.md @@ -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) diff --git a/backup.sh b/backup.sh new file mode 100755 index 0000000..2a9e72f --- /dev/null +++ b/backup.sh @@ -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)." + diff --git a/bootstrap-secrets.sh b/bootstrap-secrets.sh new file mode 100755 index 0000000..4fb19cc --- /dev/null +++ b/bootstrap-secrets.sh @@ -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." diff --git a/ci-deployer.yaml b/ci-deployer.yaml new file mode 100644 index 0000000..12bbced --- /dev/null +++ b/ci-deployer.yaml @@ -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 diff --git a/create-ci-kubeconfig.sh b/create-ci-kubeconfig.sh new file mode 100755 index 0000000..ff7e65b --- /dev/null +++ b/create-ci-kubeconfig.sh @@ -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' + diff --git a/deploy.sh b/deploy.sh new file mode 100755 index 0000000..6cc3abc --- /dev/null +++ b/deploy.sh @@ -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[@]}" diff --git a/install.sh b/install.sh new file mode 100755 index 0000000..4ffa18a --- /dev/null +++ b/install.sh @@ -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 diff --git a/postgresql-image/Dockerfile b/postgresql-image/Dockerfile new file mode 100644 index 0000000..df96579 --- /dev/null +++ b/postgresql-image/Dockerfile @@ -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 diff --git a/preflight.sh b/preflight.sh new file mode 100755 index 0000000..dafa489 --- /dev/null +++ b/preflight.sh @@ -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." diff --git a/render.sh b/render.sh new file mode 100755 index 0000000..d06b05b --- /dev/null +++ b/render.sh @@ -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}" diff --git a/templates/NOTES.txt b/templates/NOTES.txt new file mode 100644 index 0000000..d43c3fe --- /dev/null +++ b/templates/NOTES.txt @@ -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 }} + diff --git a/templates/_helpers.tpl b/templates/_helpers.tpl new file mode 100644 index 0000000..ed9cdb5 --- /dev/null +++ b/templates/_helpers.tpl @@ -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 }} + diff --git a/templates/_pod.tpl b/templates/_pod.tpl new file mode 100644 index 0000000..dd3a1a9 --- /dev/null +++ b/templates/_pod.tpl @@ -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 }} diff --git a/templates/deployment.yaml b/templates/deployment.yaml new file mode 100644 index 0000000..450e93f --- /dev/null +++ b/templates/deployment.yaml @@ -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 }} diff --git a/templates/ingress.yaml b/templates/ingress.yaml new file mode 100644 index 0000000..b43b002 --- /dev/null +++ b/templates/ingress.yaml @@ -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 }} + diff --git a/templates/pvc.yaml b/templates/pvc.yaml new file mode 100644 index 0000000..fd4fe74 --- /dev/null +++ b/templates/pvc.yaml @@ -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 }} + diff --git a/templates/secret.yaml b/templates/secret.yaml new file mode 100644 index 0000000..e293cbf --- /dev/null +++ b/templates/secret.yaml @@ -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 }} + diff --git a/templates/service.yaml b/templates/service.yaml new file mode 100644 index 0000000..878d719 --- /dev/null +++ b/templates/service.yaml @@ -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" . }} + diff --git a/templates/serviceaccount.yaml b/templates/serviceaccount.yaml new file mode 100644 index 0000000..3dc5bfa --- /dev/null +++ b/templates/serviceaccount.yaml @@ -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 }} + diff --git a/templates/worker-deployment.yaml b/templates/worker-deployment.yaml new file mode 100644 index 0000000..3e766ee --- /dev/null +++ b/templates/worker-deployment.yaml @@ -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 }} diff --git a/test.sh b/test.sh new file mode 100755 index 0000000..3070efc --- /dev/null +++ b/test.sh @@ -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." diff --git a/values-arschrock.yaml b/values-arschrock.yaml new file mode 100644 index 0000000..135118b --- /dev/null +++ b/values-arschrock.yaml @@ -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 diff --git a/values.schema.json b/values.schema.json new file mode 100644 index 0000000..091a16d --- /dev/null +++ b/values.schema.json @@ -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 } + } + } + } +} + diff --git a/values.yaml b/values.yaml new file mode 100644 index 0000000..fccaf29 --- /dev/null +++ b/values.yaml @@ -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