Files
simple-private-codecollection/docs/03-crossplane-crd-discovery.md
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

146 lines
5.4 KiB
Markdown

# 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
```bash
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
```bash
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:
```bash
kubectl get buckets.storage.gcp.upbound.io <name> -o json | jq .status.conditions
```
Expected shape:
```json
[
{
"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`](https://github.com/runwhen-contrib/runwhen-local/blob/main/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:
```yaml
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`](https://github.com/runwhen/infra-flux-nonprod-shared/blob/main/apps/runwhen-env/airgap/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](04-generation-rule-walkthrough.md).