Initial scaffold: generation-rule-only Crossplane Bucket example
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>
This commit is contained in:
@@ -0,0 +1,139 @@
|
||||
# 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/v1beta1`** — the API group and preferred
|
||||
version.
|
||||
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`.
|
||||
|
||||
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).
|
||||
Reference in New Issue
Block a user