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:
stewartshea
2026-06-30 21:51:04 -04:00
commit eb8160e659
15 changed files with 1244 additions and 0 deletions
@@ -0,0 +1,34 @@
apiVersion: runwhen.com/v1
kind: GenerationRules
spec:
platform: kubernetes
generationRules:
# Discover every Crossplane GCP Bucket in the cluster.
# Resource type syntax: plural.group/version
# plural = buckets (from `kubectl api-resources | grep storage.gcp.upbound.io`)
# group = storage.gcp.upbound.io
# version = v1beta1 (omit /version to use the API server's preferred version)
- resourceTypes:
- buckets.storage.gcp.upbound.io/v1beta1
matchRules:
# Match every Bucket by name. Swap this regex to scope by naming
# convention (for example `^prod-.*` to only cover prod buckets) or
# use `type: exists` on a label like `metadata/labels/env`.
- type: pattern
pattern: ".+"
properties: [name]
mode: substring
slxs:
# Crossplane Buckets are cluster-scoped, so we qualify by `resource`
# (the bucket name) and `cluster` only — there is no namespace.
- baseName: xp-bkt-hlth
qualifiers: ["resource", "cluster"]
baseTemplateName: gcp-bucket-crossplane-health
levelOfDetail: detailed
outputItems:
- type: slx
- type: sli
- type: runbook
# Runbook uses the *-taskset.yaml template convention because
# RunWhen historically calls a Runbook a "TaskSet" in the UI.
templateName: gcp-bucket-crossplane-health-taskset.yaml
@@ -0,0 +1,50 @@
apiVersion: runwhen.com/v1
kind: ServiceLevelIndicator
metadata:
name: {{slx_name}}
labels:
{% include "common-labels.yaml" %}
annotations:
{% include "common-annotations.yaml" %}
spec:
displayUnitsLong: Percentage
displayUnitsShort: '%'
locations:
- {{default_location}}
description: >-
Fraction of Crossplane .status.conditions[] that are True for
GCP Bucket {{match_resource.resource.metadata.name}}. Healthy = 1.0.
# NOTE: This SLI runs from a *different* codecollection than the one
# that owns this generation rule. The generic runtime (`k8s-kubectl-cmd`)
# lives in `rw-generic-codecollection` — it is already loaded by the
# airgap runner. If you move this example to another environment,
# change `repoUrl` to wherever `rw-generic-codecollection` is served
# (public GitHub, an internal proxy, a Gitea mirror, etc.).
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
intervalStrategy: intermezzo
intervalSeconds: 300
configProvided:
- name: TASK_TITLE
value: 'Crossplane GCP Bucket {{match_resource.resource.metadata.name}} condition health'
# jq computes truthyConditions / totalConditions. Returns 0 when the
# resource has never reconciled (no conditions yet), which correctly
# signals "unhealthy" to the SLO layer.
- name: KUBECTL_COMMAND
value: |
kubectl get buckets.storage.gcp.upbound.io {{match_resource.resource.metadata.name}} -o json | jq -r '(.status.conditions // []) as $c | if ($c|length)==0 then 0 else ([$c[]|select(.status=="True")]|length)/($c|length) end'
- name: TIMEOUT_SECONDS
value: '120'
secretsProvided:
{% if wb_version %}
{% include "kubernetes-auth.yaml" ignore missing %}
{% else %}
- name: kubeconfig
workspaceKey: {{custom.kubeconfig_secret_name}}
{% endif %}
alertConfig:
tasks:
persona: eager-edgar
sessionTTL: 10m
@@ -0,0 +1,36 @@
apiVersion: runwhen.com/v1
kind: ServiceLevelX
metadata:
name: {{slx_name}}
labels:
{% include "common-labels.yaml" %}
annotations:
{% include "common-annotations.yaml" %}
spec:
imageURL: https://storage.googleapis.com/runwhen-nonprod-shared-images/icons/kubernetes.svg
alias: Crossplane GCP Bucket {{match_resource.resource.metadata.name}} Health
asMeasuredBy: Fraction of Crossplane .status.conditions[] that are True (Ready + Synced).
configProvided:
- name: OBJECT_NAME
value: {{match_resource.resource.metadata.name}}
- name: API_GROUP
value: storage.gcp.upbound.io
- name: KIND
value: Bucket
owners:
- {{workspace.owner_email}}
statement: >-
Crossplane GCP Bucket {{match_resource.resource.metadata.name}} should have
every status condition (Ready, Synced) in state True 99.5% of the time.
additionalContext:
{% include "kubernetes-hierarchy.yaml" ignore missing %}
qualified_name: "{{ match_resource.qualified_name }}"
tags:
{% include "kubernetes-tags.yaml" ignore missing %}
- name: access
value: read-only
- name: managed-by
value: crossplane
- name: provider
value: upbound-gcp
@@ -0,0 +1,47 @@
apiVersion: runwhen.com/v1
kind: Runbook
metadata:
name: {{slx_name}}
labels:
{% include "common-labels.yaml" %}
annotations:
{% include "common-annotations.yaml" %}
spec:
location: {{default_location}}
# See the SLI template for the rationale on hardcoding the generic
# codecollection URL here.
codeBundle:
repoUrl: http://rw-airgap-cc-catalog-svc.runwhen-env-airgap:8080/git/rw-generic-codecollection.git
ref: main
pathToRobot: codebundles/k8s-kubectl-cmd/runbook.robot
configProvided:
- name: TASK_TITLE
value: 'Inspect Crossplane GCP Bucket {{match_resource.resource.metadata.name}} conditions'
- name: TIMEOUT_SECONDS
value: '300'
# Enable JSON-issue mode in the generic runbook: it will parse the
# stdout JSON, check the trigger key, and emit one RunWhen issue per
# element in the issues array.
- name: ISSUE_JSON_QUERY_ENABLED
value: 'true'
- name: ISSUE_JSON_TRIGGER_KEY
value: issuesIdentified
- name: ISSUE_JSON_TRIGGER_VALUE
value: 'true'
- name: ISSUE_JSON_ISSUES_KEY
value: issues
# The `jq` expression below emits the JSON envelope that the generic
# runbook expects:
# { issuesIdentified: <bool>, issues: [ {title,severity,...}, ... ] }
# One issue per condition whose status is not "True". Severity 2 for
# Ready failures (data-plane visible), 3 for others (e.g. Synced).
- name: KUBECTL_COMMAND
value: |
kubectl get buckets.storage.gcp.upbound.io {{match_resource.resource.metadata.name}} -o json | jq -c '(.status.conditions // []) as $c | { issuesIdentified: (($c|length)==0 or ([$c[]|select(.status!="True")]|length > 0)), issues: ( if ($c|length)==0 then [ { title: "Crossplane Bucket {{match_resource.resource.metadata.name}} has no status conditions yet", severity: 3, expected: "Bucket should have Ready and Synced conditions populated by the Crossplane provider", actual: "status.conditions is empty or missing", reproduce_hint: "kubectl get buckets.storage.gcp.upbound.io {{match_resource.resource.metadata.name}} -o yaml", next_steps: "1. Check that the crossplane-system provider pod for provider-gcp-storage is Running.\n2. kubectl describe buckets.storage.gcp.upbound.io {{match_resource.resource.metadata.name}} to see reconciliation events.\n3. Verify the referenced ProviderConfig exists and is Healthy.\n4. Wait one reconcile interval and re-check.", details: (.status | tostring) } ] else [ $c[] | select(.status!="True") | { title: ("Crossplane Bucket " + .type + " condition is not True: {{match_resource.resource.metadata.name}}"), severity: (if .type=="Ready" then 2 else 3 end), expected: (.type + " condition should be True"), actual: (.type + "=" + .status + " reason=" + (.reason // "n/a") + " message=" + (.message // "n/a")), reproduce_hint: "kubectl get buckets.storage.gcp.upbound.io {{match_resource.resource.metadata.name}} -o yaml", next_steps: "1. kubectl describe buckets.storage.gcp.upbound.io {{match_resource.resource.metadata.name}}\n2. Inspect the crossplane-system provider-gcp-storage pod logs for reconcile errors.\n3. Verify the ProviderConfig references a Secret with valid GCP credentials.\n4. Check GCP IAM permissions on the target bucket / project.\n5. Look for spec drift and wait for the next reconcile.", details: (. | tostring) } ] end ) }'
secretsProvided:
{% if wb_version %}
{% include "kubernetes-auth.yaml" ignore missing %}
{% else %}
- name: kubeconfig
workspaceKey: {{custom.kubeconfig_secret_name}}
{% endif %}
@@ -0,0 +1,49 @@
# gcp-bucket-crossplane-health
A **generation-rule-only** codebundle. It has no `sli.robot`, no
`runbook.robot`, and no `meta.yaml`. It ships:
| File | Role |
| --- | --- |
| [`.runwhen/generation-rules/gcp-bucket-crossplane-health.yaml`](.runwhen/generation-rules/gcp-bucket-crossplane-health.yaml) | Tells workspace-builder to discover every Crossplane `Bucket` (`storage.gcp.upbound.io/v1beta1`) in the cluster and generate one SLX per bucket. |
| [`.runwhen/templates/gcp-bucket-crossplane-health-slx.yaml`](.runwhen/templates/gcp-bucket-crossplane-health-slx.yaml) | Jinja template for the `ServiceLevelX` resource. |
| [`.runwhen/templates/gcp-bucket-crossplane-health-sli.yaml`](.runwhen/templates/gcp-bucket-crossplane-health-sli.yaml) | Jinja template for the `ServiceLevelIndicator`. Points at `rw-generic-codecollection/k8s-kubectl-cmd/sli.robot`. |
| [`.runwhen/templates/gcp-bucket-crossplane-health-taskset.yaml`](.runwhen/templates/gcp-bucket-crossplane-health-taskset.yaml) | Jinja template for the `Runbook` (TaskSet). Points at `rw-generic-codecollection/k8s-kubectl-cmd/runbook.robot` in JSON-issue mode. |
## How it works
1. `workspace-builder` loads this codecollection and reads the generation rule.
2. The rule declares `resourceTypes: [buckets.storage.gcp.upbound.io/v1beta1]`.
RunWhen Local's Kubernetes indexer expands that spec into
`plural=buckets, group=storage.gcp.upbound.io, version=v1beta1`, calls
`list_cluster_custom_object`, and stores each Bucket in the registry.
3. The `matchRules` predicate matches every Bucket by name (regex `.+`).
4. For each match, the rule emits an SLX with three output items (`slx`,
`sli`, `runbook`). Each output item is rendered from the corresponding
Jinja template.
5. The SLI and Runbook templates set `codeBundle.repoUrl` to the internal
airgap URL for `rw-generic-codecollection`, and `pathToRobot` to
`k8s-kubectl-cmd/{sli,runbook}.robot`. That's where the actual runtime
lives — this codecollection ships only the discovery and templating
logic.
## Health scoring
- **SLI** — the `KUBECTL_COMMAND` runs `kubectl get bucket ... -o json`, pipes
through `jq` to compute the fraction of `.status.conditions[]` whose
`status == "True"`, and pushes that number (0.0 to 1.0) as the metric.
A healthy Crossplane Bucket has `Ready=True` **and** `Synced=True`,
producing an SLI of `1.0`.
- **TaskSet** — the same generic bundle in JSON-issue mode. The `jq`
expression emits `{ issuesIdentified: true|false, issues: [...] }`.
Each condition whose status is not `True` becomes one issue with a
severity (higher for `Ready`), a `reproduce_hint`, and human-readable
`next_steps`.
## Why this pattern is useful
- **Reuse over rewrite**: no need to fork or maintain another robot file.
- **Small blast radius**: a private repo like this one holds just the
organization-specific discovery logic and messaging.
- **Fast iteration**: change the `jq` filter and the workspace-builder
picks it up on the next reconcile — no runner image rebuild required.