feat(gcp): Crossplane controller, provider and config tree - #1824
Merged
Conversation
Slice 5's cluster-side plumbing: the GKE cluster can now run Crossplane and authenticate to GCP through the Workload Identity binding added in #1823. Three Flux Kustomizations mirroring the AWS chain, since the ordering constraints are identical — controller must run before a Provider installs, and the Provider must be healthy (wait: true) before a ClusterProviderConfig referencing its CRDs will apply. Facts verified by unpacking the v2.6.0 packages rather than inferred by analogy with AWS, because two of them differ: - ProjectIAMMember ships in provider-gcp-cloudplatform as projectiammembers.cloudplatform.gcp.m.upbound.io. That is the only Kind GCPWorkloadIdentity renders, so it is the only provider installed and the only entry in the activation policy. - GCP's ProviderConfig has no PodIdentity credential source at all. The equivalent is InjectedIdentity — ambient ADC, which under GKE Workload Identity resolves to the KSA's federated identity. - ClusterProviderConfig requires an explicit projectID. AWS derives its account from the caller's credentials; GCP does not. The DeploymentRuntimeConfig names its ServiceAccount provider-gcp, which is the subject hard-coded in the OpenTofu binding. Nothing validates that the two agree, so a rename fails silently — documented at both ends. Functions move to a shared functions/ directory referenced by both clouds. They are cluster-scoped singletons with cloud-agnostic pins, and two copies would let the versions drift, producing compositions that behave differently per cloud. Verified the AWS render is byte-identical before and after the move. Deliberately NOT included: a Configuration package for GCP. The XRDs and Compositions belong in Smana/crossplane-configuration and need a new crossplane-configuration-gcp package released there first; pointing at an unpublished tag would leave the Kustomization failing every reconcile. That cutover is two PRs with prune disabled on the first. Also corrects the crossplane README, whose "validating a composition" section still described rendering compositions that left this repo in #1774. Evidence: validate-manifests.sh exit 0, Valid: 1214, Invalid: 0, Skipped: 0; validate-links.sh and validate-doc-claims.sh exit 0; every ${var} referenced confirmed present in the ConfigMap gke/configure creates.
Contributor
🔍 Rendered manifest diff — this PR vs
|
The bug: moving functions.yaml into its own directory broke
apps/platform/app-wizard/wizard.yaml, which carries the path as a live config
value (functionsPath, resolved under REPO_ROOT by the initContainer clone).
After merge the wizard's crossplane render would have shelled out to a file
that no longer exists and /api/render-preview would have degraded to its error
path. app-wizard is deployed. Nothing in CI catches it — validate-links.sh only
walks Markdown, and .doc-claims.yaml has no crossplane entries.
This is the same silent path rot a doc move caused before. I checked Markdown
links and kustomize references after the git mv, and not YAML config values.
A comment that claimed a verification nothing performs: crossplane-configuration
said "a healthy ClusterProviderConfig proves the Workload Identity binding
authenticates". It proves nothing. Crossplane never contacts GCP to validate
credentials, and this Kustomization sets neither wait: true nor a healthCheck,
so Flux reports Ready as soon as the object is accepted. Credentials are first
exercised when a managed resource reconciles, and this PR ships no claim that
renders one. Rewritten to say the binding is unverified until the first
ProjectIAMMember.
CI was rendering GCP manifests with a literal ${project_id}: render-bundle.py's
FIXTURE_VARS gained none of the five new GCP variables, and unknown names pass
through verbatim. They still cleared gate 1 because every target field is a
free-form string, so the GCP substitution path was validated without ever being
exercised. Added all five; project_number is unquoted so the fixture keeps the
int shape the composition has to handle. The bundle now renders real values.
A comment that invited breaking the bootstrap: crossplane-controller's
`dependsOn: crds` was described as symmetry with AWS. It is load-bearing —
crds depends on flux-sources, which defines the crossplane HelmRepository, and
on namespaces, which creates crossplane-system. Dropping it fails at a point
that looks unrelated to that file.
Stale after the move: configuration-packages.yaml said functions.yaml was "in
this directory"; app-wizard/app.yaml pointed a version-mirror comment at the old
path.
Comment reduction, ~40 lines with no information lost: the two-PR cutover
rationale was stated in full three times (now once in the README, pointers
elsewhere); deploymentruntimeconfig-gcp restated 18 lines of iam.tf, and iam.tf
still framed the ServiceAccount name as a future obligation rather than one this
PR discharges — both now point at each other; environmentconfig instructed KCL
authored in another repo, reworded to record what that module does; the
passthrough overlay comment restated its own resources: line.
Not done: a durable guard for the path class that broke here. .doc-claims.yaml
asserts prose matches a config value and cannot assert a path exists, so an
entry would give false confidence. Worth a separate check.
Evidence: validate-manifests.sh exit 0, Valid: 1214, Invalid: 0, Skipped: 0;
validate-links.sh and validate-doc-claims.sh exit 0; tofu fmt clean; bundle
confirmed to carry substituted GCP values.
Smana
added a commit
that referenced
this pull request
Aug 24, 2026
Wires the GCP Crossplane API released today. #1824 deliberately left this out because the tag did not exist; it does now. Also bumps the AWS pin v0.1.0 -> v0.2.0, which is not incidental. The schema catalog is derived from a single pin — gen-catalog.sh reads XPKG_SOURCE, the AWS configuration-packages.yaml — and fetches that release's xrd-crds.yaml asset. Left at v0.1.0 the catalog carries 5 XRDs and no GCPWorkloadIdentity, so the first GCP claim added to this repo would hard-fail validation under skipMissingSchemas: false. Loud rather than silent, but it would make the new API unusable here. The alternative was teaching gen-catalog.sh to merge assets from several pins. That script carries an explicit warning against growing conditional logic in the fetch, because a second seam is how the catalog drifts from what is installed — and merging two assets means handling CRDs that appear in both. One version across both clouds keeps it a single seam. Safe because the AWS package did not change: the package.yaml inside crossplane-configuration-aws v0.1.0 and v0.2.0 is byte-identical (189403 bytes, same 6 kinds), verified by pulling both from ghcr.io. The bump deploys the same content under a new revision. This is also not the #1774 adopt-vs-prune case — the AWS XRDs are already package-owned, so there is nothing for Flux to delete in between. Per CLAUDE.md the App Wizard's fetch-crossplane-configuration clone tag moves in the same change, or the form and render preview describe a different API version than the cluster serves. Five website pages cited v0.1.0 and none are covered by .doc-claims.yaml, so they would have gone stale silently. Updated. No prune: disabled dance for the GCP pin: that procedure exists because a package adopts XRDs Flux still has in its inventory, and no GCPWorkloadIdentity XRD or claim has ever existed on any cluster. Recorded in the kustomization so the exemption is not mistaken for the general rule. Evidence: validate-manifests.sh exit 0, Valid: 1216, Invalid: 0, Skipped: 0; the catalog now builds 6 XRD schemas including gcpworkloadidentity_v1alpha1.json, whose published schema matches the shipped API (serviceAccount carries name only, root required: [spec]); validate-links.sh and validate-doc-claims.sh exit 0.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Slice 5's cluster-side plumbing. The GKE cluster can now run Crossplane and authenticate to GCP through the Workload Identity binding added in #1823.
Design:
docs/superpowers/specs/2026-08-18-gcp-support-design.md— workstream 5.What this adds
Three Flux Kustomizations mirroring the AWS chain, because the ordering constraints are identical:
wait: trueon stage 2 is load-bearing. Without it the Kustomization reports Ready as soon as theProviderobject exists, and stage 3 applies aClusterProviderConfigwhose CRD the provider has not installed yet.Verified from the packages, not inferred from AWS
Two of these break the analogy, and copying the AWS manifests with the group swapped would have failed:
PodIdentityInjectedIdentity— GCP's enum contains noPodIdentityat allprojectIDrequiredroles.iam.aws.m.upbound.ioprojectiammembers.cloudplatform.gcp.m.upbound.ioInjectedIdentitymeans ambient Application Default Credentials, which under GKE Workload Identity resolve to the KSA's federated identity — the same end statePodIdentitygives on EKS, reached differently. No key is mounted and noiam.gke.ioannotation is set; that absence is design criterion 18.The ServiceAccount name is load-bearing
DeploymentRuntimeConfignames its ServiceAccountprovider-gcp, which is the subject hard-coded in theprincipal://string inopentofu/gcp/gke/init/iam.tf. Nothing validates that the two agree — a rename produces no error and no event, just permission failures that point at the API being called rather than at the identity. Documented at both ends.Functions are now shared
Moved to
infrastructure/base/crossplane/functions/, referenced by both clouds. They are cluster-scoped singletons with cloud-agnostic pins; two copies would let the versions drift, producing compositions that behave differently per cloud — a bug that reproduces on one cluster only.The AWS render is byte-identical before and after the move (
kustomize builddiffed againstorigin/main).One correction worth flagging
I first wrote
projectNumber: "${project_number}"in the EnvironmentConfig, expecting the quotes to stop a 12-digit number parsing as an int. Rendering showed kustomize strips quotes it considers unnecessary, so they never reach Flux's substitution.Rather than leave a comment claiming a fix that does not work, the quotes are gone and the real constraint is written down:
projectNumberarrives as an int, soGCPWorkloadIdentitymuststr()it when building the principal string. That is now a stated requirement on the composition work instead of a trap waiting in the other repo. The AWSaccountIdhas the same property and gets away with it only because nothing concatenates it.Deliberately not included
No Configuration package for GCP.
crossplane-configuration-gcpdoes not exist yet — the XRDs and Compositions belong inSmana/crossplane-configurationand need a package released there first. Pointing at an unpublished OCI tag would leave the Kustomization failing on every reconcile.So this installs a working provider with no API on top. That is still independently verifiable, and it verifies the riskiest part: a healthy
ClusterProviderConfigproves the Workload Identity binding actually authenticates.When that package lands, wiring it is a two-PR cutover with
prune: disabledon the first — packages adopt existing XRDs and preserve their uids, but Flux prune deletes them in between, taking every claim with them (#1774 / #1778).Also
Corrects the crossplane
README.md, whose "validating a composition" section still described renderingsql-instance-composition.yamlandexamples/— both left this repo in #1774. Replaced with a per-cloud table of what the directory actually owns.Evidence
./scripts/validate-manifests.shValid: 1214, Invalid: 0, Skipped: 0, all gates passed./scripts/validate-links.sh./scripts/validate-doc-claims.shkustomize builddiff vsorigin/mainempty${var}refs confirmed present in the ConfigMapgke/configurecreatesSkipped: 0matters: the newClusterProviderConfigandManagedResourceActivationPolicywere schema-checked, not ignored.Not deployed. GCP is at zero billable and this has not run against a live cluster. Criterion 18 gets verified on the next rebuild.