Add Argo CD Helm chart release
This commit is contained in:
1 parent
4c29807abb
commit
f2fb39ca62
16 files changed
+471
-385
No files matched your search
@@ -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
|
||||
@@ -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
|
||||
@@ -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.
|
||||
@@ -0,0 +1,6 @@
|
||||
apiVersion: v2
|
||||
name: phppgadmin
|
||||
description: Helm chart for the phpPgAdmin container
|
||||
type: application
|
||||
version: 0.1.0
|
||||
appVersion: "4c29807abb362b9cf407762822113c5c75d442f3"
|
||||
@@ -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.
|
||||
Executable
+16
@@ -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
|
||||
@@ -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;
|
||||
@@ -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
|
||||
@@ -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 }}
|
||||
@@ -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 }}
|
||||
@@ -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
@@ -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"
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
@@ -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" "$@"
|
||||
@@ -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"
|
||||
Reference in new issue
Block a user