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>
4.6 KiB
02 — The generation-rule-only pattern
What you'll do
Understand the split of responsibilities between this private code
collection (rules + templates only) and the shared rw-generic-codecollection
(the actual Robot runtime).
Traditional codebundle vs. rules-only codebundle
A traditional codebundle looks like this:
codebundles/k8s-my-thing/
meta.yaml
README.md
runbook.robot # ← runtime
sli.robot # ← runtime
.runwhen/
generation-rules/*.yaml
templates/*.yaml # templates reference this bundle's own robots
A rules-only codebundle looks like this — this is the pattern we are teaching:
codebundles/gcp-bucket-crossplane-health/
README.md
.runwhen/
generation-rules/gcp-bucket-crossplane-health.yaml
templates/
gcp-bucket-crossplane-health-slx.yaml
gcp-bucket-crossplane-health-sli.yaml
gcp-bucket-crossplane-health-taskset.yaml
Notice: no meta.yaml, no runbook.robot, no sli.robot. The templates
still reference pathToRobot, but they point at a different
code collection.
The delegation trick
Look at gcp-bucket-crossplane-health-sli.yaml:
codeBundle:
repoUrl: http://rw-airgap-cc-catalog-svc.runwhen-env-airgap:8080/git/rw-generic-codecollection.git
ref: main
pathToRobot: codebundles/k8s-kubectl-cmd/sli.robot
configProvided:
- name: KUBECTL_COMMAND
value: |
kubectl get buckets.storage.gcp.upbound.io {{match_resource.resource.metadata.name}} ...
repoUrl points at the airgap cc-catalog proxy for
rw-generic-codecollection. pathToRobot picks the generic
k8s-kubectl-cmd/sli.robot. configProvided.KUBECTL_COMMAND is the actual
command we want that generic robot to run — parameterized by the discovered
resource's name via the match_resource Jinja variable.
At runtime, the workflow is:
sequenceDiagram
participant WB as workspace-builder
participant CR as Cluster (kubeapi)
participant Repo as private CC (Gitea)
participant PAPI as RunWhen Platform
participant Worker as Runner Worker
participant Generic as rw-generic-codecollection
WB->>Repo: git clone (with token)
WB->>CR: list buckets.storage.gcp.upbound.io
CR-->>WB: [ Bucket A, Bucket B, ... ]
WB->>WB: match rule; render templates
WB->>PAPI: upload N SLXs (each with codeBundle.repoUrl = rw-generic)
PAPI->>Worker: schedule SLI + TaskSet runs
Worker->>Generic: clone at ref=main; load k8s-kubectl-cmd
Worker->>CR: KUBECTL_COMMAND (kubectl + jq)
CR-->>Worker: JSON status
Worker->>PAPI: push metric / issues
Why not just put the robot files in the private CC?
Because then every consumer of the private CC would need to run the
runner-side clone and image handling for those robots. The airgap runner's
runner.codeCollections list would have to include this repo (with the
same token concerns). By keeping the private CC as rules and templates
only, you never have to update the runner-side collections list — the
Robot code always resolves to the same shared, cached, image-baked
generic collection that the runner already knows how to load.
The template variables you have
At render time, workspace-builder passes each template a rich context. The most useful entries for our use case:
| Variable | Value in our case |
|---|---|
slx_name |
Generated SLX name, e.g. shared-cluster-runwhen-nonprod-shared-litellm-logging-xp-bkt-hlth |
match_resource.resource |
The full Kubernetes object as returned by the API |
match_resource.resource.metadata.name |
The Bucket's name |
match_resource.kind |
Bucket (for CRDs, filled by the indexer) |
match_resource.qualified_name |
The composite ID used by the resource registry |
qualifiers |
The dict populated per your qualifiers list in the rule (resource, cluster) |
cluster.name |
The Kubernetes cluster name (e.g. shared-cluster) |
default_location |
Workspace default runner location |
workspace.owner_email |
From workspaceInfo.workspaceOwnerEmail |
custom.kubeconfig_secret_name |
From the custom: block in workspaceInfo |
wb_version |
Set only in newer workspace-builders (used to gate the newer kubernetes-auth.yaml include) |
Screenshot placeholders
images/02-side-by-side-file-tree.png— screenshot of the two directory trees (traditional vs. rules-only) side by side.images/02-render-sequence.png— annotated version of the sequence diagram above (optional).
Next
Continue to 03 — Crossplane CRD discovery.