Skip to content

feat(gcp): Crossplane controller, provider and config tree - #1824

Merged
Smana merged 2 commits into
mainfrom
worktree-gcp-crossplane-tree
Aug 24, 2026
Merged

feat(gcp): Crossplane controller, provider and config tree#1824
Smana merged 2 commits into
mainfrom
worktree-gcp-crossplane-tree

Conversation

@Smana

@Smana Smana commented Aug 24, 2026

Copy link
Copy Markdown
Owner

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:

crossplane-controller  (crds)              → chart, cloud-agnostic
crossplane-providers   (wait: true)        → provider + runtime config + activation policy
crossplane-configuration                   → ProviderConfig + EnvironmentConfig + functions

wait: true on stage 2 is load-bearing. Without it the Kustomization reports Ready as soon as the Provider object exists, and stage 3 applies a ClusterProviderConfig whose 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:

AWS GCP
Credential source PodIdentity InjectedIdentity — GCP's enum contains no PodIdentity at all
Project / account derived from credentials explicit projectID required
IAM Kind roles.iam.aws.m.upbound.io projectiammembers.cloudplatform.gcp.m.upbound.io

InjectedIdentity means ambient Application Default Credentials, which under GKE Workload Identity resolve to the KSA's federated identity — the same end state PodIdentity gives on EKS, reached differently. No key is mounted and no iam.gke.io annotation is set; that absence is design criterion 18.

The ServiceAccount name is load-bearing

DeploymentRuntimeConfig names its ServiceAccount provider-gcp, which is the subject hard-coded in the principal:// string in opentofu/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 build diffed against origin/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: projectNumber arrives as an int, so GCPWorkloadIdentity must str() 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 AWS accountId has the same property and gets away with it only because nothing concatenates it.

Deliberately not included

No Configuration package for GCP. crossplane-configuration-gcp does not exist yet — the XRDs and Compositions belong in Smana/crossplane-configuration and 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 ClusterProviderConfig proves the Workload Identity binding actually authenticates.

When that package lands, wiring it is a two-PR cutover with prune: disabled on 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 rendering sql-instance-composition.yaml and examples/ — both left this repo in #1774. Replaced with a per-cloud table of what the directory actually owns.

Evidence

Check Result
./scripts/validate-manifests.sh exit 0 — Valid: 1214, Invalid: 0, Skipped: 0, all gates passed
./scripts/validate-links.sh exit 0
./scripts/validate-doc-claims.sh exit 0
AWS render unchanged kustomize build diff vs origin/main empty
Substitution vars all 8 ${var} refs confirmed present in the ConfigMap gke/configure creates

Skipped: 0 matters: the new ClusterProviderConfig and ManagedResourceActivationPolicy were 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.

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.
@github-actions

github-actions Bot commented Aug 24, 2026

Copy link
Copy Markdown
Contributor

🔍 Rendered manifest diff — this PR vs main (desired state)

0 changed · 8 added · 0 removed

