From f2fb39ca62df0e3e993937dd32a692b5fd5b32de Mon Sep 17 00:00:00 2001 From: 0xFEEDC0DE64 Date: Mon, 19 Oct 2026 08:43:30 +0200 Subject: [PATCH] Add Argo CD Helm chart release --- .gitea/workflows/deploy.yml | 187 ------------------------------ .gitea/workflows/publish.yml | 163 +++++++++++++++++++++++++++ README.md | 194 ++++++-------------------------- chart/Chart.yaml | 6 + chart/README.md | 18 +++ chart/install.sh | 16 +++ chart/templates/configmap.yaml | 40 +++++++ chart/templates/deployment.yaml | 73 ++++++++++++ chart/templates/ingress.yaml | 24 ++++ chart/templates/middleware.yaml | 13 +++ chart/templates/service.yaml | 16 +++ chart/test.sh | 18 +++ chart/values.schema.json | 34 ++++++ chart/values.yaml | 15 +++ install.sh | 15 +-- test.sh | 24 +--- 16 files changed, 471 insertions(+), 385 deletions(-) delete mode 100644 .gitea/workflows/deploy.yml create mode 100644 .gitea/workflows/publish.yml create mode 100644 chart/Chart.yaml create mode 100644 chart/README.md create mode 100755 chart/install.sh create mode 100644 chart/templates/configmap.yaml create mode 100644 chart/templates/deployment.yaml create mode 100644 chart/templates/ingress.yaml create mode 100644 chart/templates/middleware.yaml create mode 100644 chart/templates/service.yaml create mode 100755 chart/test.sh create mode 100644 chart/values.schema.json create mode 100644 chart/values.yaml diff --git a/.gitea/workflows/deploy.yml b/.gitea/workflows/deploy.yml deleted file mode 100644 index 825426f..0000000 --- a/.gitea/workflows/deploy.yml +++ /dev/null @@ -1,187 +0,0 @@ -name: Validate and deploy phpPgAdmin - -on: - push: - pull_request: - -env: - IMAGE: registry.brunner.ninja/feedc0de/phppgadmin - KUBECONFORM_VERSION: v0.7.0 - KUBECTL_VERSION: v1.36.3 - KUBERNETES_API: https://host.containers.internal:6443 - KUBERNETES_TLS_SERVER_NAME: 192.168.0.2 - NAMESPACE: default - -jobs: - image: - name: Build, test and publish image - if: gitea.event_name == 'push' && gitea.ref == 'refs/heads/main' - runs-on: linux_amd64 - timeout-minutes: 30 - steps: - - name: Check out source - uses: actions/checkout@v4 - - - name: Configure isolated registry access - env: - QUAY_USERNAME: ${{ secrets.QUAY_USERNAME }} - QUAY_TOKEN: ${{ secrets.QUAY_TOKEN }} - run: | - set -euo pipefail - authfile="${RUNNER_TEMP}/quay-auth.json" - printf '{"auths":{}}' > "$authfile" - chmod 0600 "$authfile" - printf '%s' "$QUAY_TOKEN" | podman login registry.brunner.ninja \ - --authfile "$authfile" --username "$QUAY_USERNAME" --password-stdin - echo "REGISTRY_AUTH_FILE=$authfile" >> "$GITHUB_ENV" - - - name: Build image - env: - COMMIT_SHA: ${{ gitea.sha }} - run: | - set -euo pipefail - podman build \ - --pull \ - --label "org.opencontainers.image.base.name=registry.brunner.ninja/feedc0de/php-apache:2.4.69-8.5.11" \ - --label "org.opencontainers.image.revision=${COMMIT_SHA}" \ - --label "org.opencontainers.image.source=https://code.brunner.ninja/feedc0de/phppgadmin-kubernetes" \ - --tag "${IMAGE}:${COMMIT_SHA}" \ - . - - - name: Smoke-test image - env: - COMMIT_SHA: ${{ gitea.sha }} - run: | - set -euo pipefail - podman run --rm --entrypoint httpd "${IMAGE}:${COMMIT_SHA}" -t - podman run --rm --entrypoint php "${IMAGE}:${COMMIT_SHA}" -r ' - foreach (["pgsql", "pdo_pgsql", "mbstring", "Zend OPcache"] as $extension) { - if (!extension_loaded($extension)) { - fwrite(STDERR, "Missing PHP extension: {$extension}\n"); - exit(1); - } - } - ' - container_id="$(podman run --detach --publish 127.0.0.1::80 "${IMAGE}:${COMMIT_SHA}")" - trap 'podman rm --force "${container_id}" >/dev/null 2>&1 || true' EXIT - port="$(podman inspect --format '{{ (index (index .NetworkSettings.Ports "80/tcp") 0).HostPort }}' "${container_id}")" - for attempt in {1..30}; do - if curl --fail --silent --show-error \ - --output "${RUNNER_TEMP}/phppgadmin.html" \ - "http://127.0.0.1:${port}/phppgadmin/" \ - && grep -Fq 'phpPgAdmin' "${RUNNER_TEMP}/phppgadmin.html"; then - exit 0 - fi - sleep 1 - done - podman logs "${container_id}" - exit 1 - - - name: Push image - if: gitea.ref == 'refs/heads/main' - env: - COMMIT_SHA: ${{ gitea.sha }} - run: | - set -euo pipefail - podman push "${IMAGE}:${COMMIT_SHA}" - podman tag "${IMAGE}:${COMMIT_SHA}" "${IMAGE}:latest" - podman push "${IMAGE}:latest" - - - name: Remove registry credentials - if: always() - run: rm -f "${RUNNER_TEMP}/quay-auth.json" - - validate: - name: Validate manifests - runs-on: ubuntu-latest - steps: - - name: Check out source - uses: actions/checkout@v4 - - - name: Validate scripts and Kubernetes resources - run: | - sh -n install.sh - bash -n create-ci-kubeconfig.sh - bash -n test.sh - docker run --rm --interactive \ - "ghcr.io/yannh/kubeconform:${KUBECONFORM_VERSION}" \ - -strict -ignore-missing-schemas -summary < phppgadmin.yaml - docker run --rm --interactive \ - "ghcr.io/yannh/kubeconform:${KUBECONFORM_VERSION}" \ - -strict -summary < ci-deployer.yaml - - deploy: - name: Deploy to Kubernetes - if: gitea.ref == 'refs/heads/main' - needs: - - image - - validate - runs-on: ubuntu-latest - steps: - - name: Check out source - uses: actions/checkout@v4 - - - name: Install kubectl - run: | - curl --fail --silent --show-error --location \ - --output "${RUNNER_TEMP}/kubectl" \ - "https://dl.k8s.io/release/${KUBECTL_VERSION}/bin/linux/amd64/kubectl" - chmod 0700 "${RUNNER_TEMP}/kubectl" - - - name: Configure Kubernetes access - env: - KUBE_CONFIG_BASE64: ${{ secrets.KUBE_CONFIG_BASE64 }} - 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" - "${RUNNER_TEMP}/kubectl" config set-cluster cluster \ - --server="${KUBERNETES_API}" \ - --tls-server-name="${KUBERNETES_TLS_SERVER_NAME}" - - - name: Apply and verify - env: - COMMIT_SHA: ${{ gitea.sha }} - run: | - export KUBECONFIG="${RUNNER_TEMP}/kubeconfig" - sed \ - "s|deployment.brunner.ninja/revision: manual|deployment.brunner.ninja/revision: ${COMMIT_SHA}|" \ - phppgadmin.yaml \ - | sed \ - "s|registry.brunner.ninja/feedc0de/phppgadmin:latest|${IMAGE}:${COMMIT_SHA}|" \ - > "${RUNNER_TEMP}/phppgadmin.yaml" - grep -Fq "deployment.brunner.ninja/revision: ${COMMIT_SHA}" \ - "${RUNNER_TEMP}/phppgadmin.yaml" - grep -Fq "image: ${IMAGE}:${COMMIT_SHA}" \ - "${RUNNER_TEMP}/phppgadmin.yaml" - "${RUNNER_TEMP}/kubectl" apply \ - --dry-run=server \ - --validate=false \ - --filename "${RUNNER_TEMP}/phppgadmin.yaml" - "${RUNNER_TEMP}/kubectl" apply \ - --validate=false \ - --filename "${RUNNER_TEMP}/phppgadmin.yaml" - - for attempt in {1..120}; do - IFS='|' read -r generation observed desired updated ready available unavailable image <<< "$( - "${RUNNER_TEMP}/kubectl" --namespace "${NAMESPACE}" \ - get deployment/phppgadmin \ - --output=jsonpath='{.metadata.generation}|{.status.observedGeneration}|{.spec.replicas}|{.status.updatedReplicas}|{.status.readyReplicas}|{.status.availableReplicas}|{.status.unavailableReplicas}|{.spec.template.spec.containers[0].image}' - )" - - echo "Rollout ${attempt}/120: generation ${observed}/${generation}, replicas ${updated}/${desired} updated, ${ready}/${desired} ready, ${available}/${desired} available, image ${image}" - if [[ "${observed}" == "${generation}" \ - && "${updated}" == "${desired}" \ - && "${ready}" == "${desired}" \ - && "${available}" == "${desired}" \ - && ( -z "${unavailable}" || "${unavailable}" == "0" ) ]]; then - echo "phpPgAdmin deployment successfully rolled out" - exit 0 - fi - - sleep 5 - done - - echo "phpPgAdmin rollout did not complete within ten minutes" >&2 - exit 1 diff --git a/.gitea/workflows/publish.yml b/.gitea/workflows/publish.yml new file mode 100644 index 0000000..71034fe --- /dev/null +++ b/.gitea/workflows/publish.yml @@ -0,0 +1,163 @@ +name: Test, publish phpPgAdmin image and chart + +on: + push: + pull_request: + +env: + IMAGE: registry.brunner.ninja/feedc0de/phppgadmin + +jobs: + validate: + runs-on: linux_amd64 + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + - name: Install Helm + run: | + curl --fail --silent --show-error --location \ + --output "${RUNNER_TEMP}/helm.tar.gz" \ + https://get.helm.sh/helm-v4.2.2-linux-amd64.tar.gz + tar -xzf "${RUNNER_TEMP}/helm.tar.gz" -C "${RUNNER_TEMP}" linux-amd64/helm + mv "${RUNNER_TEMP}/linux-amd64/helm" "${RUNNER_TEMP}/helm" + chmod 0700 "${RUNNER_TEMP}/helm" + echo "${RUNNER_TEMP}" >> "$GITHUB_PATH" + - name: Validate and render chart + run: | + sh -n chart/install.sh + sh -n test.sh + ./chart/test.sh + + publish-image: + if: gitea.event_name == 'push' && gitea.ref == 'refs/heads/main' + needs: validate + runs-on: linux_amd64 + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + - name: Configure isolated registry access + env: + QUAY_USERNAME: ${{ secrets.QUAY_USERNAME }} + QUAY_TOKEN: ${{ secrets.QUAY_TOKEN }} + run: | + set -euo pipefail + authfile="${RUNNER_TEMP}/quay-auth.json" + printf '{"auths":{}}' > "$authfile" + chmod 0600 "$authfile" + printf '%s' "$QUAY_TOKEN" | podman login registry.brunner.ninja \ + --authfile "$authfile" --username "$QUAY_USERNAME" --password-stdin + echo "REGISTRY_AUTH_FILE=$authfile" >> "$GITHUB_ENV" + - name: Build and smoke-test image + env: + COMMIT_SHA: ${{ gitea.sha }} + run: | + set -euo pipefail + podman build --pull \ + --label "org.opencontainers.image.source=https://code.brunner.ninja/feedc0de/phppgadmin-kubernetes" \ + --label "org.opencontainers.image.revision=${COMMIT_SHA}" \ + --tag "${IMAGE}:${COMMIT_SHA}" . + podman run --rm --entrypoint httpd "${IMAGE}:${COMMIT_SHA}" -t + podman run --rm --entrypoint php "${IMAGE}:${COMMIT_SHA}" -r ' + foreach (["pgsql", "pdo_pgsql", "mbstring", "Zend OPcache"] as $extension) { + if (!extension_loaded($extension)) { fwrite(STDERR, "Missing PHP extension: {$extension}\n"); exit(1); } + } + ' + container_id=$(podman run --detach --publish 127.0.0.1::80 "${IMAGE}:${COMMIT_SHA}") + trap 'podman rm --force "$container_id" >/dev/null 2>&1 || true' EXIT + port=$(podman inspect --format '{{ (index (index .NetworkSettings.Ports "80/tcp") 0).HostPort }}' "$container_id") + for attempt in {1..30}; do + if curl --fail --silent --show-error "http://127.0.0.1:${port}/phppgadmin/" -o "${RUNNER_TEMP}/page.html" \ + && grep -Fq phpPgAdmin "${RUNNER_TEMP}/page.html"; then exit 0; fi + sleep 1 + done + podman logs "$container_id" + exit 1 + - name: Push immutable image and latest + env: + COMMIT_SHA: ${{ gitea.sha }} + run: | + set -euo pipefail + podman push "${IMAGE}:${COMMIT_SHA}" + podman tag "${IMAGE}:${COMMIT_SHA}" "${IMAGE}:latest" + podman push "${IMAGE}:latest" + - if: always() + run: rm -f "${RUNNER_TEMP}/quay-auth.json" + + publish-chart: + if: gitea.event_name == 'push' && gitea.ref == 'refs/heads/main' + needs: + - validate + - publish-image + runs-on: linux_amd64 + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + - name: Install Helm + run: | + curl --fail --silent --show-error --location \ + --output "${RUNNER_TEMP}/helm.tar.gz" \ + https://get.helm.sh/helm-v4.2.2-linux-amd64.tar.gz + tar -xzf "${RUNNER_TEMP}/helm.tar.gz" -C "${RUNNER_TEMP}" linux-amd64/helm + mv "${RUNNER_TEMP}/linux-amd64/helm" "${RUNNER_TEMP}/helm" + chmod 0700 "${RUNNER_TEMP}/helm" + echo "${RUNNER_TEMP}" >> "$GITHUB_PATH" + - id: chart + name: Derive chart version + env: + COMMIT_SHA: ${{ gitea.sha }} + run: | + set -euo pipefail + git fetch --tags origin main + test "$(git rev-parse origin/main)" = "$COMMIT_SHA" + description=$(git describe --tags --long --match 'v[0-9]*' --dirty) + case "$description" in *-dirty) echo 'Refusing dirty chart tree' >&2; exit 1 ;; esac + version=${description#v} + case "$version" in *-0-g*) version=${version%%-0-g*} ;; esac + echo "version=${version}" >> "$GITHUB_OUTPUT" + - name: Package and publish chart + env: + PACKAGE_USERNAME: ${{ secrets.PACKAGE_USERNAME }} + PACKAGE_TOKEN: ${{ secrets.PACKAGE_TOKEN }} + CHART_VERSION: ${{ steps.chart.outputs.version }} + COMMIT_SHA: ${{ gitea.sha }} + run: | + set -euo pipefail + test -n "$PACKAGE_USERNAME" && test -n "$PACKAGE_TOKEN" + helm lint chart + helm template phppgadmin chart --namespace default >/dev/null + helm package chart --version "$CHART_VERSION" --app-version "$COMMIT_SHA" --destination "$RUNNER_TEMP" + curl --fail --silent --show-error \ + --user "${PACKAGE_USERNAME}:${PACKAGE_TOKEN}" \ + --request POST --upload-file "${RUNNER_TEMP}/phppgadmin-${CHART_VERSION}.tgz" \ + https://code.brunner.ninja/api/packages/feedc0de/helm/api/charts + - name: Promote GitOps chart version + env: + GITOPS_TOKEN: ${{ secrets.GITOPS_TOKEN }} + CHART_VERSION: ${{ steps.chart.outputs.version }} + COMMIT_SHA: ${{ gitea.sha }} + run: | + set -euo pipefail + test -n "$GITOPS_TOKEN" + test "$(git rev-parse origin/main)" = "$COMMIT_SHA" + auth=$(printf 'feedc0de:%s' "$GITOPS_TOKEN" | base64 | tr -d '\n') + git -c "http.extraheader=AUTHORIZATION: basic ${auth}" clone --depth=1 \ + https://code.brunner.ninja/feedc0de/homelab-gitops.git "$RUNNER_TEMP/gitops" + unset auth + cd "$RUNNER_TEMP/gitops" + gitops_auth=$(printf 'feedc0de:%s' "$GITOPS_TOKEN" | base64 | tr -d '\n') + unset GITOPS_TOKEN + git config user.name feedc0de + git config user.email feedc0de@users.noreply.code.brunner.ninja + sed -i -E "0,/targetRevision: [^ ]+/s//targetRevision: ${CHART_VERSION}/" bootstrap/phppgadmin.yaml + if git diff --quiet; then exit 0; fi + git add bootstrap/phppgadmin.yaml + git commit -m "Deploy phpPgAdmin chart ${CHART_VERSION}" + for attempt in 1 2 3; do + if git -c "http.extraheader=AUTHORIZATION: basic ${gitops_auth}" push origin HEAD:main; then exit 0; fi + git -c "http.extraheader=AUTHORIZATION: basic ${gitops_auth}" fetch origin main + git rebase origin/main + done + exit 1 diff --git a/README.md b/README.md index 92c561b..e3d4b0b 100644 --- a/README.md +++ b/README.md @@ -1,182 +1,52 @@ # phpPgAdmin on Kubernetes -Production deployment for the brunner.ninja phpPgAdmin service. The repository -uses a small private image derived from the shared PHP/Apache base, a single -plain Kubernetes manifest, Traefik ingress, health probes, zero-unavailable -rolling updates, and restricted Gitea Actions deployment credentials. +This repository builds the phpPgAdmin image and publishes its application-owned +Helm chart. The chart owns the ConfigMap, Deployment, Service, probes, optional +Ingress, and Traefik redirect Middleware. It is stateless and stores no +database credentials or session data. -## Architecture +The homelab's live server list and Ingress hostname are in the separate +[homelab-gitops application](https://code.brunner.ninja/feedc0de/homelab-gitops/src/branch/main/apps/phppgadmin). +They contain endpoint names only; do not put PostgreSQL usernames or passwords +in Git or chart values. -- The repository image adds phpPgAdmin to the same Apache/PHP layer used by the - homepage deployments. -- CI deploys the image by its immutable Git commit tag rather than `latest`. -- `default/phppgadmin-config` mounts the complete multi-server configuration at - `/etc/phppgadmin/config.inc.php`. -- A ClusterIP Service exposes TCP port 80 to Traefik. -- `https://phppgadmin.brunner.ninja` redirects to the image's native - `/phppgadmin/` path. -- One replica uses `maxUnavailable: 0`, `maxSurge: 1`, and five seconds of - minimum readiness. The old pod remains available until the replacement has - passed its HTTP probe continuously for five seconds. -- CI places the Git commit in a pod-template annotation. This forces a safe - rollout when a ConfigMap-only change would otherwise leave a `subPath` mount - unchanged in the running container. -- Startup, readiness, and liveness probes request `/phppgadmin/` over HTTP. +## Image -The deployment is stateless. phpPgAdmin does not store database passwords or -sessions on a PVC; users authenticate directly to the selected PostgreSQL -server. The ConfigMap lists server addresses but contains no credentials. +The Dockerfile installs phpPgAdmin 7.14.7 on the shared +`registry.brunner.ninja/feedc0de/php-apache` image. Main-branch CI publishes an +immutable commit-tagged image and a chart whose `appVersion` selects that exact +image. The chart's Git tag versions the chart independently. -## Container image +## Install -The Dockerfile installs the verified phpPgAdmin 7.14.7 release directly on -`registry.brunner.ninja/feedc0de/php-apache`. The common image already provides -Apache, PHP, PostgreSQL/PDO PostgreSQL, and OPcache, so Kubernetes nodes share -those layers with the PHP homepage deployments. The phpPgAdmin layer contains -only the application and its Apache alias configuration. Its base tag is the -same immutable php-apache Git commit used by the homepage values files. - -The release archive is selected by the readable phpPgAdmin version before -extraction. phpPgAdmin is exposed at -`/phppgadmin/`, and the existing ConfigMap is mounted at -`/etc/phppgadmin/config.inc.php`. - -## Repository contents - -- `phppgadmin.yaml`: ConfigMap, Deployment, Service, Ingress, and Traefik root - redirect Middleware. -- `install.sh`: server-side dry-run, apply, and rollout verification. -- `test.sh`: local script and Kubernetes schema validation. -- `ci-deployer.yaml`: restricted Gitea Actions ServiceAccount, Role, binding, - and revocable token. -- `create-ci-kubeconfig.sh`: one-time CI identity bootstrap. -- `Dockerfile` and `phppgadmin.conf`: small phpPgAdmin layer and Apache alias - on the shared php-apache image. -- `.gitea/workflows/deploy.yml`: image build and validation on every push and - pull request; image publishing and deployment after a successful `main` run. - -phpPgAdmin is intentionally not deployed through the generic PHP homepage Helm -chart. It is stateless, has no CephFS web root, and needs its own ConfigMap and -Traefik redirect Middleware. - -## Prerequisites - -- Kubernetes and kubectl. -- The unified `registry.brunner.ninja/feedc0de/php-apache` image in Quay. -- Traefik with the `traefik.io/v1alpha1` Middleware CRD and ingress class - `traefik`. -- The existing Traefik `websecure` entrypoint and default - `*.brunner.ninja` TLS certificate. -- Network connectivity from the pod to every PostgreSQL hostname listed in the - ConfigMap. - -Another cluster operator can reuse the manifest by changing the namespace, -database server list, ingress hostname, ingress class, and redirect Middleware. - -## Install manually - -With an administrator kubeconfig active, run: +For the homelab, Argo CD is the only Kubernetes writer. Elsewhere, install from +the public chart repository with your own values: ```sh -./install.sh +helm repo add brunner.ninja https://brunner.ninja/charts +helm repo update brunner.ninja +helm upgrade --install phppgadmin brunner.ninja/phppgadmin \ + --namespace default --create-namespace --wait --timeout 5m ``` -The script performs a server-side dry-run, applies all resources, and waits up -to ten minutes for the Deployment. Existing resource names are preserved, so -the Service keeps its ClusterIP and the change rolls out in place. +To install the chart directly from a checkout, run `./chart/install.sh`. Set +`VALUES_FILE` to a cluster-specific values file when configuring server +endpoints or Ingress. -Verify both application paths: +The CI workflow needs `QUAY_USERNAME` and `QUAY_TOKEN` to publish its image, +`PACKAGE_USERNAME` and `PACKAGE_TOKEN` to publish the chart, and +`GITOPS_TOKEN` to promote the chart version in homelab-gitops. It has no +Kubernetes credentials. -```sh -curl -I https://phppgadmin.brunner.ninja/ -curl -I https://phppgadmin.brunner.ninja/phppgadmin/ -``` - -The first should redirect permanently and the second should return the -phpPgAdmin application. - -## Configure PostgreSQL servers - -Edit `config.inc.php` inside the ConfigMap in `phppgadmin.yaml`. Each server -entry contains a display name, service DNS name, port, SSL mode, and default -database. Do not put PostgreSQL usernames or passwords into the ConfigMap or -Git repository. - -The file is mounted using `subPath`, so Kubernetes does not update it inside an -already-running pod. The CI workflow replaces the manifest's `manual` revision -annotation with the Git commit SHA before applying it, which safely rolls the -pod for every merged configuration change. A manual `./install.sh` changes the -annotation back to `manual` and likewise triggers a rollout after a CI deploy. - -## Validate locally +Validate locally with: ```sh ./test.sh ``` -The test checks both shell scripts and validates the Kubernetes resources with -kubeconform. The Traefik Middleware is reported as skipped because kubeconform's -standard schema catalog does not contain cluster-specific CRDs; the Kubernetes -API validates it during the server-side dry-run. - -## Automatic deployment with Gitea Actions - -The workflow validates every push and pull request. A successful push to -`main` applies the complete `phppgadmin.yaml`, including ConfigMap, Deployment, -Service, Ingress, and Middleware, then polls the Deployment for up to ten -minutes. - -The workflow builds and smoke-tests the image for every push and pull request. -On `main` it pushes the immutable Git commit tag plus `latest`, substitutes the -immutable tag in the deployment manifest, and deploys it. It requires these -Gitea Actions secrets: - -- `QUAY_USERNAME`: Quay robot-account username with read/write access. -- `QUAY_TOKEN`: token for that robot account. -- `KUBE_CONFIG_BASE64`: the restricted phpPgAdmin deployment kubeconfig, - base64-encoded on one line. - -### One-time CI setup - -1. Run `./install.sh` with an administrator identity so every application - resource already exists. -2. Run: - - ```sh - ./create-ci-kubeconfig.sh - ``` - -3. Copy only the final base64 line into the Gitea repository Actions secret - named `KUBE_CONFIG_BASE64`. -4. Protect `main`, require successful validation before merging, and review - manifest and workflow changes. -5. Push a harmless README change and confirm the deployment job succeeds. - -The workflow matches the existing brunner.ninja runner: Kubernetes API URL -`https://host.containers.internal:6443`, with TLS server name `192.168.0.2`. -Other clusters must change those values. - -CI can get, patch, and update only these existing named objects: - -- ConfigMap `phppgadmin-config`; -- Deployment and Service `phppgadmin`; -- Ingress `phppgadmin`; -- Middleware `phppgadmin-root-redirect`. - -It cannot create or delete resources and cannot call the Kubernetes Secrets -API. Revoke it by deleting `default/phppgadmin-deployer-token` and the Gitea -secret. Rerunning the wrapper creates a replacement token. - -Kubernetes RBAC cannot constrain individual fields inside an allowed -Deployment patch. A malicious change merged into `main` could alter the pod -specification. Branch protection and review therefore remain part of the -security boundary. - ## Internet exposure -This instance is reachable from the public internet and presents direct -PostgreSQL username/password authentication. Use long, unique database -passwords, restrictive PostgreSQL roles and `pg_hba.conf` rules, TLS for remote -database connections, and external rate limiting or access control where -appropriate. phpPgAdmin's `extra_login_security` remains enabled in the -committed configuration. +The homelab instance is public and presents direct PostgreSQL login. Use long, +unique database passwords, restrictive PostgreSQL roles and `pg_hba.conf` +rules, TLS for remote database connections, and suitable external access +controls. The chart retains phpPgAdmin's extra-login security defaults. diff --git a/chart/Chart.yaml b/chart/Chart.yaml new file mode 100644 index 0000000..6c3e57f --- /dev/null +++ b/chart/Chart.yaml @@ -0,0 +1,6 @@ +apiVersion: v2 +name: phppgadmin +description: Helm chart for the phpPgAdmin container +type: application +version: 0.1.0 +appVersion: "4c29807abb362b9cf407762822113c5c75d442f3" diff --git a/chart/README.md b/chart/README.md new file mode 100644 index 0000000..2f29870 --- /dev/null +++ b/chart/README.md @@ -0,0 +1,18 @@ +# phpPgAdmin Helm chart + +This chart deploys the phpPgAdmin image built in this repository. It owns the +Deployment, Service, ConfigMap, optional Ingress, and Traefik root redirect +Middleware. Configure database server endpoints through `servers`; do not put +database passwords in chart values. + +The homelab configuration is maintained in +[homelab-gitops](https://code.brunner.ninja/feedc0de/homelab-gitops/src/branch/main/apps/phppgadmin). +For a portable local install: + +```sh +helm upgrade --install phppgadmin ./chart --namespace default \ + --create-namespace --wait --timeout 5m +``` + +Chart tags version the deployment contract. `appVersion` selects the exact +immutable container image tag built by CI. diff --git a/chart/install.sh b/chart/install.sh new file mode 100755 index 0000000..39b9cee --- /dev/null +++ b/chart/install.sh @@ -0,0 +1,16 @@ +#!/bin/sh +set -eu + +release=${RELEASE_NAME:-phppgadmin} +namespace=${NAMESPACE:-default} + +helm repo add brunner.ninja https://brunner.ninja/charts --force-update +helm repo update brunner.ninja +if [ -n "${VALUES_FILE:-}" ]; then + helm upgrade --install "$release" brunner.ninja/phppgadmin \ + --namespace "$namespace" --create-namespace \ + --values "$VALUES_FILE" --wait --timeout 5m +else + helm upgrade --install "$release" brunner.ninja/phppgadmin \ + --namespace "$namespace" --create-namespace --wait --timeout 5m +fi diff --git a/chart/templates/configmap.yaml b/chart/templates/configmap.yaml new file mode 100644 index 0000000..20c158f --- /dev/null +++ b/chart/templates/configmap.yaml @@ -0,0 +1,40 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: phppgadmin-config + labels: + app.kubernetes.io/name: phppgadmin + app.kubernetes.io/instance: {{ .Release.Name }} +data: + config.inc.php: | + "$workdir/rendered.yaml" +grep -q 'name: phppgadmin-config' "$workdir/rendered.yaml" +grep -q 'name: phppgadmin-root-redirect' "$workdir/rendered.yaml" +grep -q "host.*postgres.default.svc" "$workdir/rendered.yaml" +app_version=$(sed -n 's/^appVersion: *"\{0,1\}\([^" ]*\).*/\1/p' "$chart_dir/Chart.yaml") +image_repository=$(sed -n 's/^ repository: *//p' "$chart_dir/values.yaml") +grep -Fq "${image_repository}:${app_version}" "$workdir/rendered.yaml" diff --git a/chart/values.schema.json b/chart/values.schema.json new file mode 100644 index 0000000..fb76cfc --- /dev/null +++ b/chart/values.schema.json @@ -0,0 +1,34 @@ +{ + "$schema": "https://json-schema.org/draft-07/schema#", + "type": "object", + "properties": { + "replicaCount": {"type": "integer", "minimum": 1}, + "image": { + "type": "object", + "required": ["repository", "pullPolicy"], + "properties": { + "repository": {"type": "string", "minLength": 1}, + "pullPolicy": {"type": "string", "enum": ["Always", "IfNotPresent", "Never"]} + }, + "additionalProperties": false + }, + "ingress": {"type": "object"}, + "servers": { + "type": "array", + "items": { + "type": "object", + "required": ["description", "host"], + "properties": { + "description": {"type": "string"}, + "host": {"type": "string"}, + "port": {"type": "integer", "minimum": 1, "maximum": 65535}, + "sslmode": {"type": "string"}, + "defaultdb": {"type": "string"} + }, + "additionalProperties": false + } + }, + "resources": {"type": "object"} + }, + "additionalProperties": true +} diff --git a/chart/values.yaml b/chart/values.yaml new file mode 100644 index 0000000..4cf043a --- /dev/null +++ b/chart/values.yaml @@ -0,0 +1,15 @@ +replicaCount: 1 + +image: + repository: registry.brunner.ninja/feedc0de/phppgadmin + pullPolicy: Always + +ingress: + enabled: false + host: "" + ingressClassName: traefik + entrypoints: websecure + +servers: [] + +resources: {} diff --git a/install.sh b/install.sh index 59f621f..add9613 100755 --- a/install.sh +++ b/install.sh @@ -2,19 +2,6 @@ set -eu -namespace=default script_dir=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd) -if ! command -v kubectl >/dev/null 2>&1; then - echo "Required command not found: kubectl" >&2 - exit 1 -fi - -kubectl apply --dry-run=server --filename "$script_dir/phppgadmin.yaml" -kubectl apply --filename "$script_dir/phppgadmin.yaml" -kubectl rollout status deployment/phppgadmin \ - --namespace "$namespace" \ - --timeout=10m - -echo -echo "phpPgAdmin is available at https://phppgadmin.brunner.ninja." +exec "$script_dir/chart/install.sh" "$@" diff --git a/test.sh b/test.sh index 627176a..ce9205b 100755 --- a/test.sh +++ b/test.sh @@ -4,25 +4,5 @@ set -euo pipefail project_dir=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd) -sh -n "${project_dir}/install.sh" -bash -n "${project_dir}/create-ci-kubeconfig.sh" - -validate_file() { - local manifest=$1 - - if command -v kubeconform >/dev/null 2>&1; then - kubeconform -strict -ignore-missing-schemas -summary "${manifest}" - elif command -v docker >/dev/null 2>&1; then - docker run --rm --interactive \ - ghcr.io/yannh/kubeconform:v0.7.0 \ - -strict -ignore-missing-schemas -summary < "${manifest}" - else - echo "Required command not found: install kubeconform or Docker" >&2 - exit 1 - fi -} - -validate_file "${project_dir}/phppgadmin.yaml" -validate_file "${project_dir}/ci-deployer.yaml" - -echo "phpPgAdmin scripts and Kubernetes resources are valid." +sh -n "${project_dir}/chart/install.sh" +"${project_dir}/chart/test.sh"