template-tags-hierarchy · diff
git:20260309.b06c8ee to git:20260309.838f3b7
41 added, 36 removed. Audit A to A.
---
description: Conventions for platform tag and hierarchy Jinja templates (src/templates/*-tags.yaml, *-hierarchy.yaml)
globs: src/templates/*-tags.yaml,src/templates/*-hierarchy.yaml
alwaysApply: false
---
# Tag & Hierarchy Template Contract
All platform templates (`gcp`, `aws`, `azure`, `kubernetes`) follow the same
structural rules. When adding a new provider or modifying an existing one,
enforce every rule below.
## Hierarchy template (*-hierarchy.yaml)
The hierarchy defines the resourcePath segments. Every entry MUST have a
corresponding tag of the same name in the tags template.
+ ### Structure
+
+ ```
+ platform → parent scopes → resource_name
+ ```
+
+ The hierarchy ALWAYS ends with `resource_name`. `resource_type` NEVER appears
+ in the hierarchy. Non-qualifier details (like `location`) are tags only, not
+ hierarchy entries.
+
### Leaf entry rule
```jinja
- {% if qualifiers and qualifiers.resource is defined %}
- resource_name
- {% elif match_resource.resource_type is defined %}
- - resource_type
- {% endif %}
```
- - **Resource-scoped** (qualifiers include `resource`): leaf is `resource_name`.
- - **Group-scoped** (qualifiers do NOT include `resource`): leaf is `resource_type`.
- - NEVER fall back to `resource_name` via `match_resource.name` for group-scoped
- SLXs — that picks one arbitrary child and produces a misleading path.
+ Unconditional — no branching on qualifiers or resource_type.
## Tags template (*-tags.yaml)
### Required tags (all platforms)
Every tags template MUST emit these tags when values are available:
| Tag | When emitted |
|-----|-------------|
| `platform` | Always |
| `resource_type` | Always (from `match_resource.resource_type.name`; for K8s CRDs use `kind \| lower`) |
- | `resource_name` | Always (from qualifiers, `match_resource.name` if <= 1 child, or `resource_type` if many) |
+ | `resource_name` | Always (from most-specific qualifier, or `match_resource.name` fallback) |
| `child_resource` | For each entry in `child_resource_names`, unless it equals `resource_name` |
### resource_name emission rule
- `resource_name` is ALWAYS emitted. The value depends on context:
+ `resource_name` is ALWAYS the value of the most-specific qualifier present,
+ determined by a fixed specificity chain per platform. The order qualifiers
+ appear in the generation rule does NOT affect the output.
+ Specificity chains (most → least specific):
+ - GCP: `resource` > `project`
+ - Kubernetes: `resource` > `namespace` > `cluster`
+ - AWS: `resource` > `region` > `account_name` > `account_id`
+ - Azure ARM: `resource` > `resource_group` > `subscription_name` > `subscription_id`
+ - Azure DevOps: `resource` > `project` > `organization`
+
+ Example (GCP):
```jinja
- {% if qualifiers and qualifiers.resource is defined %}
- - name: resource_name
- value: '{{ qualifiers.resource | string }}'
- {% elif qualifiers and qualifiers.resource_group is defined %}
- - name: resource_name
- value: '{{ qualifiers.resource_group | string }}'
- {% elif match_resource.name is defined and (child_resource_names | default([]) | length) <= 1 %}
- - name: resource_name
- value: '{{ match_resource.name | string }}'
- {% elif match_resource.resource_type is defined %}
+ {% set _rn_val = qualifiers.resource if qualifiers and qualifiers.resource is defined else (qualifiers.project if qualifiers and qualifiers.project is defined else (match_resource.name if match_resource.name is defined else '')) %}
+ {% if _rn_val %}
- name: resource_name
- value: '{{ match_resource.resource_type.name | string }}'
+ value: '{{ _rn_val | string }}'
{% endif %}
```
- - **Resource-scoped**: from qualifier value (specific instance name).
- - **Group-scoped, 1 child**: from `match_resource.name` (the resource IS
- the scope); the matching child is deduplicated.
- - **Group-scoped, many children**: from `resource_type` (matches the last
- hierarchy segment); all children appear as `child_resource` tags.
-
- For Kubernetes CRDs with multiple children, use `match_resource.kind | lower`
- instead of `match_resource.resource_type.name` (which is the generic `custom`).
+ resource_name is NEVER set to `resource_type`. It always represents the
+ scope of the SLX — what is being monitored.
### child_resource deduplication
Always normalize BOTH sides of the comparison identically before comparing:
```jinja
- {% set _resource_name_value = (...) | default('') | string | replace(":", "_") | replace("/", "_") %}
+ {% set _resource_name_value = _rn_val | default('') | string | replace(":", "_") | replace("/", "_") %}
{% for cr in child_resource_names | default([]) %}
{% set _cr_value = cr | string | replace(":", "_") | replace("/", "_") %}
{% if _cr_value != _resource_name_value %}
- name: child_resource
value: '{{ _cr_value }}'
{% endif %}
{% endfor %}
```
Both `_resource_name_value` and `_cr_value` MUST apply the same `replace`
filters. Asymmetric normalization defeats deduplication.
### Kubernetes CRD resource_type
For Kubernetes custom resources, `match_resource.resource_type.name` is the
generic `"custom"`. The tags template MUST use `match_resource.kind | lower`
instead to produce a meaningful type (e.g. `kustomization`, `helmrelease`):
```jinja
{% if match_resource.resource_type.name == "custom" and match_resource.kind is defined %}
- name: resource_type
value: '{{ match_resource.kind | lower | string }}'
{% elif match_resource.resource_type %}
- name: resource_type
value: '{{ match_resource.resource_type.name | string }}'
{% endif %}
```
### Kubernetes CRD resource_name
CRD resources are registered with their simple K8s metadata name (e.g.
`online-boutique-prod`), NOT the composite `plural_group_version_name`.
The composite is only used for the internal `qualified_name` registry key.
Do NOT construct or expect composite CRD names in templates.
### Platform-specific tags
Provider-specific tags (e.g. `[aws]key`, `[k8s]label`, `[azure]tag`,
`gcp_label_*`) are appended AFTER the standard tags above.
## resourcePath (computed at render time)
`resourcePath` is NEVER set in templates. It is always derived from
`hierarchy + tags` by `compute_resource_path_from_hierarchy()` in
- `src/renderers/render_output_items.py`. Do not add `resourcePath` or
- `qualified_name` logic to tag/hierarchy templates.
+ `src/renderers/render_output_items.py`.
+ The path is built by walking the hierarchy in order and joining tag values
+ with `/`. `resource_name` is ALWAYS included — even when its value duplicates
+ a parent entry. Duplication in the path is expected and intentional.
+
+ Do not add `resourcePath` or `qualified_name` logic to tag/hierarchy templates.
+
## Adding a new platform
- 1. Create `src/templates/<platform>-hierarchy.yaml` following the leaf rule.
+ 1. Create `src/templates/<platform>-hierarchy.yaml` ending with `- resource_name`.
2. Create `src/templates/<platform>-tags.yaml` emitting all required tags.
- 3. Ensure `resource_type` is always emitted as a tag.
- 4. Include the `child_resource` deduplication loop.
- 5. Add platform-specific enrichment tags after the standard block.
+ 3. Ensure `resource_type` is always emitted as a tag (but never in hierarchy).
+ 4. Emit `resource_name` from the first qualifier value.
+ 5. Include the `child_resource` deduplication loop.
+ 6. Add platform-specific enrichment tags after the standard block.