commit 6f29be298708a85045882f833d502cd3b4ecff36 Author: Nick Gorse Date: Sun May 10 22:32:49 2026 +0100 feat: initial scaffold from helm-template - Keycloak Operator v26 deployed to keycloak-system namespace - Keycloak HA instance (2 replicas, jdbc-ping cluster discovery) - Dedicated CNPG cluster (local-postgres storage, 2 instances) - KeycloakRealmImport: home-lab realm with groups and traefik-oidc client - Traefik IngressRoute: login.olb42.com (CF Access bypass) - Admin credentials placeholder (seal before first deploy) - ArgoCD ApplicationSets: login-operator + login diff --git a/.gitea/workflows/dormant-pr.yaml b/.gitea/workflows/dormant-pr.yaml new file mode 100644 index 0000000..d4aa062 --- /dev/null +++ b/.gitea/workflows/dormant-pr.yaml @@ -0,0 +1,19 @@ +name: Propose dormant overlay change + +on: + push: + tags: + - dormant/** + +jobs: + propose-dormant-pr: + runs-on: ubuntu-latest + env: + GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }} + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Run dormant PR automation + run: scripts/automation/propose-dormant-pr.sh diff --git a/.gitea/workflows/validate.yaml b/.gitea/workflows/validate.yaml new file mode 100644 index 0000000..ec9756b --- /dev/null +++ b/.gitea/workflows/validate.yaml @@ -0,0 +1,29 @@ +name: Validate login GitOps repo + +on: + push: + pull_request: + +jobs: + validate: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Install tools + run: | + apt-get update + apt-get install -y bash curl ca-certificates git python3 python3-yaml + curl -sL https://github.com/yannh/kubeconform/releases/latest/download/kubeconform-linux-amd64.tar.gz \ + | tar xz -C /usr/local/bin + curl -fsSL "https://dl.k8s.io/release/$(curl -fsSL https://dl.k8s.io/release/stable.txt)/bin/linux/amd64/kubectl" \ + -o /usr/local/bin/kubectl + chmod +x /usr/local/bin/kubectl + + - name: Validate repo + run: scripts/ci/validate + + - name: Apply bootstrap ApplicationSets + env: + KUBECONFIG_B64: ${{ secrets.KUBECONFIG_B64 }} + run: scripts/ci/apply-bootstrap diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a53df45 --- /dev/null +++ b/.gitignore @@ -0,0 +1,15 @@ +# Local secret files — never commit plain secrets +*.secret.plain.yaml +*.plain.yaml + +# CI tooling caches +.ci-schemas/ +.ci-rendered/ +.ci-chart-cache/ +.helm-cache/ +.helm-config/ +.helm-data/ +.kube/ + +# macOS +.DS_Store diff --git a/OPERATIONS.md b/OPERATIONS.md new file mode 100644 index 0000000..e0fb95f --- /dev/null +++ b/OPERATIONS.md @@ -0,0 +1,59 @@ +# login — Keycloak Operator-managed instance + +Keycloak HA deployment for `login.olb42.com`, managed by the official +Keycloak Operator. Backs all OIDC authentication across the home-lab. + +## Architecture + +- **Namespace**: `login` +- **Operator namespace**: `keycloak-system` +- **Database**: CNPG cluster `login-postgres` (2 instances, `local-postgres` storage) +- **Replicas**: 2 Keycloak pods with Infinispan jdbc-ping cluster discovery +- **Ingress**: `login.olb42.com` via Traefik IngressRoute (no CF Access — bypass) + +## Bootstrap order + +1. ArgoCD applies `login-operator` ApplicationSet → Keycloak Operator deployed +2. ArgoCD applies `login` ApplicationSet → CRDs reconciled: + - CNPG Cluster `login-postgres` created + - `Keycloak` CR reconciled → Operator deploys Keycloak pods + - `KeycloakRealmImport` applied → `home-lab` realm created + - IngressRoute activates `login.olb42.com` + +## Sealing the admin secret + +```bash +# Edit admin.secret.plain.yaml with your chosen password, then: +kubeseal --context homelab-argocd \ + --secret-file manifest/overlays/production/admin.secret.plain.yaml \ + --sealed-secret-file manifest/overlays/production/admin.sealed.secret.yaml +``` + +## Adding a per-app Keycloak client + +Create a `KeycloakRealmImport` in the app's own repo: + +```yaml +apiVersion: k8s.keycloak.org/v2alpha1 +kind: KeycloakRealmImport +metadata: + name: my-app-client + namespace: login # must match the Keycloak CR namespace +spec: + keycloakCRName: keycloak # must match the Keycloak CR name + realm: + realm: home-lab + clients: + - clientId: traefik-oidc-my-app + ... +``` + +ArgoCD syncs the `KeycloakRealmImport` → Operator registers the client in +Keycloak → Operator writes `traefik-oidc-my-app` secret → traefikoidc +Middleware references it. + +## Upgrading Keycloak + +1. Update `image: quay.io/keycloak/keycloak:` in `keycloak-instance.yaml` +2. Update the operator remote URL version in `manifest/operator/kustomization.yaml` +3. Commit and push — ArgoCD self-heals both ApplicationSets in order diff --git a/README.md b/README.md new file mode 100644 index 0000000..9194573 --- /dev/null +++ b/README.md @@ -0,0 +1,10 @@ +# login + +Operator-managed Keycloak HA deployment. Identity and access management for all home-lab services. + +- **URL**: https://login.olb42.com +- **Realm**: `home-lab` +- **Operator**: [keycloak-operator](https://github.com/keycloak/keycloak-operator) v26 +- **Database**: CNPG PostgreSQL (`local-postgres` storage class) + +See [OPERATIONS.md](./OPERATIONS.md) for bootstrap, secret sealing, and client management procedures. diff --git a/bootstrap/applicationset.yaml b/bootstrap/applicationset.yaml new file mode 100644 index 0000000..eda048a --- /dev/null +++ b/bootstrap/applicationset.yaml @@ -0,0 +1,99 @@ +# ── ApplicationSet 1: Keycloak Operator (CRDs + controller) ────────────────── +# Deploys the Keycloak Operator into its own namespace so it can manage +# Keycloak instances cluster-wide. The operator install includes cluster-scoped +# resources (CRDs, ClusterRoles) which must be applied with ServerSideApply. +apiVersion: argoproj.io/v1alpha1 +kind: ApplicationSet +metadata: + name: login-operator + namespace: argocd +spec: + ignoreApplicationDifferences: + - jsonPointers: + - /spec/syncPolicy + goTemplate: true + goTemplateOptions: ["missingkey=error"] + generators: + - list: + elements: + - environment: production + namespace: keycloak-system + template: + metadata: + name: 'login-operator-{{ .environment }}' + labels: + app.kubernetes.io/managed-by: argocd + app.kubernetes.io/name: login-operator + spec: + project: default + source: + repoURL: http://gitea-ha-http.apps:3000/olb42/login.git + targetRevision: main + path: manifest/operator + kustomize: + commonAnnotationsEnvsubst: true + commonAnnotations: + app-source: ${ARGOCD_APP_SOURCE_REPO_URL} + app-revision: ${ARGOCD_APP_SOURCE_TARGET_REVISION} + destination: + server: https://kubernetes.default.svc + namespace: '{{ .namespace }}' + syncPolicy: + automated: + prune: false + selfHeal: true + enabled: true + syncOptions: + - CreateNamespace=true + - ApplyOutOfSyncOnly=true + - ServerSideApply=true + - Replace=false +--- +# ── ApplicationSet 2: login Keycloak instance ──────────────────────────────── +# Deploys the Keycloak CR, CNPG cluster, IngressRoute, and realm config. +# Depends on login-operator being synced first (CRDs must exist). +apiVersion: argoproj.io/v1alpha1 +kind: ApplicationSet +metadata: + name: login + namespace: argocd +spec: + ignoreApplicationDifferences: + - jsonPointers: + - /spec/syncPolicy + goTemplate: true + goTemplateOptions: ["missingkey=error"] + generators: + - list: + elements: + - environment: production + namespace: login + template: + metadata: + name: 'login-{{ .environment }}' + labels: + app.kubernetes.io/managed-by: argocd + app.kubernetes.io/name: login + spec: + project: default + source: + repoURL: http://gitea-ha-http.apps:3000/olb42/login.git + targetRevision: main + path: 'manifest/overlays/{{ .environment }}' + kustomize: + commonAnnotationsEnvsubst: true + commonAnnotations: + app-source: ${ARGOCD_APP_SOURCE_REPO_URL} + app-revision: ${ARGOCD_APP_SOURCE_TARGET_REVISION} + destination: + server: https://kubernetes.default.svc + namespace: '{{ .namespace }}' + syncPolicy: + automated: + prune: true + selfHeal: true + enabled: true + syncOptions: + - CreateNamespace=true + - ApplyOutOfSyncOnly=true + - ServerSideApply=true diff --git a/bootstrap/config.yaml b/bootstrap/config.yaml new file mode 100644 index 0000000..eb1a8f6 --- /dev/null +++ b/bootstrap/config.yaml @@ -0,0 +1,3 @@ +enabled: true +target_namespace: argocd +apply_from_branch: main diff --git a/manifest/base/kustomization.yaml b/manifest/base/kustomization.yaml new file mode 100644 index 0000000..285b7f0 --- /dev/null +++ b/manifest/base/kustomization.yaml @@ -0,0 +1,6 @@ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization + +resources: + - runtime + - state diff --git a/manifest/base/runtime/kustomization.yaml b/manifest/base/runtime/kustomization.yaml new file mode 100644 index 0000000..325e356 --- /dev/null +++ b/manifest/base/runtime/kustomization.yaml @@ -0,0 +1,7 @@ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization + +# Runtime objects are managed by the Keycloak Operator via the Keycloak CR. +# Nothing to define here — the operator creates Deployments, Services, and +# ConfigMaps from the spec in manifest/overlays/production/keycloak-instance.yaml. +resources: [] diff --git a/manifest/base/state/kustomization.yaml b/manifest/base/state/kustomization.yaml new file mode 100644 index 0000000..722d5d4 --- /dev/null +++ b/manifest/base/state/kustomization.yaml @@ -0,0 +1,6 @@ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization + +# Persistent state is managed by the CNPG Cluster CR defined in the production +# overlay. CNPG handles PVC lifecycle; nothing additional is needed here. +resources: [] diff --git a/manifest/operator/kustomization.yaml b/manifest/operator/kustomization.yaml new file mode 100644 index 0000000..185011c --- /dev/null +++ b/manifest/operator/kustomization.yaml @@ -0,0 +1,17 @@ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization + +# Keycloak Operator v26 — official Kubernetes manifests. +# Pin to a specific release tag; bump here when upgrading Keycloak. +# Full release list: https://github.com/keycloak/keycloak-k8s-resources/releases +# +# These resources include: +# - keycloaks.k8s.keycloak.org CRD +# - keycloakrealmimports.k8s.keycloak.org CRD +# - keycloak-operator Deployment + RBAC (deployed to keycloak-system namespace +# as defined in the upstream manifest; operator watches all namespaces) + +resources: + - https://raw.githubusercontent.com/keycloak/keycloak-k8s-resources/26.2.5/kubernetes/keycloaks.k8s.keycloak.org-v1.yml + - https://raw.githubusercontent.com/keycloak/keycloak-k8s-resources/26.2.5/kubernetes/keycloakrealmimports.k8s.keycloak.org-v1.yml + - https://raw.githubusercontent.com/keycloak/keycloak-k8s-resources/26.2.5/kubernetes/kubernetes.yml diff --git a/manifest/overlays/dormant/kustomization.yaml b/manifest/overlays/dormant/kustomization.yaml new file mode 100644 index 0000000..2eacc0e --- /dev/null +++ b/manifest/overlays/dormant/kustomization.yaml @@ -0,0 +1,22 @@ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization + +namespace: login + +# Dormant mode: CNPG cluster stays alive to preserve data. +# Keycloak instance is scaled to zero by patching the Keycloak CR. +resources: + - ../../base/state + +patches: + - patch: | + - op: replace + path: /spec/instances + value: 0 + target: + kind: Keycloak + name: keycloak + +commonLabels: + app.kubernetes.io/part-of: login + app.kubernetes.io/environment: dormant diff --git a/manifest/overlays/production/admin.sealed.secret.yaml b/manifest/overlays/production/admin.sealed.secret.yaml new file mode 100644 index 0000000..6c33e91 --- /dev/null +++ b/manifest/overlays/production/admin.sealed.secret.yaml @@ -0,0 +1,25 @@ +apiVersion: bitnami.com/v1alpha1 +kind: SealedSecret +metadata: + name: login-admin + namespace: login + annotations: + sealedsecrets.bitnami.com/managed: "true" +spec: + encryptedData: + # Seal from admin.secret.plain.yaml: + # kubeseal --context homelab-argocd \ + # --secret-file manifest/overlays/production/admin.secret.plain.yaml \ + # --sealed-secret-file manifest/overlays/production/admin.sealed.secret.yaml + username: REPLACE_WITH_KUBESEAL_OUTPUT + password: REPLACE_WITH_KUBESEAL_OUTPUT + template: + metadata: + name: login-admin + namespace: login + annotations: + sealedsecrets.bitnami.com/managed: "true" + labels: + app.kubernetes.io/part-of: login + app.kubernetes.io/environment: production + type: Opaque diff --git a/manifest/overlays/production/cnpg-cluster.yaml b/manifest/overlays/production/cnpg-cluster.yaml new file mode 100644 index 0000000..e1018a4 --- /dev/null +++ b/manifest/overlays/production/cnpg-cluster.yaml @@ -0,0 +1,48 @@ +apiVersion: postgresql.cnpg.io/v1 +kind: Cluster +metadata: + name: login-postgres + namespace: login +spec: + # ── HA: 1 primary + 1 standby replica ──────────────────────────────────── + instances: 2 + + # ── Storage: local-postgres class (fast local NVMe, no Longhorn overhead) ─ + storage: + storageClass: local-postgres + size: 8Gi + + # ── Spread replicas across nodes ────────────────────────────────────────── + affinity: + enablePodAntiAffinity: true + topologyKey: kubernetes.io/hostname + + # ── Bootstrap: create the keycloak database and owner role ─────────────── + bootstrap: + initdb: + database: keycloak + owner: keycloak + + # ── WAL archival via barman-cloud → MinIO ───────────────────────────────── + plugins: + - name: barman-cloud.cloudnative-pg.io + isWALArchiver: true + parameters: + barmanObjectName: minio-store + + # ── PostgreSQL tuning for Keycloak workload ─────────────────────────────── + postgresql: + parameters: + max_connections: "200" + shared_buffers: "256MB" + work_mem: "8MB" + maintenance_work_mem: "64MB" + + # ── Resources ───────────────────────────────────────────────────────────── + resources: + requests: + memory: 256Mi + cpu: 100m + limits: + memory: 512Mi + cpu: 500m diff --git a/manifest/overlays/production/ingressroute.yaml b/manifest/overlays/production/ingressroute.yaml new file mode 100644 index 0000000..fd19f97 --- /dev/null +++ b/manifest/overlays/production/ingressroute.yaml @@ -0,0 +1,36 @@ +apiVersion: traefik.io/v1alpha1 +kind: IngressRoute +metadata: + name: login + namespace: login + annotations: + # Cloudflare tunnel: public access, bypass CF Access + # Keycloak IS the auth provider — it must be reachable before any OIDC + # flow can begin, so CF Access must not gate it. + dns.public: "true" + dns.public.access.policy: bypass + dns.public.hostname: login.olb42.com + + # Homepage service discovery + gethomepage.dev/enabled: "true" + gethomepage.dev/name: "Keycloak" + gethomepage.dev/group: "Security" + gethomepage.dev/description: "Identity & Access Management (operator-managed)" + gethomepage.dev/href: "https://login.olb42.com" + gethomepage.dev/icon: "keycloak" + gethomepage.dev/app: "keycloak" + gethomepage.dev/namespace: "login" +spec: + entryPoints: + - websecure + routes: + - match: Host(`login.olb42.com`) + kind: Rule + # No OIDC middleware: Keycloak is the identity provider itself. + # Traefik passes traffic straight through to the Keycloak service. + services: + - name: keycloak-service + namespace: login + port: 8080 + tls: + secretName: olb42-wildcard-tls diff --git a/manifest/overlays/production/keycloak-instance.yaml b/manifest/overlays/production/keycloak-instance.yaml new file mode 100644 index 0000000..899e5dd --- /dev/null +++ b/manifest/overlays/production/keycloak-instance.yaml @@ -0,0 +1,76 @@ +apiVersion: k8s.keycloak.org/v2alpha1 +kind: Keycloak +metadata: + name: keycloak + namespace: login +spec: + # ── HA: 2 replicas with Infinispan jdbc-ping cluster discovery ──────────── + instances: 2 + + # ── Image: pin to the same major as the operator release ───────────────── + image: quay.io/keycloak/keycloak:26.2.5 + startOptimized: false + + # ── Database: CNPG cluster (login-postgres) ─────────────────────────────── + # CNPG auto-creates the secret -app with username/password. + db: + vendor: postgres + host: login-postgres-rw + port: 5432 + database: keycloak + usernameSecret: + name: login-postgres-app + key: username + passwordSecret: + name: login-postgres-app + key: password + + # ── HTTP: plain HTTP backend; TLS terminated at Traefik ─────────────────── + http: + httpEnabled: true + httpPort: 8080 + httpsPort: 8443 + # No TLS secret — Traefik handles TLS via wildcard cert + + # ── Hostname ────────────────────────────────────────────────────────────── + hostname: + hostname: login.olb42.com + admin: login.olb42.com + # strict=false: allow X-Forwarded-Host from Traefik + strict: false + strictBackchannel: false + + # ── Proxy: trust forwarded headers from Traefik ────────────────────────── + proxy: + headers: forwarded + + # ── Bootstrap admin credentials (sealed secret — see admin.sealed.secret.yaml) + bootstrapAdminSpec: + secret: + name: login-admin + + # ── Additional Keycloak options ─────────────────────────────────────────── + additionalOptions: + # jdbc-ping: DB-backed cluster discovery — no Kubernetes API/RBAC needed + - name: cache-stack + value: jdbc-ping + - name: cache + value: ispn + - name: log-level + value: INFO + # Forward client IP through Traefik X-Forwarded-For chain + - name: proxy-trusted-addresses + value: "0.0.0.0/0" + + # ── Resources ───────────────────────────────────────────────────────────── + resources: + requests: + memory: 512Mi + cpu: 250m + limits: + memory: 1500Mi + cpu: "2" + + # ── Ingress disabled: Traefik IngressRoute manages external access ───────── + ingress: + enabled: false diff --git a/manifest/overlays/production/keycloak-realm-home-lab.yaml b/manifest/overlays/production/keycloak-realm-home-lab.yaml new file mode 100644 index 0000000..36c8048 --- /dev/null +++ b/manifest/overlays/production/keycloak-realm-home-lab.yaml @@ -0,0 +1,100 @@ +apiVersion: k8s.keycloak.org/v2alpha1 +kind: KeycloakRealmImport +metadata: + name: home-lab + namespace: login +spec: + keycloakCRName: keycloak + + # ── Realm definition ────────────────────────────────────────────────────── + # This is a Keycloak realm export JSON embedded as a structured spec. + # The operator applies this on startup and reconciles on changes. + # Add clients, groups, and roles here; per-app clients can be managed in + # their own app repos by adding additional KeycloakRealmImport resources + # targeting this same keycloakCRName. + realm: + realm: home-lab + displayName: "Home Lab" + enabled: true + loginWithEmailAllowed: true + duplicateEmailsAllowed: false + resetPasswordAllowed: true + editUsernameAllowed: false + bruteForceProtected: true + permanentLockout: false + maxFailureWaitSeconds: 900 + minimumQuickLoginWaitSeconds: 60 + waitIncrementSeconds: 60 + quickLoginCheckMilliSeconds: 1000 + maxDeltaTimeSeconds: 43200 + failureFactor: 10 + + # ── Session settings ────────────────────────────────────────────────── + ssoSessionIdleTimeout: 1800 + ssoSessionMaxLifespan: 36000 + accessTokenLifespan: 300 + accessTokenLifespanForImplicitFlow: 900 + offlineSessionIdleTimeout: 2592000 + offlineSessionMaxLifespanEnabled: false + + # ── Password policy ─────────────────────────────────────────────────── + passwordPolicy: "length(12) and notUsername(undefined)" + + # ── Groups ──────────────────────────────────────────────────────────── + groups: + - name: homelab-admin + path: /homelab-admin + - name: homelab-user + path: /homelab-user + - name: transmission-users + path: /transmission-users + - name: media-users + path: /media-users + - name: monitoring-users + path: /monitoring-users + + # ── Default client scopes ───────────────────────────────────────────── + defaultDefaultClientScopes: + - web-origins + - acr + - roles + - profile + - basic + - email + + # ── Clients ─────────────────────────────────────────────────────────── + # traefik-oidc: shared client for traefikoidc middleware (SSO cookie domain) + # Per-app clients with dedicated redirect URIs and claim mappers should be + # added as additional KeycloakRealmImport resources in each app's repo. + clients: + - clientId: traefik-oidc + name: "Traefik OIDC" + description: "Shared OIDC client for Traefik traefikoidc middleware" + enabled: true + publicClient: false + standardFlowEnabled: true + implicitFlowEnabled: false + directAccessGrantsEnabled: false + serviceAccountsEnabled: false + authorizationServicesEnabled: false + redirectUris: + - "https://transmission-ng.olb42.com/oauth2/callback" + - "https://home.olb42.com/oauth2/callback" + - "https://dozzle.olb42.com/oauth2/callback" + - "https://pgadmin4.olb42.com/oauth2/callback" + webOrigins: + - "+" + attributes: + pkce.code.challenge.method: "S256" + post.logout.redirect.uris: "+" + protocolMappers: + - name: groups + protocol: openid-connect + protocolMapper: oidc-group-membership-mapper + consentRequired: false + config: + full.path: "false" + id.token.claim: "true" + access.token.claim: "true" + claim.name: "groups" + userinfo.token.claim: "true" diff --git a/manifest/overlays/production/kustomization.yaml b/manifest/overlays/production/kustomization.yaml new file mode 100644 index 0000000..8bf4625 --- /dev/null +++ b/manifest/overlays/production/kustomization.yaml @@ -0,0 +1,16 @@ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization + +namespace: login + +resources: + - ../../base/state + - cnpg-cluster.yaml + - keycloak-instance.yaml + - keycloak-realm-home-lab.yaml + - ingressroute.yaml + - admin.sealed.secret.yaml + +commonLabels: + app.kubernetes.io/part-of: login + app.kubernetes.io/environment: production diff --git a/scripts/automation/propose-dormant-pr.sh b/scripts/automation/propose-dormant-pr.sh new file mode 100755 index 0000000..6606eef --- /dev/null +++ b/scripts/automation/propose-dormant-pr.sh @@ -0,0 +1,35 @@ +#!/usr/bin/env bash +# Copied from helm-template; update GITEA_API_URL and repo details as needed. +set -euo pipefail + +GITEA_API_URL="${GITEA_API_URL:-https://git.olb42.com/api/v1}" +REPO_OWNER="${REPO_OWNER:-olb42}" +REPO_NAME="${REPO_NAME:-login}" +PR_BRANCH="dormant/$(date +%Y%m%d-%H%M%S)" +BASE_BRANCH="main" + +if [[ -z "${GITEA_TOKEN:-}" ]]; then + echo "GITEA_TOKEN is required" + exit 1 +fi + +git config user.email "ci@olb42.com" +git config user.name "CI Automation" + +git checkout -b "$PR_BRANCH" + +# Swap production overlay for dormant in the ArgoCD ApplicationSet +# This is handled by ArgoCD dormant logic; just open the PR. +git push origin "$PR_BRANCH" + +curl -s -X POST "$GITEA_API_URL/repos/$REPO_OWNER/$REPO_NAME/pulls" \ + -H "Authorization: token $GITEA_TOKEN" \ + -H "Content-Type: application/json" \ + -d "{ + \"title\": \"Propose dormant mode for login\", + \"body\": \"Automated PR to switch login Keycloak to dormant overlay.\", + \"head\": \"$PR_BRANCH\", + \"base\": \"$BASE_BRANCH\" + }" + +echo "Dormant PR created" diff --git a/scripts/ci/apply-bootstrap b/scripts/ci/apply-bootstrap new file mode 100755 index 0000000..fccef9c --- /dev/null +++ b/scripts/ci/apply-bootstrap @@ -0,0 +1,35 @@ +#!/usr/bin/env bash +set -euo pipefail + +event_name="${GITHUB_EVENT_NAME:-}" +git_ref="${GITHUB_REF:-}" + +if [[ "$event_name" != "push" || "$git_ref" != "refs/heads/main" ]]; then + echo "Skipping bootstrap apply: only push events on main may apply" + exit 0 +fi + +if [[ ! -f bootstrap/config.yaml ]] || ! grep -Eq '^[[:space:]]*enabled:[[:space:]]*true[[:space:]]*$' bootstrap/config.yaml; then + echo "Skipping bootstrap apply: bootstrap/config.yaml is not enabled" + exit 0 +fi + +if [[ -z "${KUBECONFIG_B64:-}" ]]; then + echo "KUBECONFIG_B64 secret is required to apply bootstrap/applicationset.yaml" + exit 1 +fi + +curl -fsSL "https://dl.k8s.io/release/$(curl -fsSL https://dl.k8s.io/release/stable.txt)/bin/linux/amd64/kubectl" \ + -o /usr/local/bin/kubectl +chmod +x /usr/local/bin/kubectl + +mkdir -p "${HOME}/.kube" +printf '%s' "$KUBECONFIG_B64" | base64 -d > "${HOME}/.kube/config" + +if ! kubectl config current-context >/dev/null 2>&1; then + echo "Skipping bootstrap apply: kubeconfig secret is not usable in this runner" + exit 0 +fi + +# Apply both ApplicationSets (multi-document YAML) +kubectl apply -f bootstrap/applicationset.yaml diff --git a/scripts/ci/validate b/scripts/ci/validate new file mode 100755 index 0000000..c7efe30 --- /dev/null +++ b/scripts/ci/validate @@ -0,0 +1,87 @@ +#!/usr/bin/env bash +set -euo pipefail + +mkdir -p .ci-schemas/argoproj.io .ci-schemas/traefik.io .ci-schemas/bitnami.com .ci-rendered + +download_schema() { + local url="$1" dir="$2" base="$3" + mkdir -p "$dir" + if curl -fsSL "$url" -o "$dir/${base}_v1alpha1.json" 2>/dev/null; then + cp "$dir/${base}_v1alpha1.json" "$dir/${base}.json" + else + echo "Warning: could not download schema $url" + fi +} + +validate_yaml_syntax() { + echo "==> Validating YAML syntax" + find . -type f \( -name '*.yaml' -o -name '*.yml' \) \ + ! -path './.git/*' ! -path './.ci-*/*' \ + -print0 | while IFS= read -r -d '' file; do + python3 - "$file" <<'PY' +import sys, yaml +with open(sys.argv[1]) as f: + list(yaml.safe_load_all(f)) +PY + done +} + +validate_kustomize_overlays() { + echo "==> Building Kustomize overlays" + for overlay in manifest/overlays/*/; do + [[ -f "${overlay}kustomization.yaml" ]] || continue + name="$(basename "$overlay")" + echo " Building $name overlay" + if command -v kustomize &>/dev/null; then + kustomize build "$overlay" > ".ci-rendered/kustomize-${name}.yaml" 2>&1 || \ + echo " Warning: kustomize build failed for $name (remote resources may require network)" + else + kubectl kustomize "$overlay" > ".ci-rendered/kustomize-${name}.yaml" 2>&1 || \ + echo " Warning: kubectl kustomize failed for $name" + fi + done +} + +prepare_crd_schemas() { + echo "==> Preparing CRD schemas" + download_schema \ + "https://raw.githubusercontent.com/datreeio/CRDs-catalog/main/argoproj.io/applicationset_v1alpha1.json" \ + ".ci-schemas/argoproj.io" "applicationset" + download_schema \ + "https://raw.githubusercontent.com/datreeio/CRDs-catalog/main/traefik.io/ingressroute_v1alpha1.json" \ + ".ci-schemas/traefik.io" "ingressroute" + download_schema \ + "https://raw.githubusercontent.com/datreeio/CRDs-catalog/main/bitnami.com/sealedsecret_v1alpha1.json" \ + ".ci-schemas/bitnami.com" "sealedsecret" +} + +validate_kubernetes_manifests() { + echo "==> Validating Kubernetes manifests with kubeconform" + local bootstrap_enabled=false + if grep -Eq '^[[:space:]]*enabled:[[:space:]]*true' bootstrap/config.yaml 2>/dev/null; then + bootstrap_enabled=true + fi + + find . -type f \( -name '*.yaml' -o -name '*.yml' \) \ + ! -path './.git/*' ! -path './.gitea/*' ! -path './.ci-*/*' \ + ! -name '*kustomization.yaml' ! -name 'config.yaml' \ + ! -name '*.plain.yaml' ! -name '*.example.yaml' \ + $( [[ "$bootstrap_enabled" != "true" ]] && echo "! -name 'applicationset.yaml'" ) \ + | sort | xargs kubeconform \ + -strict \ + -kubernetes-version 1.35.0 \ + -schema-location default \ + -schema-location 'https://git.olb42.com/olb042/kubeconform/raw/branch/main/crdSchemas/{{ .ResourceKind }}_{{ .ResourceAPIVersion }}.json' \ + -ignore-missing-schemas \ + -summary +} + +if ! command -v python3 &>/dev/null; then echo "python3 required"; exit 1; fi +if ! command -v kubeconform &>/dev/null; then echo "kubeconform required"; exit 1; fi + +validate_yaml_syntax +prepare_crd_schemas +validate_kustomize_overlays +validate_kubernetes_manifests + +echo "Validation completed"