Skip to main content
Version: 0.4.x

GitOps Infrastructure

wxops-gitops-infrastructure is the single source of truth for every Kubernetes resource on every cluster. One kubectl apply -f app-of-apps.yaml seeds ArgoCD; from that point on every change — new platform tool, new tenant, new workload — is a Git commit.

No manual cluster operations after bootstrap.

App-of-Apps: Planes Pattern

Instead of one file per application, the repo uses one ApplicationSet per functional plane. Adding a service to an existing plane = one list entry. Adding a new plane = one new ApplicationSet.

Sync Wave Order

WavePlaneWhat it manages
1ingress-planeTraefik, external-dns, traefik-configs
1policy-planeKyverno ClusterPolicies
2security-planeVault, oauth2-proxy, Pinniped
2orchestration-planeArgoCD (self-managed), Crossplane runtime, act-runner
2platform-configscert-manager, ESO, cluster secrets, ArgoCD Image Updater, RBAC, users
3observability-planeGrafana Alloy, kube-prometheus-stack, Loki, Tempo, Pyroscope
3data-planeSeaweedFS, CloudNativePG, SchemaHero
4tenantsNamespace, RBAC, Vault identity, Gitea orgs per tenant
5tenants-appsTenant workloads (dev: auto-sync; staging/prod: manual gate)

Wave ordering guarantees Traefik exists before Vault, Vault before tenant namespaces, namespaces before workloads.

Adding a New Platform Tool

For planes that use the git-directory ApplicationSet generator (observability, data), adding a new tool is automatic:

base/observability-plane/<new-tool>/
Chart.yaml # or kustomization.yaml
values.yaml

ArgoCD discovers the directory and creates an Application automatically.

For list-based planes (ingress, security, orchestration), append an entry to the plane's ApplicationSet YAML.

Tenant Provisioning

Each tenant maps to one directory in tenants/<team>/. The tenants ApplicationSet (wave 4) discovers all directories here and creates one ArgoCD Application per team.

Required Files per Tenant

FileWhat it creates
namespace.yamlNamespace with wxops.cloud/tenant + wxops.cloud/portal: "true" labels
rbac.yamlThree RoleBinding objects: Managers → tenant-manager, Developers → tenant-developer, Viewers → tenant-viewer
vault-identity.yamlVault Policy (read tenants/<team>/* + shared/*) and K8s auth AuthBackendRole
gitea-org-members.yamlXGiteaOrg + XGiteaTeam claims — provisions Gitea organisation and team membership
argo-project.yamlArgoCD AppProject scoped to the tenant namespace
image-updater.yamlArgoCD Image Updater CR watching the tenant's apps
(optional) resource-quota.yamlOverride default quotas for large teams

The wxops.cloud/tenant label on the namespace triggers three Kyverno GeneratingPolicy objects that auto-provision into the namespace:

PolicyCreates
network-policiesDefault-deny ingress (allow Traefik + same-namespace) + allow all in-cluster egress
resource-quotaResourceQuota (2CPU/4Gi req, 4CPU/8Gi limit, 20 pods) + LimitRange (100m/128Mi defaults)
sync-secretsClones the regcred image pull Secret from kube-system

RBAC Personas

ClusterRoleWho gets itAccess scope
tenant-developer{org}:Developers sub-groupCRUD workloads + XRDs; read secrets + ExternalSecrets
tenant-manager{org}:Managers sub-groupFull workload control + ingress/HPA/PDB/PVCs; no RBAC management
tenant-viewer{org}:Viewers sub-groupRead-only across all tenant resources
platform-oncallPlatform team on-callRead cluster-wide; exec/port-forward; patch deployments
platform-cluster-adminPlatform team break-glassFull cluster-admin via ClusterRoleBinding

Bindings use the full orgName:teamName string as the subject — matching exactly what Pinniped relays from Gitea OIDC groups.

Secret Management Architecture

Three ClusterSecretStore objects, each backed by a dedicated Vault K8s auth role:

StoreVault mountUsed by
vault-platformplatform/Platform team infrastructure secrets
vault-tenanttenants/Tenant app secrets, scoped per team by policy
vault-sharedshared/Cross-team shared secrets (image pull credentials, etc.)

Vault infrastructure — mounts, OIDC auth, K8s auth, policies, roles — is fully GitOps-managed via Crossplane provider-vault CRDs under platform/vault/. Secret values are never in Git — only ExternalSecret references.

Vault Path Conventions

Path patternContent
platform/database-clusters/{clusterName}/…Platform CNPG cluster credentials
tenants/{team}/{appName}/envApp environment variables (synced to {appName}-env Secret)
tenants/{team}/databases/{appName}-db/connection-credsDB credentials (synced to {appName}-db-creds Secret)
shared/{name}Cross-team shared secrets

How the Portal Writes Here

When a developer scaffolds a new app through the portal:

  1. Portal commits tenants-apps/<team>/<app>/base/ (XTenantApp, XTenantDatabase, ExternalSecrets, catalog entities) and overlays/dev/ (image transformer + dev patch)
  2. The tenants-apps ApplicationSet (wave 5) auto-discovers the new overlays/dev/ directory and creates an ArgoCD Application with auto-sync + prune + selfHeal
  3. ArgoCD syncs to the dev spoke cluster; Crossplane expands the XTenantApp XR

overlays/staging/ and overlays/production/ are gated behind manual sync in ArgoCD — the portal creates them via PR, but ArgoCD will not apply them until a platform team member (or manager) clicks Sync or merges the Confirm step.

Portal-generated directories

tenants-apps/<team>/<app>/ directories are marked portal-managed via Gitea label. Do not edit them manually — the portal's next scaffold or promote action may overwrite changes.

CI in This Repo

WorkflowTriggerAction
catalog-cache-refreshPush touching service-catalog/POSTs to PORTAL_URL/api/v1/webhooks/catalog/refresh to invalidate portal cache
cve-scanSchedule + pushTrivy scan; opens auto-PR on CRITICAL findings
renovateScheduleAutomated dependency updates