Educational RunWhen CodeCollection that discovers Crossplane GCP Bucket CRDs (storage.gcp.upbound.io/v1beta1) and generates one SLX per bucket. Ships only generation rules and Jinja templates; the runtime lives in rw-generic-codecollection/k8s-kubectl-cmd (already loaded by the airgap runner). Includes: - codebundles/gcp-bucket-crossplane-health with generation rule + 3 templates - docs/01..07 numbered training chapters with screenshot placeholders - README, .gitignore Co-authored-by: Cursor <cursoragent@cursor.com>
5.1 KiB
03 — Crossplane CRD discovery
What you'll do
Understand how RunWhen's Kubernetes indexer discovers custom resources — specifically the Crossplane GCP Bucket CRD — driven entirely by what your generation rules ask for.
Step 1 — Confirm the CRD exists
kubectl api-resources | grep storage.gcp.upbound.io
Expected output (trimmed):
buckets storage.gcp.upbound.io/v1beta1 false Bucket
bucketiammembers storage.gcp.upbound.io/v1beta1 false BucketIAMMember
hmackeys storage.gcp.upbound.io/v1beta1 false HMACKey
Three things to note:
buckets— this is the plural resource name. It is what you put in the generation rule, not the singularBucket.storage.gcp.upbound.io/v1beta1— the API group and preferred version.falsein the "NAMESPACED" column — Crossplane managed resources are cluster-scoped. This is why our generation rule usesqualifiers: ["resource", "cluster"]and notnamespace.
Screenshot placeholder: images/03-kubectl-api-resources.png — output of
the above command.
Step 2 — Confirm buckets exist to discover
kubectl get buckets.storage.gcp.upbound.io -o wide
Sample output on the airgap cluster:
NAME READY SYNCED EXTERNAL-NAME
runwhen-nonprod-shared-litellm-logging True True runwhen-nonprod-shared-litellm-logging
runwhen-nonprod-shared-loki True True runwhen-nonprod-shared-loki
runwhen-nonprod-shared-mimir True True runwhen-nonprod-shared-mimir
runwhen-nonprod-shared-tempo True True runwhen-nonprod-shared-tempo
Screenshot placeholder: images/03-kubectl-buckets.png.
Step 3 — Look at the .status.conditions[] we care about
This is what the SLI and TaskSet will read. Pick any bucket and inspect it:
kubectl get buckets.storage.gcp.upbound.io <name> -o json | jq .status.conditions
Expected shape:
[
{
"lastTransitionTime": "2025-11-14T10:15:11Z",
"reason": "Available",
"status": "True",
"type": "Ready"
},
{
"lastTransitionTime": "2025-11-14T10:15:10Z",
"reason": "ReconcileSuccess",
"status": "True",
"type": "Synced"
}
]
Every Crossplane managed resource carries these two conditions:
Ready— the external resource (the actual GCS bucket in GCP) exists and is available. WhenReady=False, something in GCP is wrong (missing IAM, quota, deleted out-of-band, etc.).Synced— the last reconcile between the Crossplane spec and the provider succeeded. WhenSynced=False, Crossplane could not talk to the provider or hit a validation error.
Our SLI computes (#True conditions) / (#total conditions), so a healthy
bucket returns 1.0 and any single failing condition drops it to 0.5
or 0.0.
Step 4 — Understand how workspace-builder gets there
RunWhen Local's Kubernetes indexer performs selective discovery: it
only lists custom resource types that appear in loaded generation rules.
That's why simply declaring buckets.storage.gcp.upbound.io/v1beta1 in
our rule is enough — no extra customResourceTypes: list in
workspaceInfo.yaml.
The parsing (in
runwhen-local/src/indexers/kubetypes.py)
takes the string plural.group/version and splits it into
(plural='buckets', group='storage.gcp.upbound.io', version='v1beta1').
It then calls CustomObjectsApi.list_cluster_custom_object(...).
You can also specify custom resources with a dict form, useful when you need to be explicit about a particular version:
resourceTypes:
- platform: kubernetes
resourceType: custom
kind: buckets # plural, NOT PascalCase
group: storage.gcp.upbound.io
version: v1beta1
Common pitfalls
- Wrong plural — the biggest source of "nothing gets discovered". Always
copy the plural from
kubectl api-resources, never guess it from theKind. - Version omission — leaving off
/v1beta1uses the API server's preferred version. Usually fine but pin it explicitly if you rely on a specific schema. - RBAC — workspace-builder needs
get/list/watchon the CRD. The airgap runner already bindsworkspace-builderSA toviewat the cluster scope viarbac/workspace-builder-view-rbac.yaml, which covers CRDs installed after the binding was created. - Namespace scope — some CRDs are namespaced. If you had chosen a
namespaced CRD you would need to include the CRD's namespaces in your
cloudConfig.kubernetes.namespaceslist or leave that list empty.
Next
Continue to 04 — Generation rule walkthrough.