discovery-indexers · git:20260630.d9e89df · 2026-06-30 · sha256 38c1d0aeff93061f

discovery-indexers git:20260630.d9e89dfA

Immutable. This exact content never changes and is served at /api/v1/blob/38c1d0aeff93061f.

---
description: Contract for RunWhen Local discovery indexers (native SDK + CloudQuery). Enforces registry parity, collector registration, normalizer shape, and docs/test/generation-rule definition of done.
globs: src/indexers/**,src/enrichers/azure.py,src/enrichers/gcp.py,src/enrichers/aws.py,scripts/azure/**,scripts/gcp/**
alwaysApply: false
---

# Discovery indexer contract

RunWhen Local discovers resources via native SDK indexers (`azureapi`,
`gcpapi`, `awsapi`) plus the legacy `cloudquery` indexer. The native indexers
are now the **default** backend for all three clouds (`azureIndexerBackend`,
`gcpIndexerBackend`, `awsIndexerBackend` default to `azureapi`/`gcpapi`/`awsapi`;
the resource store defaults to `sqlite`). CloudQuery is retained only as an
explicit opt-in (`*IndexerBackend: cloudquery`) and exists to be removed
entirely. By default the CloudQuery indexer skips all three clouds. When editing
any indexer, enforce every rule below.

For full workflows use the `extend-discovery-type` and `add-discovery-type`
skills. Worked examples: `docs/architecture/{azure,gcp}-indexer-internals.md`.

## CloudQuery parity is the public contract

- The **CloudQuery table name** (e.g. `gcp_compute_instances`) is the canonical
  `resource_type` used in generation rules. Native indexers MUST normalize
  payloads into the same CloudQuery-shaped dict so rules don't change when the
  backend flips.
- Every CloudQuery table for a platform MUST resolve via the registry by its
  canonical name or a registered alias. Don't drop parity to simplify.

## Resource-type matching is alias-aware (accepted-name set)

- Each registry entry has an explicit **accepted-name set** (`match_names`) =
  canonical CloudQuery/native name + every legacy/alias name rules have used.
  The indexer stores **one canonical name** per resource and matching expands to
  the accepted-name set at lookup, so a rule matches a resource if its requested
  type is **any** accepted name (canonical OR legacy). Do NOT depend on
  positional `aliases[0]` ordering for matching.
- This is wired into BOTH the generation-rule matcher
  (`PlatformHandler.get_resources`) AND selective discovery (`find_spec` ->
  registry `find()`), via `src/resource_type_resolver.py`. Names unknown to
  every cloud registry (Kubernetes, custom, `azure_devops`) fall back to
  exact-string matching — preserve that fallback.
- NOTE: the indexer today still stores the legacy `aliases[0]` name; flipping
  stored values to the canonical (and then native) name is staged as Phase 2.
  See `docs/architecture/resource-type-naming-migration.md` for the contract and
  the phased plan.

## Generated registry — never hand-edit

`src/indexers/*_resource_type_registry.yaml` is GENERATED. To change a mapping:

1. Edit the overrides YAML (`scripts/<platform>/<platform>_resource_type_overrides.yaml`).
2. Re-run `scripts/<platform>/sync_<platform>_resource_type_registry.py`.
3. Commit both the overrides and the regenerated YAML; the sync `--dry-run`
   must report 0 drift.

Use `null` for the native type of tables with no API equivalent so the generic
pass skips them.

## Typed vs generic collectors

- The **generic collector** (Azure `resources.list()`, GCP Cloud Asset
  Inventory, AWS Cloud Control) provides broad parity.
- **Typed collectors** are enrichment for high-value types. Register them in
  `_TYPED_COLLECTORS` keyed by the canonical CloudQuery table name. A typed type
  is automatically excluded from the generic pass — write each resource once.
- Lazy-import SDK clients inside the collector so importing the module never
  hard-requires the SDK.

## Discovery is selective + anchored

- Collect only types referenced by loaded generation rules, plus the mandatory
  anchor (GCP `gcp_projects`, Azure `azure_resources_resource_groups`).
- Emit the anchor first so child resources link to it at parse time.
- Respect Level-of-Detail: scopes resolving to `none` are skipped entirely.

## Normalizer shape

Every resource flows: `normalize -> tag filter -> handler.parse_resource_data
-> writer.add_resource`. Normalizers MUST map cloud labels/tags into the
canonical `tags` field and preserve the qualifier fields the tag/hierarchy
templates expect. Persist only via the `ResourceWriter`, never directly.

## Definition of done (every indexer change)

A change is incomplete until ALL of these are updated together:

- [ ] Code + registry (regenerated, not hand-edited)
- [ ] Unit tests: registry loader, normalizer+handler round-trip, selective dispatch
- [ ] Docs: `docs/authoring/indexed-resources/<platform>.md` and the indexer-internals doc
- [ ] Generation-rule contract verified: the type resolves by CloudQuery table
      name and the normalized dict carries the fields the templates need
- [ ] `cd src && python -m unittest discover -s indexers -p 'test_*.py'` passes