Add Argo CD Helm chart release
Test, publish phpPgAdmin image and chart / publish-image (push) Successful in 7s
Test, publish phpPgAdmin image and chart / validate (push) Successful in 2s
Test, publish phpPgAdmin image and chart / publish-chart (push) Successful in 6s

This commit is contained in:
feedc0de committed 2026-10-19 08:43:30 +02:00
1 parent 4c29807abb
commit f2fb39ca62
16 files changed
+471 -385

No files matched your search

-187
View File
@@ -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
+163
View File
@@ -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
+32 -162
View File
@@ -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.
+6
View File
@@ -0,0 +1,6 @@
apiVersion: v2
name: phppgadmin
description: Helm chart for the phpPgAdmin container
type: application
version: 0.1.0
appVersion: "4c29807abb362b9cf407762822113c5c75d442f3"
+18
View File
@@ -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.
+16
View File
@@ -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
+40
View File
@@ -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: |
<?php
{{- range $index, $server := .Values.servers }}
$conf['servers'][{{ $index }}]['desc'] = {{ $server.description | quote }};
$conf['servers'][{{ $index }}]['host'] = {{ $server.host | quote }};
$conf['servers'][{{ $index }}]['port'] = {{ $server.port | default 5432 }};
$conf['servers'][{{ $index }}]['sslmode'] = {{ $server.sslmode | default "allow" | quote }};
$conf['servers'][{{ $index }}]['defaultdb'] = {{ $server.defaultdb | default "template1" | quote }};
{{- if eq $index 0 }}
$conf['servers'][{{ $index }}]['pg_dump_path'] = '/usr/bin/pg_dump';
$conf['servers'][{{ $index }}]['pg_dumpall_path'] = '/usr/bin/pg_dumpall';
{{- end }}
{{- end }}
$conf['default_lang'] = 'auto';
$conf['extra_session_security'] = true;
$conf['autocomplete'] = 'default on';
$conf['extra_login_security'] = true;
$conf['owned_only'] = false;
$conf['show_comments'] = true;
$conf['show_advanced'] = false;
$conf['show_system'] = false;
$conf['min_password_length'] = 1;
$conf['left_width'] = 200;
$conf['theme'] = 'default';
$conf['show_oids'] = false;
$conf['max_rows'] = 30;
$conf['max_chars'] = 50;
$conf['use_xhtml_strict'] = false;
$conf['help_base'] = 'http://www.postgresql.org/docs/%s/interactive/';
$conf['ajax_refresh'] = 3;
$conf['plugins'] = array();
$conf['version'] = 19;
+73
View File
@@ -0,0 +1,73 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: phppgadmin
labels:
app: phppgadmin
app.kubernetes.io/name: phppgadmin
app.kubernetes.io/instance: {{ .Release.Name }}
spec:
replicas: {{ .Values.replicaCount }}
revisionHistoryLimit: 3
minReadySeconds: 5
progressDeadlineSeconds: 600
strategy:
type: RollingUpdate
rollingUpdate:
maxUnavailable: 0
maxSurge: 1
selector:
matchLabels:
app: phppgadmin
template:
metadata:
labels:
app: phppgadmin
app.kubernetes.io/name: phppgadmin
app.kubernetes.io/instance: {{ .Release.Name }}
annotations:
checksum/config: {{ include (print $.Template.BasePath "/configmap.yaml") . | sha256sum }}
spec:
automountServiceAccountToken: false
terminationGracePeriodSeconds: 30
containers:
- name: phppgadmin
image: {{ printf "%s:%s" .Values.image.repository .Chart.AppVersion | quote }}
imagePullPolicy: {{ .Values.image.pullPolicy }}
ports:
- name: http
containerPort: 80
protocol: TCP
startupProbe:
httpGet:
path: /phppgadmin/
port: http
periodSeconds: 2
timeoutSeconds: 2
failureThreshold: 60
readinessProbe:
httpGet:
path: /phppgadmin/
port: http
periodSeconds: 5
timeoutSeconds: 3
failureThreshold: 2
livenessProbe:
httpGet:
path: /phppgadmin/
port: http
periodSeconds: 15
timeoutSeconds: 3
failureThreshold: 3
{{- with .Values.resources }}
resources:
{{- toYaml . | nindent 12 }}
{{- end }}
volumeMounts:
- name: phppgadmin-config
mountPath: /etc/phppgadmin/config.inc.php
subPath: config.inc.php
volumes:
- name: phppgadmin-config
configMap:
name: phppgadmin-config
+24
View File
@@ -0,0 +1,24 @@
{{- if .Values.ingress.enabled }}
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: phppgadmin
labels:
app.kubernetes.io/name: phppgadmin
annotations:
traefik.ingress.kubernetes.io/router.entrypoints: {{ .Values.ingress.entrypoints | quote }}
traefik.ingress.kubernetes.io/router.middlewares: default-phppgadmin-root-redirect@kubernetescrd
spec:
ingressClassName: {{ .Values.ingress.ingressClassName | quote }}
rules:
- host: {{ .Values.ingress.host | quote }}
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: phppgadmin
port:
name: http
{{- end }}
+13
View File
@@ -0,0 +1,13 @@
{{- if .Values.ingress.enabled }}
apiVersion: traefik.io/v1alpha1
kind: Middleware
metadata:
name: phppgadmin-root-redirect
labels:
app.kubernetes.io/name: phppgadmin
spec:
redirectRegex:
regex: ^https://{{ .Values.ingress.host | replace "." "\\." }}/?$
replacement: https://{{ .Values.ingress.host }}/phppgadmin/
permanent: true
{{- end }}
+16
View File
@@ -0,0 +1,16 @@
apiVersion: v1
kind: Service
metadata:
name: phppgadmin
labels:
app: phppgadmin
app.kubernetes.io/name: phppgadmin
app.kubernetes.io/instance: {{ .Release.Name }}
spec:
ports:
- name: http
port: 80
protocol: TCP
targetPort: http
selector:
app: phppgadmin
Executable
+18
View File
@@ -0,0 +1,18 @@
#!/bin/sh
set -eu
chart_dir=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
workdir=$(mktemp -d)
trap 'rm -rf "$workdir"' EXIT
helm lint "$chart_dir"
helm template phppgadmin "$chart_dir" --namespace default \
--set ingress.enabled=true --set ingress.host=dbadmin.example \
--set servers[0].description=local --set servers[0].host=postgres.default.svc \
>"$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"
+34
View File
@@ -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
}
+15
View File
@@ -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: {}
+1 -14
View File
@@ -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" "$@"
+2 -22
View File
@@ -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"