diff --git a/.github/actionlint.yaml b/.github/actionlint.yaml new file mode 100644 index 0000000..80ea56c --- /dev/null +++ b/.github/actionlint.yaml @@ -0,0 +1,3 @@ +self-hosted-runner: + labels: + - arc-tf diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index eaec11d..2115bdb 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -17,12 +17,12 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - name: Setup Python - uses: actions/setup-python@v5 + uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.12" - name: pre-commit tests - uses: pre-commit/action@v3.0.1 + uses: pre-commit/action@2c7b3805fd2a0fd8c1884dcaebf91fc102a13ecd # v3.0.1 diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 3dc6e98..90ab3f1 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,11 +1,11 @@ repos: - repo: https://github.com/compilerla/conventional-pre-commit - rev: v4.4.0 + rev: 91ab4bf57e58b32adf1a122681f6ebe164d081c8 # v4.4.0 hooks: - id: conventional-pre-commit stages: [commit-msg] - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v6.0.0 + rev: 3e8a8703264a2f4a69428a0aa4dcb512790b2c8c # v6.0.0 hooks: - id: check-case-conflict - id: check-merge-conflict @@ -20,12 +20,12 @@ repos: - id: mixed-line-ending - id: trailing-whitespace - repo: https://github.com/gitleaks/gitleaks - rev: v8.30.1 + rev: 83d9cd684c87d95d656c1458ef04895a7f1cbd8e # v8.30.1 hooks: - id: gitleaks entry: gitleaks dir --redact --verbose . pass_filenames: false - repo: https://github.com/stackrox/kube-linter - rev: v0.8.3 + rev: 10ae003038c81855aca8489df5e35da150f4dc2e # v0.8.3 hooks: - id: kube-linter diff --git a/.sops.yaml b/.sops.yaml index 00b1214..fb2bc8a 100644 --- a/.sops.yaml +++ b/.sops.yaml @@ -1,6 +1,6 @@ --- creation_rules: - # Default: encrypt only common secret fields + # Encrypt credential fields only so metadata remains reviewable. - age: age152ek83tm4fj5u70r3fecytn4kg7c5xca24erjchxexx4pfqg6das7q763l encrypted_regex: '^(token|api-token|apiToken|clientID|clientSecret|client_id|client_secret|password|secret|github_token|CLOUDFLARE_API_TOKEN|credentials\.json|.*_SERVICE_KEY|GF_AUTH_(GITHUB|GENERIC_OAUTH)_CLIENT_SECRET|GF_SECURITY_ADMIN_PASSWORD|dex\.[a-z]+\.clientID|dex\.[a-z]+\.clientSecret|OIDC_CLIENT_(ID|SECRET))$' diff --git a/README.md b/README.md index 201092e..6f6ad2a 100644 --- a/README.md +++ b/README.md @@ -15,13 +15,16 @@ The root `kustomization.yaml` is for local `kustomize build` testing only. ArgoC ## Sync Wave Flow ``` -Wave 0: ArgoCD configuration, RBAC, CI service account -Wave 1: bootstrap-secrets and gitops-operators Applications -Wave 2: gitops-workloads Application -PostSync: ci-token-sync, wait-for-* jobs +Bootstrap apply: ArgoCD configuration, RBAC, CI, and independent root Applications +gitops-operators: installs operator controllers and CRDs +gitops-workloads PreSync: wait-for-crds blocks until required CRDs exist +gitops-workloads Sync: creates child Applications and direct workload CRs ``` -Sync waves order resources within a single Application — they are not global across Applications. Cross-Application ordering is enforced by the App-of-Apps structure and `wait-for-*` post-sync jobs. +The `bootstrap-secrets`, `gitops-operators`, and `gitops-workloads` root +Applications reconcile independently. Sync waves are local to each Application; +the `gitops-workloads` `PreSync` hook gates its child Application and direct CR +creation until every workload-required operator CRD is available. ## External Traffic diff --git a/bootstrap/argocd-config.yaml b/bootstrap/argocd-config.yaml index 5e0766d..5cc062f 100644 --- a/bootstrap/argocd-config.yaml +++ b/bootstrap/argocd-config.yaml @@ -1,8 +1,4 @@ --- -# ArgoCD instance managed by argocd-operator (community). -# - KSOPS for SOPS-encrypted secrets using AGE keys -# - GitHub OAuth authentication via Dex -# - RBAC with GitHub org/team mapping apiVersion: argoproj.io/v1beta1 kind: ArgoCD metadata: @@ -11,9 +7,12 @@ metadata: annotations: argocd.argoproj.io/sync-wave: "0" spec: - # Disable local admin account - use GitHub SSO instead + # Interactive access must use GitHub SSO. disableAdmin: true - # Server configuration + # API-only account; RBAC below restricts it to Application operations. + extraConfig: + accounts.mcp: apiKey + # CPU limits are omitted to avoid throttling on the single-node cluster. server: host: argocd.makeitwork.cloud resources: @@ -22,7 +21,6 @@ spec: memory: 128Mi limits: memory: 256Mi - # Controller resources (no CPU limits to prevent throttling) controller: resources: requests: @@ -30,7 +28,6 @@ spec: memory: 1Gi limits: memory: 2Gi - # ApplicationSet controller resources applicationSet: resources: requests: @@ -38,7 +35,6 @@ spec: memory: 512Mi limits: memory: 1Gi - # Redis resources redis: resources: requests: @@ -46,7 +42,6 @@ spec: memory: 128Mi limits: memory: 256Mi - # KSOPS configuration kustomizeBuildOptions: "--enable-alpha-plugins --enable-exec" repo: resources: @@ -67,7 +62,6 @@ spec: secret: secretName: sops-age-keys initContainers: - # Use alpine to download and install KSOPS binaries - name: install-ksops image: docker.io/alpine:3.23 command: ["/bin/sh", "-c"] @@ -93,7 +87,6 @@ spec: - mountPath: /.config/sops/age/key.txt name: sops-age-keys subPath: key.txt - # GitHub OAuth via Dex (SSO) sso: provider: dex dex: @@ -132,12 +125,12 @@ spec: secret: $dex.forgejo.clientSecret redirectURIs: - https://forgejo.makeitwork.cloud/user/oauth2/dex/callback - # RBAC with GitHub team mapping rbac: defaultPolicy: '' policy: | - # Admin access for admins team g, makeitworkcloud:admins, role:admin - # Read-only access for developers team g, makeitworkcloud:developers, role:readonly + # MCP has no access to projects, repositories, clusters, or accounts. + p, mcp, applications, get, */*, allow + p, mcp, applications, sync, */*, allow scopes: '[groups]' diff --git a/bootstrap/ci-service-account.yaml b/bootstrap/ci-service-account.yaml index 1bf63d9..ca99284 100644 --- a/bootstrap/ci-service-account.yaml +++ b/bootstrap/ci-service-account.yaml @@ -1,6 +1,4 @@ --- -# Service account for CI/CD pipelines (GitHub Actions) -# Token is created as a long-lived secret for headless authentication apiVersion: v1 kind: ServiceAccount metadata: @@ -9,8 +7,8 @@ metadata: annotations: argocd.argoproj.io/sync-wave: "0" --- -# Long-lived API token for CI/CD -# Extract with: kubectl get secret ci-deployer-token -n argocd -o jsonpath='{.data.token}' | base64 -d +# External GitHub Actions cannot use projected pod tokens, so this Secret +# provides the long-lived credential synchronized to approved repositories. apiVersion: v1 kind: Secret metadata: @@ -21,7 +19,8 @@ metadata: kubernetes.io/service-account.name: ci-deployer type: kubernetes.io/service-account-token --- -# Grant cluster-admin to CI service account +# Security boundary: this token is cluster-admin and must be distributed only +# to the approved CI repositories. apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRoleBinding metadata: diff --git a/bootstrap/ci-token-sync-job.yaml b/bootstrap/ci-token-sync-job.yaml index c2041ed..56675e7 100644 --- a/bootstrap/ci-token-sync-job.yaml +++ b/bootstrap/ci-token-sync-job.yaml @@ -53,7 +53,6 @@ spec: # (NOT the auto-mounted bound token which expires in 1 hour) SA_TOKEN=$(cat /var/run/secrets/ci-deployer/token) - # Repos that need the ci-deployer cluster API token REPOS=( "makeitworkcloud/images" "makeitworkcloud/kustomize-cluster" diff --git a/bootstrap/kustomization.yaml b/bootstrap/kustomization.yaml index bc02a8f..209419c 100644 --- a/bootstrap/kustomization.yaml +++ b/bootstrap/kustomization.yaml @@ -2,21 +2,11 @@ apiVersion: kustomize.config.k8s.io/v1beta1 kind: Kustomization resources: - # Wave 0: Configure ArgoCD (KSOPS + GitHub OAuth + RBAC) - # NOTE: cluster-admin ClusterRoleBinding is managed by ansible-role-crc - # (ArgoCD cannot grant itself permissions it doesn't have) - argocd-config.yaml - # Wave 0: GitHub-team-to-cluster-admin RBAC for kubectl OIDC users - oidc-rbac.yaml - # Wave 0: CI/CD service account with cluster-admin for GitHub Actions - ci-service-account.yaml - # PostSync: Sync ci-deployer token to GitHub Actions secrets - ci-token-sync-job.yaml - wait-for-repo-server.yaml - # Wave 1: Bootstrap secrets (KSOPS-dependent) - bootstrap-secrets-app.yaml - # Wave 1: Deploy operators (install CRDs) - operators-app.yaml - - wait-for-crds.yaml - # Wave 2: Deploy workloads (CRs that depend on CRDs) - workloads-app.yaml diff --git a/bootstrap/secrets/ci-token-sync-secret.yaml b/bootstrap/secrets/ci-token-sync-secret.yaml index ab3e23d..2e2fbd7 100644 --- a/bootstrap/secrets/ci-token-sync-secret.yaml +++ b/bootstrap/secrets/ci-token-sync-secret.yaml @@ -1,4 +1,3 @@ -# GitHub PAT for CI token sync job apiVersion: v1 kind: Secret metadata: diff --git a/bootstrap/secrets/github-oauth-secret.yaml b/bootstrap/secrets/github-oauth-secret.yaml index 29cf8b2..7852845 100644 --- a/bootstrap/secrets/github-oauth-secret.yaml +++ b/bootstrap/secrets/github-oauth-secret.yaml @@ -1,4 +1,3 @@ -# GitHub OAuth credentials for ArgoCD Dex connector # This secret must be labeled to be read by ArgoCD apiVersion: v1 kind: Secret diff --git a/bootstrap/wait-for-crds.yaml b/bootstrap/wait-for-crds.yaml deleted file mode 100644 index c01f03e..0000000 --- a/bootstrap/wait-for-crds.yaml +++ /dev/null @@ -1,73 +0,0 @@ ---- -# PostSync hook to wait for operator CRDs before workloads sync -# Runs after operators app syncs, before workloads app syncs -apiVersion: batch/v1 -kind: Job -metadata: - name: wait-for-crds - namespace: argocd - annotations: - argocd.argoproj.io/sync-wave: "1" - argocd.argoproj.io/hook: PostSync - argocd.argoproj.io/hook-delete-policy: HookSucceeded - ignore-check.kube-linter.io/non-existent-service-account: "SA created by GitOps operator" - ignore-check.kube-linter.io/latest-tag: "bitnami/kubectl:latest is acceptable for a one-shot bootstrap wait Job" -spec: - ttlSecondsAfterFinished: 300 - backoffLimit: 30 - activeDeadlineSeconds: 900 - template: - spec: - serviceAccountName: argocd-argocd-application-controller - restartPolicy: Never - securityContext: - runAsNonRoot: true - seccompProfile: - type: RuntimeDefault - containers: - - name: wait - image: bitnami/kubectl:latest - command: - - /bin/bash - - -c - - | - set -e - echo "Waiting for operator CRDs..." - - # Wait for ARC CRDs - echo "Checking for ARC CRDs..." - until kubectl get crd runnerdeployments.actions.summerwind.dev 2>/dev/null; do - echo "Waiting for runnerdeployments.actions.summerwind.dev CRD..." - sleep 10 - done - echo "ARC CRDs ready" - - # Wait for Cloudflare Operator CRDs - echo "Checking for Cloudflare Operator CRDs..." - until kubectl get crd clustertunnels.networking.cfargotunnel.com 2>/dev/null; do - echo "Waiting for clustertunnels.networking.cfargotunnel.com CRD..." - sleep 10 - done - echo "Cloudflare Operator CRDs ready" - - # Wait for Tor Controller CRDs - echo "Checking for Tor Controller CRDs..." - until kubectl get crd onionservices.tor.k8s.torproject.org 2>/dev/null; do - echo "Waiting for onionservices.tor.k8s.torproject.org CRD..." - sleep 10 - done - echo "Tor Controller CRDs ready" - - echo "All operator CRDs are available" - securityContext: - allowPrivilegeEscalation: false - readOnlyRootFilesystem: true - capabilities: - drop: - - ALL - resources: - requests: - cpu: 10m - memory: 64Mi - limits: - memory: 128Mi diff --git a/opencode.json b/opencode.json index f9d1c96..5345b95 100644 --- a/opencode.json +++ b/opencode.json @@ -16,6 +16,7 @@ "aws-prod": {"enabled": false}, "grafana": {"enabled": false}, "terraform-docs": {"enabled": false}, + "argocd-makeitwork": {"enabled": true}, "argocd-staging-eks": {"enabled": false}, "argocd-prod-eks": {"enabled": false} }, @@ -30,6 +31,7 @@ "aws-prod_*": false, "grafana_*": false, "terraform-docs_*": false, + "argocd-makeitwork_*": true, "argocd-staging-eks_*": false, "argocd-prod-eks_*": false } diff --git a/operators/cert-manager/cloudflare-api-token-secret.yaml b/operators/cert-manager/cloudflare-api-token-secret.yaml index 47334e8..94688e1 100644 --- a/operators/cert-manager/cloudflare-api-token-secret.yaml +++ b/operators/cert-manager/cloudflare-api-token-secret.yaml @@ -1,4 +1,3 @@ -# Cloudflare API token for cert-manager DNS-01 challenges apiVersion: v1 kind: Secret metadata: diff --git a/operators/cert-manager/cluster-issuer.yaml b/operators/cert-manager/cluster-issuer.yaml index ba403fe..f80cd08 100644 --- a/operators/cert-manager/cluster-issuer.yaml +++ b/operators/cert-manager/cluster-issuer.yaml @@ -1,6 +1,4 @@ --- -# ClusterIssuer for Let's Encrypt with Cloudflare DNS-01 challenge -# Used to issue certificates for *.makeitwork.cloud domains apiVersion: cert-manager.io/v1 kind: ClusterIssuer metadata: @@ -9,11 +7,8 @@ metadata: argocd.argoproj.io/sync-wave: "2" spec: acme: - # Production Let's Encrypt server server: https://acme-v02.api.letsencrypt.org/directory - # Email for certificate expiry notifications email: admin@makeitwork.cloud - # Secret to store ACME account private key privateKeySecretRef: name: letsencrypt-cloudflare-account-key solvers: diff --git a/operators/cloudflare/cluster-tunnel.yaml b/operators/cloudflare/cluster-tunnel.yaml index f0a5fe2..2db6708 100644 --- a/operators/cloudflare/cluster-tunnel.yaml +++ b/operators/cloudflare/cluster-tunnel.yaml @@ -13,9 +13,7 @@ spec: domain: makeitwork.cloud secret: cloudflare-api-credentials CLOUDFLARE_API_TOKEN: CLOUDFLARE_API_TOKEN - # Default fallback for unmatched requests fallbackTarget: http_status:404 - # Patch the cloudflared deployment deployPatch: | spec: template: @@ -23,7 +21,6 @@ spec: containers: - name: cloudflared image: docker.io/cloudflare/cloudflared:latest - # Increase probe delays to allow tunnel connection time livenessProbe: initialDelaySeconds: 30 periodSeconds: 30 diff --git a/operators/cloudflare/kustomization.yaml b/operators/cloudflare/kustomization.yaml index 859a933..0a7fc6b 100644 --- a/operators/cloudflare/kustomization.yaml +++ b/operators/cloudflare/kustomization.yaml @@ -2,9 +2,7 @@ apiVersion: kustomize.config.k8s.io/v1beta1 kind: Kustomization resources: - # Operator from upstream GitHub release - https://github.com/adyanth/cloudflare-operator.git/config/default?ref=v0.13.1 - # ClusterTunnel CR (cloudflared deployment created by operator) - cluster-tunnel.yaml generators: - ksops-cloudflare-secrets.yaml @@ -12,7 +10,7 @@ images: - name: adyanth/cloudflare-operator newName: docker.io/adyanth/cloudflare-operator patches: - # Remove container resource requests/limits from operator deployment + # Omit resource constraints on the single-node cluster. - patch: | - op: remove path: /spec/template/spec/containers/0/resources diff --git a/operators/tor-controller/kustomization.yaml b/operators/tor-controller/kustomization.yaml index 72f0518..2fdab13 100644 --- a/operators/tor-controller/kustomization.yaml +++ b/operators/tor-controller/kustomization.yaml @@ -3,10 +3,8 @@ apiVersion: kustomize.config.k8s.io/v1beta1 kind: Kustomization resources: - # Operator from upstream GitHub - https://github.com/bugfest/tor-controller.git/config/default?ref=tor-controller-0.1.17 -# Override namespace from tor-controller-system to tor-controller namespace: tor-controller # Upstream pins gcr.io/kubebuilder/kube-rbac-proxy which Google removed; re-point @@ -17,14 +15,14 @@ images: newTag: v0.18.0 patches: - # Fix namespace references in ClusterRoleBinding + # The upstream binding still names tor-controller-system after the override. - patch: | - op: replace path: /subjects/0/namespace value: tor-controller target: kind: ClusterRoleBinding - # Remove container resource requests/limits on single-node cluster + # Omit resource constraints on the single-node cluster. - patch: | - op: remove path: /spec/template/spec/containers/0/resources diff --git a/workloads/apps/kustomization.yaml b/workloads/apps/kustomization.yaml index 242e985..46159ed 100644 --- a/workloads/apps/kustomization.yaml +++ b/workloads/apps/kustomization.yaml @@ -1,14 +1,12 @@ --- -# Workload Applications - each workload is deployed as a separate ArgoCD Application apiVersion: kustomize.config.k8s.io/v1beta1 kind: Kustomization resources: - # Wave 0: No CRD dependencies + - wait-for-crds.yaml - argocd-proxy-app.yaml - makeitwork-proxy-app.yaml - warp-app.yaml - # Wave 1: Depend on operator CRDs (TunnelBinding requires cloudflare-operator) - arc-app.yaml - grafana-app.yaml - kubectl-tunnel-app.yaml - - ../argocd # Direct include - TunnelBinding for ArgoCD + - ../argocd diff --git a/workloads/apps/wait-for-crds.yaml b/workloads/apps/wait-for-crds.yaml new file mode 100644 index 0000000..13003d5 --- /dev/null +++ b/workloads/apps/wait-for-crds.yaml @@ -0,0 +1,66 @@ +--- +# Child Applications reconcile workload CRs immediately, so all required operator +# CRDs must exist before those Applications are created. +apiVersion: batch/v1 +kind: Job +metadata: + name: wait-for-crds + namespace: argocd + annotations: + argocd.argoproj.io/sync-wave: "1" + argocd.argoproj.io/hook: PreSync + argocd.argoproj.io/hook-delete-policy: BeforeHookCreation,HookSucceeded + ignore-check.kube-linter.io/non-existent-service-account: "SA created by GitOps operator" + ignore-check.kube-linter.io/latest-tag: "bitnami/kubectl:latest is acceptable for a one-shot bootstrap wait Job" +spec: + ttlSecondsAfterFinished: 300 + backoffLimit: 30 + activeDeadlineSeconds: 900 + template: + spec: + serviceAccountName: argocd-argocd-application-controller + restartPolicy: Never + securityContext: + runAsNonRoot: true + seccompProfile: + type: RuntimeDefault + containers: + - name: wait + image: bitnami/kubectl:latest + command: + - /bin/bash + - -c + - | + set -euo pipefail + + CRDS=( + autoscalingrunnersets.actions.github.com + tunnelbindings.networking.cfargotunnel.com + onionservices.tor.k8s.torproject.org + grafanas.grafana.integreatly.org + grafanadatasources.grafana.integreatly.org + grafanadashboards.grafana.integreatly.org + servicemonitors.monitoring.coreos.com + probes.monitoring.coreos.com + ) + + for crd in "${CRDS[@]}"; do + until kubectl wait --for=condition=Established --timeout=10s \ + "crd/$crd" >/dev/null 2>&1; do + sleep 5 + done + done + + echo "All operator CRDs are available" + securityContext: + allowPrivilegeEscalation: false + readOnlyRootFilesystem: true + capabilities: + drop: + - ALL + resources: + requests: + cpu: 10m + memory: 64Mi + limits: + memory: 128Mi diff --git a/workloads/arc/github-token-secret.yaml b/workloads/arc/github-token-secret.yaml index 0e1c792..b445541 100644 --- a/workloads/arc/github-token-secret.yaml +++ b/workloads/arc/github-token-secret.yaml @@ -1,4 +1,3 @@ -# GitHub PAT for ARC runner scale set apiVersion: v1 kind: Secret metadata: diff --git a/workloads/makeitwork-proxy/onion-service.yaml b/workloads/makeitwork-proxy/onion-service.yaml index 7f2b662..3fb5c67 100644 --- a/workloads/makeitwork-proxy/onion-service.yaml +++ b/workloads/makeitwork-proxy/onion-service.yaml @@ -1,5 +1,4 @@ --- -# Nginx reverse proxy to makeitwork.cloud (external site) apiVersion: v1 kind: ConfigMap metadata: