Retire direct Argo CD deployment

This commit is contained in:
2026-10-18 15:23:34 +02:00
parent d2f4bb7c95
commit abc2c69217
8 changed files with 8 additions and 324 deletions
-60
View File
@@ -1,60 +0,0 @@
name: Validate and deploy Argo CD
on:
push:
pull_request:
env:
ARGOCD_CHART_VERSION: 10.7.2
HELM_VERSION: v4.2.2
KUBECTL_VERSION: v1.36.3
KUBERNETES_API: https://host.containers.internal:6443
KUBERNETES_TLS_SERVER_NAME: 192.168.0.2
NAMESPACE: argocd
jobs:
validate:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install Helm
run: |
curl -fsSL -o "${RUNNER_TEMP}/helm.tgz" "https://get.helm.sh/helm-${HELM_VERSION}-linux-amd64.tar.gz"
tar -xzf "${RUNNER_TEMP}/helm.tgz" -C "${RUNNER_TEMP}" --strip-components=1 linux-amd64/helm
echo "${RUNNER_TEMP}" >> "${GITHUB_PATH}"
- run: bash test.sh
deploy:
if: gitea.ref == 'refs/heads/main'
needs: validate
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install clients
run: |
curl -fsSL -o "${RUNNER_TEMP}/helm.tgz" "https://get.helm.sh/helm-${HELM_VERSION}-linux-amd64.tar.gz"
tar -xzf "${RUNNER_TEMP}/helm.tgz" -C "${RUNNER_TEMP}" --strip-components=1 linux-amd64/helm
curl -fsSL -o "${RUNNER_TEMP}/kubectl" "https://dl.k8s.io/release/${KUBECTL_VERSION}/bin/linux/amd64/kubectl"
chmod 0700 "${RUNNER_TEMP}/helm" "${RUNNER_TEMP}/kubectl"
echo "${RUNNER_TEMP}" >> "${GITHUB_PATH}"
- name: Configure cluster
env:
KUBE_CONFIG_BASE64: ${{ secrets.KUBE_CONFIG_BASE64 }}
run: |
test -n "${KUBE_CONFIG_BASE64}"
printf '%s' "${KUBE_CONFIG_BASE64}" | base64 -d > "${RUNNER_TEMP}/kubeconfig"
chmod 0600 "${RUNNER_TEMP}/kubeconfig"
export KUBECONFIG="${RUNNER_TEMP}/kubeconfig"
kubectl config set-cluster cluster --server="${KUBERNETES_API}" --tls-server-name="${KUBERNETES_TLS_SERVER_NAME}"
- name: Apply and verify
run: |
export KUBECONFIG="${RUNNER_TEMP}/kubeconfig"
./render.sh "${RUNNER_TEMP}/argocd.yaml"
kubectl apply --server-side --force-conflicts --dry-run=server --validate=false -f "${RUNNER_TEMP}/argocd.yaml"
kubectl apply --server-side --force-conflicts --validate=false -f "${RUNNER_TEMP}/argocd.yaml"
for resource in deployment/argocd-applicationset-controller deployment/argocd-notifications-controller deployment/argocd-repo-server deployment/argocd-server deployment/argocd-dex-server deployment/argocd-redis statefulset/argocd-application-controller; do
for attempt in {1..120}; do
IFS='|' read -r generation observed desired updated ready available <<< "$(kubectl -n "${NAMESPACE}" get "${resource}" -o jsonpath='{.metadata.generation}|{.status.observedGeneration}|{.spec.replicas}|{.status.updatedReplicas}|{.status.readyReplicas}|{.status.availableReplicas}')"
echo "${resource} ${attempt}/120: ${updated}/${desired} updated, ${ready}/${desired} ready"
[[ "${observed}" == "${generation}" && "${updated}" == "${desired}" && "${ready}" == "${desired}" && "${available}" == "${desired}" ]] && break
[[ "${attempt}" == 120 ]] && { echo "${resource} rollout timed out" >&2; exit 1; }
sleep 5
done
done
curl -fsS --max-time 15 https://cd.brunner.ninja/api/version >/dev/null
+8 -23
View File
@@ -1,25 +1,10 @@
# Argo CD on Kubernetes
# Argo CD deployment (retired)
Argo CD is pinned to chart `10.7.2` (Argo CD `v3.5.2`) and deployed in
`argocd`. CRDs, notifications, Dex, and the chart-supported Redis backend stay
enabled. Login is delegated directly to the Authentik OIDC provider at
`/application/o/argocd/`; members of `authentik Admins` receive Argo CD admin
access. The committed values deliberately render no Secrets: existing
`argocd-secret`, `argocd-notifications-secret`, `argocd-redis`, and
`argocd-oidc` stay inside Kubernetes and continue to be consumed by the
enabled components.
Argo CD now manages its own pinned upstream chart from the public
[homelab-gitops application](https://code.brunner.ninja/feedc0de/homelab-gitops/src/branch/main/apps/argocd).
That repository contains the canonical chart version, cluster values, Argo
Application, AppProject, and administrator recovery installer.
Run `./install.sh` for administrator bootstrap or `./test.sh` for local chart
rendering and schema validation. Gitea Actions validates every change and
updates only exact existing Argo CD objects and its three exact CRDs on `main`;
it cannot read Secrets, create objects, or delete objects. Because it manages
privileged Argo CD workloads and their exact RBAC objects, protect and review
`main`.
Create the CI identity with `./create-ci-kubeconfig.sh` (or set
`KUBECTL_SSH_HOST=arschrock`), then store only its final line as the repository
Actions secret `KUBE_CONFIG_BASE64`.
The Authentik repository's administrator install provisions and synchronizes
the OIDC client secret into `authentik/authentik-runtime` and
`argocd/argocd-oidc`. CI can reference that Secret but cannot read or alter it.
This repository no longer contains deployment automation or cluster
credentials and may be archived. Its Git history remains available as the
record of the former direct-deployment and bootstrap approach.
-43
View File
@@ -1,43 +0,0 @@
global:
domain: cd.brunner.ninja
crds:
install: true
configs:
cm:
oidc.config: |
name: Authentik
issuer: https://auth.brunner.ninja/application/o/argocd/
clientID: argocd
clientSecret: $argocd-oidc:client-secret
requestedScopes: ["openid", "profile", "email", "groups"]
requestedIDTokenClaims:
groups:
essential: true
rbac:
policy.csv: |
g, authentik Admins, role:admin
policy.default: ''
scopes: '[groups]'
secret:
# Preserve the existing argocd-secret; no secret values belong in Git.
createSecret: false
params:
server.insecure: true
notifications:
secret:
# Preserve the existing argocd-notifications-secret.
create: false
redisSecretInit:
# Preserve the existing argocd-redis Secret without giving CI Secret access.
enabled: false
server:
ingress:
enabled: true
# ingressClassName: traefik
annotations:
traefik.ingress.kubernetes.io/router.entrypoints: websecure
-82
View File
@@ -1,82 +0,0 @@
apiVersion: v1
kind: ServiceAccount
metadata:
name: argocd-deployer
namespace: argocd
automountServiceAccountToken: false
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: argocd-deployer
rules:
- apiGroups: [apiextensions.k8s.io]
resources: [customresourcedefinitions]
resourceNames: [applications.argoproj.io, applicationsets.argoproj.io, appprojects.argoproj.io]
verbs: [get, patch, update]
- apiGroups: [networking.k8s.io]
resources: [networkpolicies]
resourceNames: [argocd-application-controller, argocd-notifications-controller, argocd-repo-server, argocd-server, argocd-dex-server, argocd-redis]
verbs: [get, patch, update]
- apiGroups: [""]
resources: [serviceaccounts]
resourceNames: [argocd-application-controller, argocd-applicationset-controller, argocd-notifications-controller, argocd-repo-server, argocd-server, argocd-dex-server]
verbs: [get, patch, update]
- apiGroups: [""]
resources: [configmaps]
resourceNames: [argocd-cm, argocd-cmd-params-cm, argocd-gpg-keys-cm, argocd-notifications-cm, argocd-rbac-cm, argocd-ssh-known-hosts-cm, argocd-tls-certs-cm, argocd-redis-health-configmap]
verbs: [get, patch, update]
- apiGroups: [""]
resources: [services]
resourceNames: [argocd-applicationset-controller, argocd-repo-server, argocd-server, argocd-dex-server, argocd-redis]
verbs: [get, patch, update]
- apiGroups: [apps]
resources: [deployments]
resourceNames: [argocd-applicationset-controller, argocd-notifications-controller, argocd-repo-server, argocd-server, argocd-dex-server, argocd-redis]
verbs: [get, patch, update]
- apiGroups: [apps]
resources: [statefulsets]
resourceNames: [argocd-application-controller]
verbs: [get, patch, update]
- apiGroups: [networking.k8s.io]
resources: [ingresses]
resourceNames: [argocd-server]
verbs: [get, patch, update]
- apiGroups: [rbac.authorization.k8s.io]
resources: [clusterroles]
resourceNames: [argocd-application-controller, argocd-notifications-controller, argocd-server]
verbs: [get, patch, update, escalate, bind]
- apiGroups: [rbac.authorization.k8s.io]
resources: [clusterrolebindings]
resourceNames: [argocd-application-controller, argocd-notifications-controller, argocd-server]
verbs: [get, patch, update]
- apiGroups: [rbac.authorization.k8s.io]
resources: [roles]
resourceNames: [argocd-application-controller, argocd-applicationset-controller, argocd-notifications-controller, argocd-repo-server, argocd-server, argocd-dex-server]
verbs: [get, patch, update, escalate, bind]
- apiGroups: [rbac.authorization.k8s.io]
resources: [rolebindings]
resourceNames: [argocd-application-controller, argocd-applicationset-controller, argocd-notifications-controller, argocd-repo-server, argocd-server, argocd-dex-server]
verbs: [get, patch, update]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: argocd-deployer
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: argocd-deployer
subjects:
- kind: ServiceAccount
name: argocd-deployer
namespace: argocd
---
apiVersion: v1
kind: Secret
metadata:
name: argocd-deployer-token
namespace: argocd
annotations:
kubernetes.io/service-account.name: argocd-deployer
type: kubernetes.io/service-account-token
-26
View File
@@ -1,26 +0,0 @@
#!/usr/bin/env bash
set -euo pipefail
project_dir=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)
namespace=${NAMESPACE:-argocd}
service_account=argocd-deployer
secret=argocd-deployer-token
ssh_host=${KUBECTL_SSH_HOST:-}
cluster_kubectl() { if [[ -n "${ssh_host}" ]]; then ssh "${ssh_host}" kubectl "$@"; else kubectl "$@"; fi; }
if [[ -n "${ssh_host}" ]]; then cluster_kubectl apply -f - < "${project_dir}/ci-deployer.yaml" >&2; else cluster_kubectl apply -f "${project_dir}/ci-deployer.yaml" >&2; fi
for attempt in {1..30}; do
token_data=$(cluster_kubectl -n "${namespace}" get secret "${secret}" -o jsonpath='{.data.token}' 2>/dev/null || true)
[[ -n "${token_data}" ]] && break
[[ "${attempt}" == 30 ]] && { echo "Timed out waiting for token" >&2; exit 1; }
sleep 1
done
workdir=$(mktemp -d); trap 'rm -rf "${workdir}"' EXIT
server=$(cluster_kubectl config view --minify -o jsonpath='{.clusters[0].cluster.server}')
cluster_kubectl -n "${namespace}" get secret "${secret}" -o jsonpath='{.data.ca\.crt}' | base64 -d > "${workdir}/ca.crt"
token=$(printf '%s' "${token_data}" | base64 -d)
export KUBECONFIG="${workdir}/config"
kubectl config set-cluster cluster --server="${server}" --certificate-authority="${workdir}/ca.crt" --embed-certs=true >/dev/null
kubectl config set-credentials "${service_account}" --token="${token}" >/dev/null
kubectl config set-context argocd --cluster=cluster --user="${service_account}" --namespace="${namespace}" >/dev/null
kubectl config use-context argocd >/dev/null
echo "Store the following line as KUBE_CONFIG_BASE64; do not commit it." >&2
base64 -w0 "${KUBECONFIG}"; printf '\n'
-64
View File
@@ -1,64 +0,0 @@
#!/bin/sh
set -eu
chart_version=10.10.5
project_dir=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
expected_context=kubernetes-admin@kubernetes
current_context=$(kubectl config current-context)
if [ "$current_context" != "$expected_context" ]; then
echo "Refusing mutation: expected context $expected_context, got $current_context" >&2
exit 1
fi
for command in helm kubectl openssl base64 curl; do
if ! command -v "$command" >/dev/null 2>&1; then
echo "Required command not found: $command" >&2
exit 1
fi
done
kubectl create namespace argocd --dry-run=client --output=yaml | kubectl apply --filename=-
if ! kubectl --namespace argocd get secret argocd-secret >/dev/null 2>&1; then
kubectl --namespace argocd create secret generic argocd-secret
fi
if [ -z "$(kubectl --namespace argocd get secret argocd-secret --output='jsonpath={.data.server\.secretkey}')" ]; then
secret_key=$(openssl rand -hex 32)
secret_key_base64=$(printf '%s' "$secret_key" | base64 | tr -d '\n')
secret_patch=$(mktemp)
chmod 600 "$secret_patch"
printf '{"data":{"server.secretkey":"%s"}}\n' "$secret_key_base64" > "$secret_patch"
kubectl --namespace argocd patch secret argocd-secret \
--type=merge --patch-file="$secret_patch" >/dev/null
rm -f "$secret_patch"
unset secret_key secret_key_base64
fi
kubectl --namespace argocd label secret argocd-secret \
app.kubernetes.io/name=argocd-secret \
app.kubernetes.io/part-of=argocd --overwrite >/dev/null
if ! kubectl --namespace argocd get secret argocd-notifications-secret >/dev/null 2>&1; then
kubectl --namespace argocd create secret generic argocd-notifications-secret
fi
if ! kubectl --namespace argocd get secret argocd-redis >/dev/null 2>&1; then
redis_password=$(openssl rand -hex 32)
kubectl --namespace argocd create secret generic argocd-redis \
--from-literal="auth=${redis_password}"
fi
if ! kubectl --namespace argocd get secret argocd-oidc >/dev/null 2>&1; then
echo "Missing argocd/argocd-oidc; run ../authentik/install.sh first." >&2
exit 1
fi
chart_archive=$(mktemp)
trap 'rm -f "$chart_archive"' EXIT HUP INT TERM
curl --fail --silent --show-error --location --output "$chart_archive" \
"https://github.com/argoproj/argo-helm/releases/download/argo-cd-${chart_version}/argo-cd-${chart_version}.tgz"
helm upgrade --install argocd "$chart_archive" \
--namespace argocd \
--version "$chart_version" \
--values "$project_dir/argocd-values.yaml" \
--wait \
--timeout 10m
-11
View File
@@ -1,11 +0,0 @@
#!/usr/bin/env bash
set -euo pipefail
project_dir=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)
version=${ARGOCD_CHART_VERSION:-10.10.5}
output=${1:?Usage: render.sh OUTPUT_FILE}
chart=$(mktemp)
trap 'rm -f "${chart}"' EXIT
curl --fail --silent --show-error --location --output "${chart}" \
"https://github.com/argoproj/argo-helm/releases/download/argo-cd-${version}/argo-cd-${version}.tgz"
helm template argocd "${chart}" --namespace argocd \
--values "${project_dir}/argocd-values.yaml" > "${output}"
-15
View File
@@ -1,15 +0,0 @@
#!/usr/bin/env bash
set -euo pipefail
project_dir=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)
workdir=$(mktemp -d); trap 'rm -rf "${workdir}"' EXIT
sh -n "${project_dir}/install.sh"
bash -n "${project_dir}/render.sh" "${project_dir}/create-ci-kubeconfig.sh" "${project_dir}/test.sh"
"${project_dir}/render.sh" "${workdir}/argocd.yaml"
if grep -Eq '^kind: Secret$' "${workdir}/argocd.yaml"; then
echo "Rendered Argo CD state must not contain Secrets" >&2; exit 1
fi
image=ghcr.io/yannh/kubeconform:v0.7.0
validate() { if command -v kubeconform >/dev/null; then kubeconform -strict -exit-on-error -summary "$@"; else local file=${!#}; podman run --rm -i "${image}" -strict -exit-on-error -summary "${@:1:$#-1}" < "$file"; fi; }
validate -ignore-missing-schemas "${workdir}/argocd.yaml"
validate "${project_dir}/ci-deployer.yaml"
echo "Argo CD chart and CI resources are valid."