From 9ae5e84d9718589a7b4a5956741aca399c2032a8 Mon Sep 17 00:00:00 2001 From: 0xFEEDC0DE64 Date: Sat, 5 Sep 2026 18:17:06 +0200 Subject: [PATCH] Fix CI/CD --- .gitea/workflows/deploy.yml | 10 +++++--- README.md | 7 +++++ kubernetes-dashboard-values.yaml | 34 +++++++++++++++++++++++- test.sh | 14 ++++++++++ wait-for-deployments.sh | 44 ++++++++++++++++++++++++++++++++ 5 files changed, 105 insertions(+), 4 deletions(-) create mode 100755 wait-for-deployments.sh diff --git a/.gitea/workflows/deploy.yml b/.gitea/workflows/deploy.yml index 42c1ccf..0d1913a 100644 --- a/.gitea/workflows/deploy.yml +++ b/.gitea/workflows/deploy.yml @@ -65,9 +65,13 @@ jobs: --filename auth-proxy.yaml \ --filename kubernetes-dashboard-ingressroute.yaml - for deployment in kubernetes-dashboard-api kubernetes-dashboard-auth kubernetes-dashboard-web kubernetes-dashboard-metrics-scraper kubernetes-dashboard-kong kubernetes-dashboard-auth-proxy; do - kubectl --namespace "${NAMESPACE}" rollout status "deployment/${deployment}" --timeout=10m - done + bash ./wait-for-deployments.sh \ + kubernetes-dashboard-api \ + kubernetes-dashboard-auth \ + kubernetes-dashboard-web \ + kubernetes-dashboard-metrics-scraper \ + kubernetes-dashboard-kong \ + kubernetes-dashboard-auth-proxy status=$(curl --silent --show-error --output /dev/null --write-out '%{http_code}' --max-time 15 https://kubernetes.brunner.ninja/) case "${status}" in diff --git a/README.md b/README.md index c14400b..e841a62 100644 --- a/README.md +++ b/README.md @@ -18,6 +18,11 @@ automatically every hour; it is never stored in Git, a static Secret, Gitea, or Traefik configuration. Supplying the Authorization header makes Dashboard skip its bearer-token login page. +Every Dashboard container retains a CPU request, right-sized to `25m` from an +observed idle use of roughly `1-2m`. Requests are not removed: they still give +the scheduler a placement signal while the higher CPU limits leave room for +interactive bursts. + This preserves the previous cluster-admin behavior, but Kubernetes audit events identify every Dashboard action as the same service account. Native Authentik OIDC would provide per-user identity, but requires kube-apiserver configuration @@ -44,6 +49,8 @@ Then run `./create-ci-kubeconfig.sh` and store its final line as the Gitea Actions secret `KUBE_CONFIG_BASE64`. The CI identity can update only named, already-existing Dashboard objects. It cannot read Secrets, edit the `admin-user` cluster-admin binding, create resources, or delete resources. +Deployment readiness is checked by polling those exact object names; the CI +identity deliberately has no namespace-wide Deployment `list/watch` access. The deploy workflow runs only for `main`; pull requests and all pushes run the validation job. The chart's generated CSRF Secret is created by the bootstrap diff --git a/kubernetes-dashboard-values.yaml b/kubernetes-dashboard-values.yaml index 8011a4a..9d3d228 100644 --- a/kubernetes-dashboard-values.yaml +++ b/kubernetes-dashboard-values.yaml @@ -20,6 +20,14 @@ auth: scaling: replicas: 2 revisionHistoryLimit: 3 + containers: + resources: + requests: + cpu: 25m + memory: 200Mi + limits: + cpu: 250m + memory: 400Mi api: image: @@ -28,6 +36,14 @@ api: scaling: replicas: 2 revisionHistoryLimit: 3 + containers: + resources: + requests: + cpu: 25m + memory: 200Mi + limits: + cpu: 250m + memory: 400Mi web: image: @@ -36,6 +52,14 @@ web: scaling: replicas: 2 revisionHistoryLimit: 3 + containers: + resources: + requests: + cpu: 25m + memory: 200Mi + limits: + cpu: 250m + memory: 400Mi metricsScraper: enabled: true @@ -44,6 +68,14 @@ metricsScraper: tag: 1.2.2 scaling: revisionHistoryLimit: 3 + containers: + resources: + requests: + cpu: 25m + memory: 200Mi + limits: + cpu: 250m + memory: 400Mi # Browser TLS terminates at Traefik. Kong still uses TLS because the private # auth proxy sends the privileged projected bearer token on this hop. @@ -55,7 +87,7 @@ kong: replicaCount: 2 resources: requests: - cpu: 100m + cpu: 25m memory: 256Mi limits: cpu: 500m diff --git a/test.sh b/test.sh index d25d0a3..18c98f0 100755 --- a/test.sh +++ b/test.sh @@ -11,6 +11,7 @@ bash -n \ "${project_dir}/pin-images.sh" \ "${project_dir}/render.sh" \ "${project_dir}/create-ci-kubeconfig.sh" \ + "${project_dir}/wait-for-deployments.sh" \ "${project_dir}/test.sh" awk -f "${project_dir}/filter-secrets.awk" /dev/null >/dev/null @@ -34,11 +35,24 @@ if grep -Fq 'namespace: default' "${project_dir}/kubernetes-dashboard-ingressrou echo "Dashboard route must not use a cross-namespace middleware" >&2 exit 1 fi +if grep -Fq 'rollout status' "${project_dir}/.gitea/workflows/deploy.yml"; then + echo "Restricted CI must poll exact Deployment names instead of list/watch" >&2 + exit 1 +fi grep -Fq 'expirationSeconds: 3600' "${project_dir}/auth-proxy.yaml" grep -Fq 'proxy_pass https://kubernetes-dashboard-kong-proxy' "${project_dir}/auth-proxy.yaml" grep -Fq 'containerPort: 8443' "${workdir}/dashboard.yaml" grep -Fq 'dashboard-api:1.14.0@sha256:2bd14c0ffee99d15fb1595644ebd1083ac32c5157c6e6fd8615b0f556a1390c2' "${workdir}/dashboard.yaml" +request_count=$(awk ' + $1 == "cpu:" && $2 == "25m" { count++ } + END { print count + 0 } +' "${workdir}/dashboard.yaml") +if [[ "${request_count}" -lt 5 ]] || grep -Fq 'cpu: 100m' "${workdir}/dashboard.yaml"; then + echo "Expected every Dashboard chart component to request only 25m CPU" >&2 + exit 1 +fi + image=ghcr.io/yannh/kubeconform:v0.7.0@sha256:85dbef6b4b312b99133decc9c6fc9495e9fc5f92293d4ff3b7e1b30f5611823c for file in \ "${workdir}/dashboard.yaml" \ diff --git a/wait-for-deployments.sh b/wait-for-deployments.sh new file mode 100755 index 0000000..54c05fb --- /dev/null +++ b/wait-for-deployments.sh @@ -0,0 +1,44 @@ +#!/usr/bin/env bash + +set -euo pipefail + +namespace=${NAMESPACE:-kubernetes-dashboard} +timeout_seconds=${DEPLOYMENT_TIMEOUT_SECONDS:-600} +poll_seconds=${DEPLOYMENT_POLL_SECONDS:-5} + +if [[ "$#" -eq 0 ]]; then + echo "Usage: wait-for-deployments.sh DEPLOYMENT..." >&2 + exit 2 +fi + +for deployment in "$@"; do + deadline=$((SECONDS + timeout_seconds)) + state="not observed" + complete=false + + while ((SECONDS < deadline)); do + state=$(kubectl --namespace "${namespace}" get "deployment/${deployment}" \ + --output=jsonpath='{.metadata.generation}{"|"}{.status.observedGeneration}{"|"}{.spec.replicas}{"|"}{.status.updatedReplicas}{"|"}{.status.readyReplicas}{"|"}{.status.availableReplicas}{"\n"}') + IFS='|' read -r generation observed desired updated ready available <<< "${state}" + observed=${observed:-0} + desired=${desired:-0} + updated=${updated:-0} + ready=${ready:-0} + available=${available:-0} + + if [[ "${observed}" -ge "${generation}" \ + && "${updated}" -eq "${desired}" \ + && "${ready}" -eq "${desired}" \ + && "${available}" -eq "${desired}" ]]; then + echo "deployment/${deployment} is ready (${ready}/${desired})" + complete=true + break + fi + sleep "${poll_seconds}" + done + + if [[ "${complete}" != true ]]; then + echo "Timed out waiting for deployment/${deployment}; generation observed desired updated ready available: ${state}" >&2 + exit 1 + fi +done