Files
simple-private-codecollection/docs/03-crossplane-crd-discovery.md
T
stewartshea 31ac475c97 Update Crossplane GCP Bucket generation rules to use preferred API version
Modified the generation rules for Crossplane GCP Buckets to omit the version in resource type definitions, allowing the use of the API server's preferred version (currently v1beta2). Updated documentation to reflect this change and clarify version pinning for schema stability.

Changes include:
- Updated YAML generation rules to remove explicit versioning.
- Revised documentation to explain the implications of omitting versioning and the preferred version usage.

This enhances resilience across CRD upgrades and simplifies the configuration for users.
2026-06-30 21:55:36 -04:00

5.4 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:

  1. buckets — this is the plural resource name. It is what you put in the generation rule, not the singular Bucket.
  2. storage.gcp.upbound.io/v1beta2 — the API group and preferred version (older v1beta1 may also be served for compatibility).
  3. false in the "NAMESPACED" column — Crossplane managed resources are cluster-scoped. This is why our generation rule uses qualifiers: ["resource", "cluster"] and not namespace.

Version pinning: our rule uses just buckets.storage.gcp.upbound.io (no /version), which tells the indexer to use whatever the API server reports as preferred. That's the most resilient choice across CRD upgrades. Pin /v1beta1 or /v1beta2 only when you deliberately need a specific schema.

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. When Ready=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. When Synced=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 the Kind.
  • Version omission — leaving off /v1beta1 uses 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/watch on the CRD. The airgap runner already binds workspace-builder SA to view at the cluster scope via rbac/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.namespaces list or leave that list empty.

Next

Continue to 04 — Generation rule walkthrough.