From c7bfd4953c6382459a433b2993440952f367f35f Mon Sep 17 00:00:00 2001 From: Felix Wolf Date: Mon, 30 Mar 2026 23:09:50 +0200 Subject: [PATCH] feat: Wire ArgoCD to Forgejo for GitOps management Configure myks with global repoURL pointing to Forgejo, in-cluster destination, and disabled placeholder cluster Secret. Implement App of Apps pattern with a root Application that syncs all child apps. Add argocd-deploy-key-init Job that generates an ed25519 SSH keypair, registers it as a deploy key via Forgejo API, and creates the ArgoCD repository secret with insecure host key verification (avoids chicken-and-egg with ArgoCD managing its own known hosts ConfigMap). Additional changes: - Ignore /status field diffs globally (K8s 1.32 compat) - Add Replace=true sync option on Jobs (immutable resource compat) - Switch job images from bitnami/kubectl to alpine/k8s - Update CLAUDE.md with ArgoCD status and no-bitnami rule Co-Authored-By: Claude Opus 4.6 (1M context) --- CLAUDE.md | 5 +- envs/_env/argocd/secret.overlay.ytt.yaml | 14 -- envs/env-data.ytt.yaml | 8 + prototypes/argocd/helm/argo-cd.yaml | 6 + .../forgejo/ytt/admin-secret-job.ytt.yaml | 4 +- .../ytt/argocd-deploy-key-job.ytt.yaml | 139 ++++++++++++++++++ rendered/argocd/production/app-argocd.yaml | 4 +- .../argocd/production/app-cert-manager.yaml | 4 +- rendered/argocd/production/app-forgejo.yaml | 2 +- rendered/argocd/production/app-traefik.yaml | 4 +- .../argocd/production/env-production.yaml | 17 +-- ...argo-cd-argocd-application-controller.yaml | 2 +- ...go-cd-argocd-notifications-controller.yaml | 2 +- .../clusterrole-argo-cd-argocd-server.yaml | 2 +- ...argo-cd-argocd-application-controller.yaml | 2 +- ...go-cd-argocd-notifications-controller.yaml | 2 +- ...sterrolebinding-argo-cd-argocd-server.yaml | 2 +- ...argo-cd-argocd-redis-health-configmap.yaml | 2 +- .../argocd/configmap-argocd-cm.yaml | 7 +- .../configmap-argocd-cmd-params-cm.yaml | 2 +- .../argocd/configmap-argocd-gpg-keys-cm.yaml | 2 +- .../configmap-argocd-notifications-cm.yaml | 2 +- .../argocd/configmap-argocd-rbac-cm.yaml | 2 +- .../configmap-argocd-ssh-known-hosts-cm.yaml | 2 +- .../argocd/configmap-argocd-tls-certs-cm.yaml | 2 +- ...cedefinition-applications.argoproj.io.yaml | 2 +- ...efinition-applicationsets.argoproj.io.yaml | 2 +- ...rcedefinition-appprojects.argoproj.io.yaml | 2 +- ...o-cd-argocd-applicationset-controller.yaml | 2 +- .../deployment-argo-cd-argocd-dex-server.yaml | 2 +- ...go-cd-argocd-notifications-controller.yaml | 2 +- .../deployment-argo-cd-argocd-redis.yaml | 2 +- ...deployment-argo-cd-argocd-repo-server.yaml | 4 +- .../deployment-argo-cd-argocd-server.yaml | 4 +- .../argocd/ingress-argo-cd-argocd-server.yaml | 2 +- .../job-argo-cd-argocd-redis-secret-init.yaml | 2 +- .../production/argocd/namespace-argocd.yaml | 2 +- ...argo-cd-argocd-application-controller.yaml | 2 +- ...o-cd-argocd-applicationset-controller.yaml | 2 +- .../role-argo-cd-argocd-dex-server.yaml | 2 +- ...go-cd-argocd-notifications-controller.yaml | 2 +- ...role-argo-cd-argocd-redis-secret-init.yaml | 2 +- .../role-argo-cd-argocd-repo-server.yaml | 2 +- .../argocd/role-argo-cd-argocd-server.yaml | 2 +- ...argo-cd-argocd-application-controller.yaml | 2 +- ...o-cd-argocd-applicationset-controller.yaml | 2 +- ...rolebinding-argo-cd-argocd-dex-server.yaml | 2 +- ...go-cd-argocd-notifications-controller.yaml | 2 +- ...ding-argo-cd-argocd-redis-secret-init.yaml | 2 +- ...olebinding-argo-cd-argocd-repo-server.yaml | 2 +- .../rolebinding-argo-cd-argocd-server.yaml | 2 +- .../secret-argocd-notifications-secret.yaml | 2 +- .../argocd/secret-argocd-secret.yaml | 2 +- ...o-cd-argocd-applicationset-controller.yaml | 2 +- .../service-argo-cd-argocd-dex-server.yaml | 2 +- .../argocd/service-argo-cd-argocd-redis.yaml | 2 +- .../service-argo-cd-argocd-repo-server.yaml | 2 +- .../argocd/service-argo-cd-argocd-server.yaml | 2 +- ...ount-argo-cd-argocd-redis-secret-init.yaml | 2 +- ...iceaccount-argo-cd-argocd-repo-server.yaml | 2 +- ...account-argocd-application-controller.yaml | 2 +- ...ount-argocd-applicationset-controller.yaml | 2 +- .../serviceaccount-argocd-dex-server.yaml | 2 +- ...count-argocd-notifications-controller.yaml | 2 +- .../argocd/serviceaccount-argocd-server.yaml | 2 +- ...argo-cd-argocd-application-controller.yaml | 4 +- .../clusterissuer-letsencrypt.yaml | 2 +- .../clusterrole-cert-manager-cainjector.yaml | 2 +- ...clusterrole-cert-manager-cluster-view.yaml | 2 +- ...er-controller-approve_cert-manager-io.yaml | 2 +- ...-cert-manager-controller-certificates.yaml | 2 +- ...controller-certificatesigningrequests.yaml | 2 +- ...le-cert-manager-controller-challenges.yaml | 2 +- ...ert-manager-controller-clusterissuers.yaml | 2 +- ...-cert-manager-controller-ingress-shim.yaml | 2 +- ...rrole-cert-manager-controller-issuers.yaml | 2 +- ...errole-cert-manager-controller-orders.yaml | 2 +- .../clusterrole-cert-manager-edit.yaml | 2 +- .../clusterrole-cert-manager-view.yaml | 2 +- ...-manager-webhook_subjectaccessreviews.yaml | 2 +- ...errolebinding-cert-manager-cainjector.yaml | 2 +- ...er-controller-approve_cert-manager-io.yaml | 2 +- ...-cert-manager-controller-certificates.yaml | 2 +- ...controller-certificatesigningrequests.yaml | 2 +- ...ng-cert-manager-controller-challenges.yaml | 2 +- ...ert-manager-controller-clusterissuers.yaml | 2 +- ...-cert-manager-controller-ingress-shim.yaml | 2 +- ...nding-cert-manager-controller-issuers.yaml | 2 +- ...inding-cert-manager-controller-orders.yaml | 2 +- ...-manager-webhook_subjectaccessreviews.yaml | 2 +- ...n-certificaterequests.cert-manager.io.yaml | 2 +- ...finition-certificates.cert-manager.io.yaml | 2 +- ...ition-challenges.acme.cert-manager.io.yaml | 2 +- ...nition-clusterissuers.cert-manager.io.yaml | 2 +- ...rcedefinition-issuers.cert-manager.io.yaml | 2 +- ...efinition-orders.acme.cert-manager.io.yaml | 2 +- .../deployment-cert-manager-cainjector.yaml | 2 +- .../deployment-cert-manager-webhook.yaml | 2 +- .../cert-manager/deployment-cert-manager.yaml | 2 +- .../job-cert-manager-startupapicheck.yaml | 2 +- ...ookconfiguration-cert-manager-webhook.yaml | 2 +- .../cert-manager/namespace-cert-manager.yaml | 2 +- ...ert-manager-cainjector_leaderelection.yaml | 2 +- ...t-manager-startupapicheck_create-cert.yaml | 2 +- .../role-cert-manager-tokenrequest.yaml | 2 +- ...-cert-manager-webhook_dynamic-serving.yaml | 2 +- .../role-cert-manager_leaderelection.yaml | 2 +- ...ert-manager-cainjector_leaderelection.yaml | 2 +- ...ert-manager-cert-manager-tokenrequest.yaml | 2 +- ...t-manager-startupapicheck_create-cert.yaml | 2 +- ...-cert-manager-webhook_dynamic-serving.yaml | 2 +- ...lebinding-cert-manager_leaderelection.yaml | 2 +- .../service-cert-manager-cainjector.yaml | 2 +- .../service-cert-manager-webhook.yaml | 2 +- .../cert-manager/service-cert-manager.yaml | 2 +- ...erviceaccount-cert-manager-cainjector.yaml | 2 +- ...eaccount-cert-manager-startupapicheck.yaml | 2 +- .../serviceaccount-cert-manager-webhook.yaml | 2 +- .../serviceaccount-cert-manager.yaml | 2 +- ...ookconfiguration-cert-manager-webhook.yaml | 2 +- .../clusterrole-argocd-deploy-key-init.yaml | 15 ++ ...terrolebinding-argocd-deploy-key-init.yaml | 15 ++ .../forgejo/job-argocd-deploy-key-init.yaml | 104 +++++++++++++ .../job-forgejo-admin-secret-init.yaml | 3 +- ...serviceaccount-argocd-deploy-key-init.yaml | 7 + .../traefik/clusterrole-traefik-traefik.yaml | 2 +- .../clusterrolebinding-traefik-traefik.yaml | 2 +- ...-accesscontrolpolicies.hub.traefik.io.yaml | 2 +- ...edefinition-aiservices.hub.traefik.io.yaml | 2 +- ...rcedefinition-apiauths.hub.traefik.io.yaml | 2 +- ...edefinition-apibundles.hub.traefik.io.yaml | 2 +- ...nition-apicatalogitems.hub.traefik.io.yaml | 2 +- ...rcedefinition-apiplans.hub.traefik.io.yaml | 2 +- ...inition-apiportalauths.hub.traefik.io.yaml | 2 +- ...edefinition-apiportals.hub.traefik.io.yaml | 2 +- ...finition-apiratelimits.hub.traefik.io.yaml | 2 +- ...esourcedefinition-apis.hub.traefik.io.yaml | 2 +- ...definition-apiversions.hub.traefik.io.yaml | 2 +- ...tlspolicies.gateway.networking.k8s.io.yaml | 2 +- ...ewayclasses.gateway.networking.k8s.io.yaml | 2 +- ...on-gateways.gateway.networking.k8s.io.yaml | 2 +- ...-grpcroutes.gateway.networking.k8s.io.yaml | 2 +- ...-httproutes.gateway.networking.k8s.io.yaml | 2 +- ...cedefinition-ingressroutes.traefik.io.yaml | 2 +- ...efinition-ingressroutetcps.traefik.io.yaml | 2 +- ...efinition-ingressrouteudps.traefik.io.yaml | 2 +- ...on-managedapplications.hub.traefik.io.yaml | 2 +- ...n-managedsubscriptions.hub.traefik.io.yaml | 2 +- ...urcedefinition-middlewares.traefik.io.yaml | 2 +- ...edefinition-middlewaretcps.traefik.io.yaml | 2 +- ...rencegrants.gateway.networking.k8s.io.yaml | 2 +- ...finition-serverstransports.traefik.io.yaml | 2 +- ...ition-serverstransporttcps.traefik.io.yaml | 2 +- ...ourcedefinition-tlsoptions.traefik.io.yaml | 2 +- ...sourcedefinition-tlsstores.traefik.io.yaml | 2 +- ...definition-traefikservices.traefik.io.yaml | 2 +- .../production/traefik/daemonset-traefik.yaml | 2 +- .../traefik/ingressclass-traefik.yaml | 2 +- .../production/traefik/namespace-traefik.yaml | 2 +- .../production/traefik/service-traefik.yaml | 2 +- .../traefik/serviceaccount-traefik.yaml | 2 +- 161 files changed, 464 insertions(+), 188 deletions(-) delete mode 100644 envs/_env/argocd/secret.overlay.ytt.yaml create mode 100644 prototypes/forgejo/ytt/argocd-deploy-key-job.ytt.yaml create mode 100644 rendered/envs/production/forgejo/clusterrole-argocd-deploy-key-init.yaml create mode 100644 rendered/envs/production/forgejo/clusterrolebinding-argocd-deploy-key-init.yaml create mode 100644 rendered/envs/production/forgejo/job-argocd-deploy-key-init.yaml create mode 100644 rendered/envs/production/forgejo/serviceaccount-argocd-deploy-key-init.yaml diff --git a/CLAUDE.md b/CLAUDE.md index 07381ce..213c3a4 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -72,7 +72,10 @@ export TALOSCONFIG=./talos/talosconfig - **Namespace race condition**: First `kubectl apply` of a new app often fails because namespace isn't ready. Re-apply once. - **Traefik DaemonSet updates**: Requires `updateStrategy.rollingUpdate.maxSurge: 0` because hostPort conflicts prevent surge. - **Forgejo Ingress API version**: Chart renders `extensions/v1beta1`, fixed via `ytt/ingress-fix.ytt.yaml` overlay to `networking.k8s.io/v1`. -- **ArgoCD Phase 3**: Repo not yet pushed to Forgejo, ArgoCD not yet wired. +- **ArgoCD**: Fully wired to Forgejo via App of Apps. Root Application in `default` project syncs `rendered/argocd/production/`. Deploy key provisioned automatically by `argocd-deploy-key-init` Job in forgejo namespace. + +## Container Images +- **Never use bitnami images.** Use `alpine/k8s` or plain `alpine` for utility Jobs instead. ## Secrets (not in git) - `cert-manager/letsencrypt-account-key` — ACME account key (auto-generated) diff --git a/envs/_env/argocd/secret.overlay.ytt.yaml b/envs/_env/argocd/secret.overlay.ytt.yaml deleted file mode 100644 index bcd1cb2..0000000 --- a/envs/_env/argocd/secret.overlay.ytt.yaml +++ /dev/null @@ -1,14 +0,0 @@ -#@ load("@ytt:overlay", "overlay") ---- -#@ def secret_fragment(): -kind: Secret -metadata: - labels: - argocd.argoproj.io/secret-type: cluster -#@ end - -#@overlay/match by=overlay.subset(secret_fragment()), expects="0+" ---- -stringData: - config: ARGOCD_CLUSTER_CONNECT_CONFIG - server: ARGOCD_CLUSTER_SERVER_URL diff --git a/envs/env-data.ytt.yaml b/envs/env-data.ytt.yaml index e8bb4e0..33eee95 100644 --- a/envs/env-data.ytt.yaml +++ b/envs/env-data.ytt.yaml @@ -5,5 +5,13 @@ argocd: app: prefix: app- finalizers: [] + source: + repoURL: ssh://git@git.tr1ceracop.de:222/gitea_admin/k8s-and-chill.git + destination: + server: https://kubernetes.default.svc project: prefix: env- + destination: + server: https://kubernetes.default.svc + env: + generateSecret: false diff --git a/prototypes/argocd/helm/argo-cd.yaml b/prototypes/argocd/helm/argo-cd.yaml index 439f20f..0960723 100644 --- a/prototypes/argocd/helm/argo-cd.yaml +++ b/prototypes/argocd/helm/argo-cd.yaml @@ -9,6 +9,12 @@ global: configs: params: server.insecure: true + cm: + resource.customizations.ignoreDifferences.all: | + managedFieldsManagers: + - kube-controller-manager + jsonPointers: + - /status server: ingress: diff --git a/prototypes/forgejo/ytt/admin-secret-job.ytt.yaml b/prototypes/forgejo/ytt/admin-secret-job.ytt.yaml index ceede1d..2b8a02a 100644 --- a/prototypes/forgejo/ytt/admin-secret-job.ytt.yaml +++ b/prototypes/forgejo/ytt/admin-secret-job.ytt.yaml @@ -41,6 +41,8 @@ kind: Job metadata: name: forgejo-admin-secret-init namespace: #@ ns + annotations: + argocd.argoproj.io/sync-options: Replace=true spec: ttlSecondsAfterFinished: 300 template: @@ -49,7 +51,7 @@ spec: restartPolicy: OnFailure containers: - name: init - image: bitnami/kubectl:latest + image: alpine/k8s:1.32.3 command: - sh - -c diff --git a/prototypes/forgejo/ytt/argocd-deploy-key-job.ytt.yaml b/prototypes/forgejo/ytt/argocd-deploy-key-job.ytt.yaml new file mode 100644 index 0000000..18014d6 --- /dev/null +++ b/prototypes/forgejo/ytt/argocd-deploy-key-job.ytt.yaml @@ -0,0 +1,139 @@ +#@ load("@ytt:data", "data") + +#@ ns = data.values.application.namespace + +--- +apiVersion: v1 +kind: ServiceAccount +metadata: + name: argocd-deploy-key-init + namespace: #@ ns + +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: argocd-deploy-key-init +rules: + - apiGroups: [""] + resources: ["secrets"] + verbs: ["get", "create"] + +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: argocd-deploy-key-init +subjects: + - kind: ServiceAccount + name: argocd-deploy-key-init + namespace: #@ ns +roleRef: + kind: ClusterRole + name: argocd-deploy-key-init + apiGroup: rbac.authorization.k8s.io + +--- +apiVersion: batch/v1 +kind: Job +metadata: + name: argocd-deploy-key-init + namespace: #@ ns + annotations: + argocd.argoproj.io/sync-options: Replace=true +spec: + ttlSecondsAfterFinished: 300 + template: + spec: + serviceAccountName: argocd-deploy-key-init + restartPolicy: OnFailure + containers: + - name: init + image: alpine/k8s:1.32.3 + command: + - sh + - -c + - | + set -e + + apk add --no-cache openssh-keygen > /dev/null 2>&1 + + ARGOCD_NS="argocd" + REPO_SECRET="forgejo-repo" + REPO_URL="ssh://git@git.tr1ceracop.de:222/gitea_admin/k8s-and-chill.git" + FORGEJO_URL="https://git.tr1ceracop.de" + REPO_OWNER="gitea_admin" + REPO_NAME="k8s-and-chill" + + # Wait for Forgejo to be ready + echo "Waiting for Forgejo to be ready..." + for i in $(seq 1 60); do + if curl -sk "${FORGEJO_URL}/api/v1/version" >/dev/null 2>&1; then + echo "Forgejo HTTPS is ready" + break + fi + if [ "$i" -eq 60 ]; then + echo "Forgejo did not become ready in time" + exit 1 + fi + sleep 5 + done + + # Check if ArgoCD repo secret already exists + if kubectl get secret "${REPO_SECRET}" -n "${ARGOCD_NS}" >/dev/null 2>&1; then + echo "Secret ${REPO_SECRET} already exists in ${ARGOCD_NS}, skipping" + exit 0 + fi + + # Read admin credentials + ADMIN_USER=$(kubectl get secret forgejo-admin-secret -n "${NAMESPACE}" -o jsonpath='{.data.username}' | base64 -d) + ADMIN_PASS=$(kubectl get secret forgejo-admin-secret -n "${NAMESPACE}" -o jsonpath='{.data.password}' | base64 -d) + + # Generate ed25519 SSH keypair + KEYDIR=$(mktemp -d) + ssh-keygen -t ed25519 -f "${KEYDIR}/id_ed25519" -N "" -q + PRIVKEY=$(cat "${KEYDIR}/id_ed25519") + PUBKEY=$(cat "${KEYDIR}/id_ed25519.pub") + rm -rf "${KEYDIR}" + + # Register deploy key via Forgejo API + echo "Registering deploy key..." + HTTP_CODE=$(curl -sk -o /tmp/response.json -w "%{http_code}" \ + -X POST "${FORGEJO_URL}/api/v1/repos/${REPO_OWNER}/${REPO_NAME}/keys" \ + -H "Content-Type: application/json" \ + -u "${ADMIN_USER}:${ADMIN_PASS}" \ + -d "{\"title\":\"argocd-deploy-key\",\"key\":\"${PUBKEY}\",\"read_only\":true}") + + if [ "${HTTP_CODE}" = "201" ]; then + echo "Deploy key registered successfully" + elif [ "${HTTP_CODE}" = "422" ]; then + echo "Deploy key already exists in Forgejo (422), continuing" + else + echo "Failed to register deploy key: HTTP ${HTTP_CODE}" + cat /tmp/response.json + exit 1 + fi + + # Create ArgoCD repository secret with insecure flag (skip host key verification) + cat < /dev/null 2>&1 + + ARGOCD_NS="argocd" + REPO_SECRET="forgejo-repo" + REPO_URL="ssh://git@git.tr1ceracop.de:222/gitea_admin/k8s-and-chill.git" + FORGEJO_URL="https://git.tr1ceracop.de" + REPO_OWNER="gitea_admin" + REPO_NAME="k8s-and-chill" + + # Wait for Forgejo to be ready + echo "Waiting for Forgejo to be ready..." + for i in $(seq 1 60); do + if curl -sk "${FORGEJO_URL}/api/v1/version" >/dev/null 2>&1; then + echo "Forgejo HTTPS is ready" + break + fi + if [ "$i" -eq 60 ]; then + echo "Forgejo did not become ready in time" + exit 1 + fi + sleep 5 + done + + # Check if ArgoCD repo secret already exists + if kubectl get secret "${REPO_SECRET}" -n "${ARGOCD_NS}" >/dev/null 2>&1; then + echo "Secret ${REPO_SECRET} already exists in ${ARGOCD_NS}, skipping" + exit 0 + fi + + # Read admin credentials + ADMIN_USER=$(kubectl get secret forgejo-admin-secret -n "${NAMESPACE}" -o jsonpath='{.data.username}' | base64 -d) + ADMIN_PASS=$(kubectl get secret forgejo-admin-secret -n "${NAMESPACE}" -o jsonpath='{.data.password}' | base64 -d) + + # Generate ed25519 SSH keypair + KEYDIR=$(mktemp -d) + ssh-keygen -t ed25519 -f "${KEYDIR}/id_ed25519" -N "" -q + PRIVKEY=$(cat "${KEYDIR}/id_ed25519") + PUBKEY=$(cat "${KEYDIR}/id_ed25519.pub") + rm -rf "${KEYDIR}" + + # Register deploy key via Forgejo API + echo "Registering deploy key..." + HTTP_CODE=$(curl -sk -o /tmp/response.json -w "%{http_code}" \ + -X POST "${FORGEJO_URL}/api/v1/repos/${REPO_OWNER}/${REPO_NAME}/keys" \ + -H "Content-Type: application/json" \ + -u "${ADMIN_USER}:${ADMIN_PASS}" \ + -d "{\"title\":\"argocd-deploy-key\",\"key\":\"${PUBKEY}\",\"read_only\":true}") + + if [ "${HTTP_CODE}" = "201" ]; then + echo "Deploy key registered successfully" + elif [ "${HTTP_CODE}" = "422" ]; then + echo "Deploy key already exists in Forgejo (422), continuing" + else + echo "Failed to register deploy key: HTTP ${HTTP_CODE}" + cat /tmp/response.json + exit 1 + fi + + # Create ArgoCD repository secret with insecure flag (skip host key verification) + cat <