# 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]` 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).