From b83e27f27d2f047ddaad7508ea58cc97ea1c219c Mon Sep 17 00:00:00 2001 From: 0xFEEDC0DE64 Date: Sun, 18 Oct 2026 15:22:15 +0200 Subject: [PATCH] Let Argo CD manage its own deployment --- README.md | 15 +++++++ apps/argocd/.gitignore | 2 + apps/argocd/Chart.yaml | 9 +++++ apps/argocd/values.yaml | 40 +++++++++++++++++++ bootstrap/argocd-project.yaml | 39 ++++++++++++++++++ bootstrap/argocd.yaml | 23 +++++++++++ bootstrap/install-argocd.sh | 74 +++++++++++++++++++++++++++++++++++ bootstrap/install.sh | 6 +++ bootstrap/kustomization.yaml | 2 + 9 files changed, 210 insertions(+) create mode 100644 apps/argocd/.gitignore create mode 100644 apps/argocd/Chart.yaml create mode 100644 apps/argocd/values.yaml create mode 100644 bootstrap/argocd-project.yaml create mode 100644 bootstrap/argocd.yaml create mode 100755 bootstrap/install-argocd.sh diff --git a/README.md b/README.md index 868e060..31cdcc3 100644 --- a/README.md +++ b/README.md @@ -10,6 +10,8 @@ container images and then commits the selected image tag here; it never receives hostname, album selection, resources, and encrypted API credential. - `apps/metallb/` pins the upstream native MetalLB installation and owns this cluster's L2 address pool. +- `apps/argocd/` wraps the pinned upstream Argo CD chart with this cluster's + ingress, Authentik OIDC, RBAC, and secret-reference configuration. - `bootstrap/` contains Argo CD projects and Applications, including the upstream Sealed Secrets controller. - `scripts/update-immich-sync-image.sh` is the single image-promotion interface used by CI. @@ -29,6 +31,19 @@ shape; `apps/immichframe/values.yaml` owns only this cluster's choices. The cluster already runs Argo CD in the `argocd` namespace and exposes its UI at . +Argo CD manages its own chart after bootstrap. If the controller does not +exist, recover it first from the same canonical chart dependency and values, +then recreate the AppProjects and Applications: + +```sh +./bootstrap/install-argocd.sh +./bootstrap/install.sh +``` + +The recovery installer preserves `argocd-secret`, `argocd-oidc`, +`argocd-notifications-secret`, and `argocd-redis`. Back up or provision those +Secrets separately; none of their values belongs in this public repository. + 1. Push this repository so Argo CD can read its public HTTPS URL. 2. Configure the `immich-sync` repository's cross-repository token as described below. 3. Review the rendered workload: diff --git a/apps/argocd/.gitignore b/apps/argocd/.gitignore new file mode 100644 index 0000000..2946e34 --- /dev/null +++ b/apps/argocd/.gitignore @@ -0,0 +1,2 @@ +Chart.lock +charts/ diff --git a/apps/argocd/Chart.yaml b/apps/argocd/Chart.yaml new file mode 100644 index 0000000..d8b50bf --- /dev/null +++ b/apps/argocd/Chart.yaml @@ -0,0 +1,9 @@ +apiVersion: v2 +name: argocd +description: Self-managed Argo CD deployment for the homelab cluster +type: application +version: 0.1.0 +dependencies: +- name: argo-cd + version: 10.10.5 + repository: https://argoproj.github.io/argo-helm diff --git a/apps/argocd/values.yaml b/apps/argocd/values.yaml new file mode 100644 index 0000000..67b893b --- /dev/null +++ b/apps/argocd/values.yaml @@ -0,0 +1,40 @@ +argo-cd: + 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: + createSecret: false + params: + server.insecure: true + + notifications: + secret: + create: false + + redisSecretInit: + enabled: false + + server: + ingress: + enabled: true + annotations: + traefik.ingress.kubernetes.io/router.entrypoints: websecure diff --git a/bootstrap/argocd-project.yaml b/bootstrap/argocd-project.yaml new file mode 100644 index 0000000..16e87ab --- /dev/null +++ b/bootstrap/argocd-project.yaml @@ -0,0 +1,39 @@ +apiVersion: argoproj.io/v1alpha1 +kind: AppProject +metadata: + name: argocd + namespace: argocd +spec: + description: Argo CD self-management + sourceRepos: + - https://code.brunner.ninja/feedc0de/homelab-gitops.git + - https://argoproj.github.io/argo-helm + destinations: + - namespace: argocd + server: https://kubernetes.default.svc + clusterResourceWhitelist: + - group: apiextensions.k8s.io + kind: CustomResourceDefinition + - group: rbac.authorization.k8s.io + kind: ClusterRole + - group: rbac.authorization.k8s.io + kind: ClusterRoleBinding + namespaceResourceWhitelist: + - group: "" + kind: ConfigMap + - group: "" + kind: Service + - group: "" + kind: ServiceAccount + - group: apps + kind: Deployment + - group: apps + kind: StatefulSet + - group: networking.k8s.io + kind: Ingress + - group: networking.k8s.io + kind: NetworkPolicy + - group: rbac.authorization.k8s.io + kind: Role + - group: rbac.authorization.k8s.io + kind: RoleBinding diff --git a/bootstrap/argocd.yaml b/bootstrap/argocd.yaml new file mode 100644 index 0000000..2921540 --- /dev/null +++ b/bootstrap/argocd.yaml @@ -0,0 +1,23 @@ +apiVersion: argoproj.io/v1alpha1 +kind: Application +metadata: + name: argocd + namespace: argocd +spec: + project: argocd + source: + repoURL: https://code.brunner.ninja/feedc0de/homelab-gitops.git + targetRevision: main + path: apps/argocd + helm: + releaseName: argocd + destination: + server: https://kubernetes.default.svc + namespace: argocd + syncPolicy: + automated: + enabled: true + prune: false + selfHeal: true + syncOptions: + - ServerSideApply=true diff --git a/bootstrap/install-argocd.sh b/bootstrap/install-argocd.sh new file mode 100755 index 0000000..4d30f81 --- /dev/null +++ b/bootstrap/install-argocd.sh @@ -0,0 +1,74 @@ +#!/bin/sh +set -eu + +expected_context=kubernetes-admin@kubernetes +current_context=$(kubectl config current-context) +if [ "${current_context}" != "${expected_context}" ]; then + echo "Refusing recovery through unexpected context: ${current_context}" >&2 + exit 1 +fi + +script_dir=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd) +repository_dir=$(dirname -- "${script_dir}") +app_dir="${repository_dir}/apps/argocd" + +for command in helm kubectl openssl base64 ruby; 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}" + unset redis_password +fi +if ! kubectl --namespace argocd get secret argocd-oidc >/dev/null 2>&1; then + echo "Missing argocd/argocd-oidc; restore or provision it before recovery." >&2 + exit 1 +fi + +helm dependency update "${app_dir}" +set -- "${app_dir}"/charts/argo-cd-*.tgz +if [ "$#" -ne 1 ] || [ ! -f "$1" ]; then + echo "Expected exactly one downloaded argo-cd chart archive" >&2 + exit 1 +fi +chart_archive=$1 + +child_values=$(mktemp) +trap 'rm -f "${child_values}"' EXIT HUP INT TERM +ruby -ryaml -e 'puts YAML.dump(YAML.load_file(ARGV.fetch(0)).fetch("argo-cd"))' \ + "${app_dir}/values.yaml" > "${child_values}" + +helm upgrade --install argocd "${chart_archive}" \ + --namespace argocd \ + --values "${child_values}" \ + --wait \ + --timeout 10m diff --git a/bootstrap/install.sh b/bootstrap/install.sh index 50077d0..02e896b 100755 --- a/bootstrap/install.sh +++ b/bootstrap/install.sh @@ -35,3 +35,9 @@ kubectl --namespace argocd wait application/metallb \ kubectl --namespace argocd wait application/metallb \ --for=jsonpath='{.status.health.status}'=Healthy \ --timeout=5m +kubectl --namespace argocd wait application/argocd \ + --for=jsonpath='{.status.sync.status}'=Synced \ + --timeout=10m +kubectl --namespace argocd wait application/argocd \ + --for=jsonpath='{.status.health.status}'=Healthy \ + --timeout=10m diff --git a/bootstrap/kustomization.yaml b/bootstrap/kustomization.yaml index 81aa493..9490024 100644 --- a/bootstrap/kustomization.yaml +++ b/bootstrap/kustomization.yaml @@ -1,6 +1,8 @@ apiVersion: kustomize.config.k8s.io/v1beta1 kind: Kustomization resources: +- argocd-project.yaml +- argocd.yaml - project.yaml - platform-project.yaml - sealed-secrets.yaml