Files
stewartshea eb8160e659 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>
2026-06-30 21:51:04 -04:00

5.9 KiB

05 — SLX and templates

What you'll do

Read the three Jinja templates that render the artifacts for every discovered Bucket.

The three templates

Output type Template file Kind produced
slx gcp-bucket-crossplane-health-slx.yaml ServiceLevelX
sli gcp-bucket-crossplane-health-sli.yaml ServiceLevelIndicator
runbook gcp-bucket-crossplane-health-taskset.yaml Runbook

The SLX template

gcp-bucket-crossplane-health-slx.yaml

Key sections:

spec:
  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

configProvided on the SLX itself is displayed in the UI as "configured properties" of the service. It's not fed to Robot code — that's what the SLI and Runbook configProvided blocks are for. Use it for values that describe the thing being monitored, not values passed to the robot doing the monitoring.

The template also uses two shared includes shipped by workspace-builder:

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
  • kubernetes-hierarchy.yaml — emits the hierarchy: [platform, cluster, resource_name] list used to build the SLX's UI breadcrumb.
  • kubernetes-tags.yaml — emits platform, cluster, resource_type, resource_name, and every Kubernetes label as a [k8s]<key> tag. It also emits resource_type: bucket because match_resource.resource_type.name is custom for CRDs (the include handles that special case).

The SLI template

gcp-bucket-crossplane-health-sli.yaml

The most important line is the codeBundle block:

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

repoUrl deliberately points at the airgap cc-catalog proxy, not at this private CC and not at public GitHub. That's how the runner reaches rw-generic-codecollection — it's the URL the runner already trusts and mirrors.

If you move this example to another environment, change this URL to wherever the generic collection is served: public GitHub for internet-connected environments, a public JCR / mirror otherwise.

The configProvided block wires up k8s-kubectl-cmd's expected inputs:

configProvided:
  - name: TASK_TITLE
    value: 'Crossplane GCP Bucket {{match_resource.resource.metadata.name}} condition health'
  - 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'

The jq breakdown:

Fragment Meaning
(.status.conditions // []) as $c Save the conditions array (or empty array if missing) into $c
if ($c|length)==0 then 0 No conditions yet → return 0 (unhealthy). Signals "the resource has not reconciled".
else ([$c[]|select(.status=="True")]|length) / ($c|length) Otherwise fraction of True conditions over total

RW.Core.Push Metric ${rsp.stdout} inside the generic SLI robot takes that value (a JSON number between 0 and 1) and pushes it to the platform.

Finally the secrets block:

secretsProvided:
{% if wb_version %}
  {% include "kubernetes-auth.yaml" ignore missing %}
{% else %}
  - name: kubeconfig
    workspaceKey: {{custom.kubeconfig_secret_name}}
{% endif %}

wb_version is set only by newer workspace-builders. This lets the same template render correctly against both old and new runners. The kubeconfig_secret_name value comes from the custom: block in your runner's workspaceInfo.yaml (which we don't need to change — the existing airgap runner already provides k8s:file@secret/kubeconfig:kubeconfig).

The TaskSet template

gcp-bucket-crossplane-health-taskset.yaml

Same shape as the SLI, but pointing at runbook.robot and with a bigger jq expression that emits the JSON issue envelope:

- 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

These four variables are how k8s-kubectl-cmd/runbook.robot decides whether to parse the stdout as JSON. When enabled, it looks for an object like:

{
  "issuesIdentified": true,
  "issues": [
    {
      "title": "...",
      "severity": 2,
      "expected": "...",
      "actual": "...",
      "reproduce_hint": "...",
      "next_steps": "...",
      "details": "..."
    }
  ]
}

Our KUBECTL_COMMAND produces exactly that shape from .status.conditions[]. One issue per condition that is not True. Ready failures are severity 2, others (like Synced) are severity 3.

Screenshot placeholders

  • images/05-rendered-slx.png — a rendered SLX YAML in the RunWhen UI (from the SLX detail view).
  • images/05-tags-and-hierarchy.png — the SLX detail sidebar showing the hierarchy and tags produced by the shared includes.

Next

Continue to 06 — Runner integration.