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>
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 thehierarchy: [platform, cluster, resource_name]list used to build the SLX's UI breadcrumb.kubernetes-tags.yaml— emitsplatform,cluster,resource_type,resource_name, and every Kubernetes label as a[k8s]<key>tag. It also emitsresource_type: bucketbecausematch_resource.resource_type.nameiscustomfor 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.