Docs · Glidepath
Installation
platform-cicd installs onto any Kubernetes cluster declaratively, via ArgoCD - the
same way every other cluster-config piece gets installed: a couple of ArgoCD
Application manifests in that cluster’s own gitops-cluster-<name> repo. See
gitops-cluster-dev/50-platform-cicd/ for a working reference - it wires up
tektoncd/operator (Tekton Pipelines/Triggers/Chains/Dashboard/PaC in one namespace),
glidepath-catalog, and glidepath-control-plane.
Steps for a brand-new cluster:
-
Get ArgoCD running on the target cluster (out of scope here - see that cluster’s own bootstrap).
-
Generate this cluster’s own values, writing straight into its
gitops-cluster-<name>repo checkout (not intoplatform-cicd- see that script’s own header for why):./hack/generate-cluster-values.sh <kube-context> <cluster-name> \ ../gitops-cluster-<name>/50-platform-cicd/glidepath-control-planeThis reads the cluster’s own API server root CA live and generates a fresh, independent Fulcio signing root for it - it never copies another cluster’s trust material. Commit and push the resulting file.
-
Add the two Application manifests (control-plane, catalog) to that repo, each multi-source: one source is this platform’s chart at
charts/glidepath-catalog/charts/glidepath-control-plane, the other adirectorysource (exclude: "*") pointed at the cluster-config repo itself,$ref’d forvalueFiles- seegitops-cluster-dev/50-platform-cicd/glidepath-control-plane/application.yamlfor the exact shape, and architecture-decisions ADR-0006 for why cluster state never lives insideplatform-cicditself. -
Push. ArgoCD takes it from there.
Values the cluster repo must supply
None of these has a default naming a cluster, an owner or a lab host; a render without the required ones fails with a message naming the value.
| Chart | Value | Required | Usual source |
|---|---|---|---|
glidepath-control-plane |
platformCicdRepoUrl |
yes | this repo’s URL in the fleet’s GitHub owner (clusters.yaml scm.owner) |
glidepath-control-plane |
clusterName |
yes | the cluster’s registry name (= kubectl context), from clusters.yaml |
glidepath-control-plane |
clusters, aws |
no | clusters.yaml as a value file |
glidepath-control-plane |
defaultChart |
yes, for apps | cluster-defaults.yaml as a value file |
glidepath-control-plane |
observability (namespace, instrumentation) |
no | cluster-defaults.yaml as a value file |
glidepath-control-plane |
backstageBaseUrl, backstageHostAliasIP |
no | only for a Backstage hostname a pod cannot resolve |
glidepath-catalog |
defaultChart.repoURL, defaultChart.targetRevision |
yes | cluster-defaults.yaml as a value file |
glidepath-catalog |
valuesValidatorRepository (or valuesValidatorImage) |
yes | where the fleet’s Airframe publishes airframe-validate |
glidepath-catalog |
backstageBaseUrl, backstageHostAliasIP |
no | same values as the control plane’s |
glidepath-catalog |
pipelinesAsCode.applicationName |
no (default Glidepath) |
must equal the cluster’s PaC ConfigMap application-name: the first half of every gate’s check name (github-app.md) |
The control plane forwards platformCicdRepoUrl, defaultChart, the Backstage alias and
clusterTaxonomy (whose self is clusterName: the dev cluster) to every app’s
glidepath-app; an app’s Ground environments and previews render with that cluster name
and clusterType: dev. With observability set, each environment that renders the cluster’s default chart also
gets airframe-application’s observability values: Prometheus is admitted through the app’s NetworkPolicy, and
the OpenTelemetry Operator instruments the app for its runtime (platformIdentity.runtime, written by Airframe).
Each of those environments also gets airframe-application’s platform-owned serviceNamespacePattern
({type}-{app}-{env}, from glidepath-app.envNamespacePattern, the same helper that names the environment’s own
namespace), which the chart’s env[].fromService uses to resolve another app’s Service URL. It needs the cluster’s
default chart to be an airframe-application with that key (the release after v0.3.167): bump every cluster’s
defaultChart first. An environment pinned to another chart (deploy.chart, environments[].chart) gets neither
value, and a fromService there fails its render instead of guessing.
This keeps platform-cicd installable standalone on any cluster - it carries zero
cluster-specific secrets or identity in its own repo. For local/kind development,
hack/kind-config.yaml creates a raw, Calico-enabled cluster to point ArgoCD at -
there’s no separate imperative install path beyond that; get ArgoCD running, then
follow the steps above.
Grafana serves the platform’s own dashboards (rendered as ConfigMaps by the control-plane chart, not a separate apply step). The Tekton Dashboard is the complementary low-level view - installed read-only deliberately, matching this platform’s PaaS/RBAC posture.
Operational notes
- NetworkPolicy needs a real CNI.
default-deny/allow-from-same-namespacemanifests are silent no-ops on a CNI that doesn’t enforceNetworkPolicy(e.g. kindnet). TokenReview authentication on the broker - not NetworkPolicy - is the actual trust boundary (see chaining.md); NetworkPolicy is defense-in-depth on top of it. Pin a Calico/Cilium-class CNI for anything beyond a shared dev cluster. - Two names on every check. PaC’s
application-name(Glidepath) names the check runs and must match the catalog’spipelinesAsCode.applicationName; the GitHub App’s own name is free, but Backstage’sglidepath.gateCommentAppsmust list its slug or Tower shows no signature evidence. See github-app.md. - The GitHub App needs a public endpoint. Pipelines-as-Code’s webhook delivery
can’t reach a local/private cluster directly - front the PaC controller with a
tunnel (
cloudflared,ngrok) for local dev, or real ingress/DNS for anything else. token-review-interceptor/glidepath-relayimages areIfNotPresent+:latest. A source change underglidepath/broker/cmd/does nothing to a running cluster until you rebuild, push, andkubectl rollout restartthe affected Deployment - there’s no CI wired to a private cluster to do this automatically.- Image builds need Pod Security
baseline, a binfmt handler, and seccompDefault off.build-imageruns rootless BuildKit as UID 1000; it creates a user namespace, so a namespace enforcingrestrictedrejects it, and a kubelet withseccompDefault: trueblocks it unless you give the step a Localhost seccomp profile. Building amd64 on an arm64 node (or the reverse) needs binfmt_misc handlers registered on the node. See rootless-builds.md. - Tekton’s Affinity Assistant is off (
coschedule: disabled). Set in the cluster repo’s50-platform-cicd/tekton-operator/tektonconfig.yaml(2026-10-09). The default (workspaces) starts a StatefulSet and placeholder pod before every build and test run, 10-25 s each, only to keep TaskRuns that share a PVC on one node. A single-node cluster, or one whose StorageClass pins a PV to a node (local-path), gets that anyway. On a multi-node cluster with network-attachedReadWriteOncestorage usecoschedule: pipelineruns: the per-appbuild-cachePVC and the run’ssourcePVC are both mounted bybuild-sourceandunit-test, and must land together. - One Trivy server holds the vulnerability DB.
glidepath-control-planerunstrivy-serverinplatform-system(trivyServervalues; its tag must equalTRIVY_VERSIONincatalog/toolbox/Dockerfile), andimage-scanscans against it (trivyServerUrlin the catalog values). Before 2026-10-09 every scan pod downloaded the ~120 MiB DB itself. If the server does not answer/healthzor fails mid-scan, the scan runs standalone and downloads the DB as before, so an outage costs time, not a false failure.generate-sbomdoes not use it: a CycloneDX-only run has no vulnerability scanner and never needed the DB. - Ground deploys ask Argo CD to refresh.
deploy-manifestsannotates the environment’s Application inargocd-appswithargocd.argoproj.io/refresh=hardright after its push, so the new release file is picked up in seconds instead of on the next 120 s (+60 s jitter) poll. The grant is per app (glidepath-apptemplates/env/argocd-refresh-rbac.yaml, get+patch on its own Ground Applications), and the control plane’s ValidatingAdmissionPolicypipeline-runner-application-refresh-onlydenies any change from apipeline-runnerServiceAccount other than that annotation and thehangar.io/dora-*ones, so the grant can never change an Application’s project, destination or sync operation. It needs Kubernetes 1.30+ (ValidatingAdmissionPolicy GA). Without the grant the deploy still works, at poll speed.
Upper environments (staging/prod)
A separate bootstrap, covered in multi-cluster.md -
hack/bootstrap-upper-cluster.sh installs just ArgoCD on a target cluster; this
platform’s own Tekton/broker never runs there.