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

179 lines
5.9 KiB
Markdown

# 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`](../codebundles/gcp-bucket-crossplane-health/.runwhen/templates/gcp-bucket-crossplane-health-slx.yaml)
Key sections:
```yaml
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:
```yaml
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`](../codebundles/gcp-bucket-crossplane-health/.runwhen/templates/gcp-bucket-crossplane-health-sli.yaml)
The most important line is the `codeBundle` block:
```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
```
`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:
```yaml
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:
```yaml
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`](../codebundles/gcp-bucket-crossplane-health/.runwhen/templates/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:
```yaml
- 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:
```json
{
"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](06-runner-integration.md).