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>
This commit is contained in:
@@ -0,0 +1,178 @@
|
||||
# 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).
|
||||
Reference in New Issue
Block a user