template-tags-hierarchy · git:20260309.b06c8ee · 2026-03-09 · sha256 86d1dac5fd354277

template-tags-hierarchy git:20260309.b06c8eeA

Immutable. This exact content never changes and is served at /api/v1/blob/86d1dac5fd354277.

---
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.

### 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.

## 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) |
| `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:

```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 %}
    - name: resource_name
      value: '{{ match_resource.resource_type.name | 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`).

### child_resource deduplication

Always normalize BOTH sides of the comparison identically before comparing:

```jinja
{% set _resource_name_value = (...) | 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.

## Adding a new platform

1. Create `src/templates/<platform>-hierarchy.yaml` following the leaf rule.
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.