eb8160e659
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>
179 lines
5.9 KiB
Markdown
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).
|