Rendered with kustomize build + helm template (source of truth = git), so Helm-expanded workloads are included. Shows what Flux will apply — not a diff against live cluster state (drift is alerted on separately), and not CRD-defaulted / webhook-mutated output. Secret values are redacted; per-render noise (webhook caBundles, checksum/* annotations, render timestamps) is normalized out.

🟢 added — apiextensions.crossplane.io/v1alpha1/ManagedResourceActivationPolicy/-/gcp-resources
--- a/apiextensions.crossplane.io/v1alpha1/ManagedResourceActivationPolicy/-/gcp-resources
+++ b/apiextensions.crossplane.io/v1alpha1/ManagedResourceActivationPolicy/-/gcp-resources
@@ -0,0 +1,7 @@
+apiVersion: apiextensions.crossplane.io/v1alpha1
+kind: ManagedResourceActivationPolicy
+metadata:
+  name: gcp-resources
+spec:
+  activate:
+  - projectiammembers.cloudplatform.gcp.m.upbound.io
🟢 added — apiextensions.crossplane.io/v1beta1/EnvironmentConfig/crossplane-system/gke-environment
--- a/apiextensions.crossplane.io/v1beta1/EnvironmentConfig/crossplane-system/gke-environment
+++ b/apiextensions.crossplane.io/v1beta1/EnvironmentConfig/crossplane-system/gke-environment
@@ -0,0 +1,14 @@
+apiVersion: apiextensions.crossplane.io/v1beta1
+data:
+  clusterName: foobar
+  networkName: vpc-foobar
+  privateDomainName: priv.cluster.local
+  projectID: ogenki-435905
+  projectNumber: 323586397743
+  region: eu-west-3
+  workloadPool: ogenki-435905.svc.id.goog
+  zone: europe-west4-a
+kind: EnvironmentConfig
+metadata:
+  name: gke-environment
+  namespace: crossplane-system
🟢 added — gcp.m.upbound.io/v1beta1/ClusterProviderConfig/crossplane-system/default
--- a/gcp.m.upbound.io/v1beta1/ClusterProviderConfig/crossplane-system/default
+++ b/gcp.m.upbound.io/v1beta1/ClusterProviderConfig/crossplane-system/default
@@ -0,0 +1,9 @@
+apiVersion: gcp.m.upbound.io/v1beta1
+kind: ClusterProviderConfig
+metadata:
+  name: default
+  namespace: crossplane-system
+spec:
+  credentials:
+    source: InjectedIdentity
+  projectID: ogenki-435905
🟢 added — pkg.crossplane.io/v1/Function/-/function-auto-ready
--- a/pkg.crossplane.io/v1/Function/-/function-auto-ready
+++ b/pkg.crossplane.io/v1/Function/-/function-auto-ready
@@ -0,0 +1,6 @@
+apiVersion: pkg.crossplane.io/v1
+kind: Function
+metadata:
+  name: function-auto-ready
+spec:
+  package: xpkg.crossplane.io/crossplane-contrib/function-auto-ready:v0.7.0
🟢 added — pkg.crossplane.io/v1/Function/-/function-environment-configs
--- a/pkg.crossplane.io/v1/Function/-/function-environment-configs
+++ b/pkg.crossplane.io/v1/Function/-/function-environment-configs
@@ -0,0 +1,6 @@
+apiVersion: pkg.crossplane.io/v1
+kind: Function
+metadata:
+  name: function-environment-configs
+spec:
+  package: xpkg.crossplane.io/crossplane-contrib/function-environment-configs:v0.7.2
🟢 added — pkg.crossplane.io/v1/Function/-/function-kcl
--- a/pkg.crossplane.io/v1/Function/-/function-kcl
+++ b/pkg.crossplane.io/v1/Function/-/function-kcl
@@ -0,0 +1,6 @@
+apiVersion: pkg.crossplane.io/v1
+kind: Function
+metadata:
+  name: function-kcl
+spec:
+  package: xpkg.upbound.io/crossplane-contrib/function-kcl:v0.12.1
🟢 added — pkg.crossplane.io/v1/Provider/-/provider-gcp-cloudplatform
--- a/pkg.crossplane.io/v1/Provider/-/provider-gcp-cloudplatform
+++ b/pkg.crossplane.io/v1/Provider/-/provider-gcp-cloudplatform
@@ -0,0 +1,8 @@
+apiVersion: pkg.crossplane.io/v1
+kind: Provider
+metadata:
+  name: provider-gcp-cloudplatform
+spec:
+  package: xpkg.crossplane.io/crossplane-contrib/provider-gcp-cloudplatform:v2.6.0
+  runtimeConfigRef:
+    name: gcp-config
🟢 added — pkg.crossplane.io/v1beta1/DeploymentRuntimeConfig/-/gcp-config
--- a/pkg.crossplane.io/v1beta1/DeploymentRuntimeConfig/-/gcp-config
+++ b/pkg.crossplane.io/v1beta1/DeploymentRuntimeConfig/-/gcp-config
@@ -0,0 +1,9 @@
+apiVersion: pkg.crossplane.io/v1beta1
+kind: DeploymentRuntimeConfig
+metadata:
+  name: gcp-config
+spec:
+  deploymentTemplate: {}
+  serviceAccountTemplate:
+    metadata:
+      name: provider-gcp

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
Smana merged commit dfaf203 into main Aug 24, 2026
8 checks passed
@Smana
Smana deleted the worktree-gcp-crossplane-tree branch August 24, 2026 13:48
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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant