31ac475c97
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.
146 lines
5.4 KiB
Markdown
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).
|