Skip to content
GantryCD

Resource Explorer & Resource Views

Resources captured from each stack’s latest state-bearing run are browsable in two cohesive surfaces that share the same components:

  • Explore (org-wide) — every readable stack’s resources.
  • Stack → Resources tab — the same view scoped to one stack.

Explore also has a Topology tab that maps how logical groups of stacks connect via their dependencies — see Stack Group Topology.

Both filter server-side against the resource_index (gantrycd_explore) with keyset pagination and the shared query bar; the stack tab is just Explore pinned to stack:<id>. See datastores.md for the store itself.

Both render a master–detail browser: a list on the left, and one detail card per opened resource on the right. Opening is two-tier, so browsing doesn’t pile up cards you never asked to keep: a click previews — the card takes the single unpinned slot, replacing the previous preview — and a double-click pins it (as does the card’s “Pin” button). Pinned cards accumulate side by side, survive the next preview, and are shielded from “Close all”. Unpinning hands the card the preview slot back, so there is never more than one unpinned card. Explore keys open cards by stack_id:address because the same address can exist in two stacks.

The detail body is shared (ResourceBody): the resource as an HCL block — resource "type" "name" { … } (or data) — with a # <full.module.address[index]> comment for nested/indexed resources, and a small metadata block (provider, alias, module, index, tainted, depends_on) below.

Filters

A shared query bar drives both surfaces: a small Lucene-subset query — type:aws_* AND mode:managed, attr.region:us-east-1, provider:aws.us_east_1, has:attr.tags, or bare free text — compiled to parameterized SQL server-side. Boolean composition (AND/OR/NOT/parens), prefix (val*), membership (field:(a OR b)), nested jsonb containment (attr.k.v:x), and attribute-value wildcards (attr.region:eu-west-*, an unindexed scan) are all supported. The facet dropdowns insert field:value tokens into the query rather than holding separate filter state, so the query string (in the URL) is the single source of truth.

A per-query cost budget keeps one search from scanning the store unboundedly: indexed fields (type, stack) are free and discount the query; non-indexed ones (provider_config, mode, attributes, name substrings, free text) cost budget, so we bound them with the budget instead of indexing every high-cardinality column. A 5 s statement_timeout is the runtime backstop. Substring / free-text search is unindexed for now — add a pg_trgm GIN to scale it. The full query language, the cost guard, and the measured behaviour at one and ten million rows are in Resource Explorer: Filtering & Scale.

The facets endpoint returns the distinct type / provider-reference / mode / stack values (with counts) that populate the query bar’s insert dropdowns, scoped to the caller’s readable stacks.

Provider aliases & for_each provider instances

The provider reference is the single provider filter, shown in short HCL form: aws, aws.us_east_1, aws.by_region["us-east-1"] (provider name + any alias + any for_each provider-instance key).

This is not available from tofu show -json — its provider_name is the bare fully-qualified name, with the alias and instance key stripped by the JSON format. They survive only in the raw state, where each instance records e.g. provider["registry.opentofu.org/hashicorp/aws"].by_region["us-east-1"]. So the runner, alongside tofu show -json, also captures tofu state pull and joins the parsed reference onto the inventory by address (tofuinspect.ApplyProviderConfigs). It is best-effort and always populated:

  • works on both plan and apply runs (state pull reads the current state in either);
  • a resource the raw state doesn’t cover — or a failed/absent pull (e.g. an older runner) — falls back to the short provider name derived from the fully-qualified provider, so the field is never blank for a known provider;
  • for_each provider instance keys are recovered per instance (aws.by_region["us-east-1"]), not just the alias.

The value flows as provider_config through StateResourceanalysis.json’s state_resourcesIndexedResource → the resource_index.provider_config column, exposed as the provider_configs facet and the ?provider_config= filter. The detail’s alias line shows it only when it carries an alias/instance (a bare default ref would just duplicate the fully-qualified provider line).

Which run the index follows

Not “the latest successful run”: the latest run that touched state — a success, or an apply that exited non-zero. A tofu apply can fail part-way and still have created or destroyed real infrastructure, so the runner reads the post-apply state file whatever the exit code and uploads it in analysis.json, and both the per-run reindex and the 12-hourly rebuild pick that run up. Before this, a stack whose newest run was a failed partial apply kept showing the pre-apply snapshot — resources the apply created were missing, resources it destroyed were still listed, duplicate detection missed a duplicate the apply created, and the rebuild never healed any of it because it re-picked the same stale run.

Failed plan-style runs (plan, destroy, refresh) stay excluded: they change nothing and upload no inventory, so promoting one would pin the tab to a run with no state to read.

The rows a failed apply uploads carry no “partial” marker. They are the best available evidence and strictly better than the pre-apply snapshot they replace, and a flag with no user-visible consumer is not worth a column. What is uploaded is the state OpenTofu persisted, which is not always the state that exists: if the persist itself failed, OpenTofu leaves an errored.tfstate locally while tofu show -json re-reads the backend (it never consults that file) and returns the last persisted view. Still no worse than the pre-apply snapshot, so it is uploaded rather than suppressed.

Two cases are deliberately out of scope:

  • Cancelled applies. A cancelled apply can also have changed state, but its pod is killed, so the deferred upload never runs and there is no artifact to follow.
  • No artifact at all. A run can fail before OpenTofu ever runs — pod evicted, clone or init failed, a lifecycle hook rejected it, the stale-cutoff sweep failed it with no runner reporting — and then there is no inventory to index. A missing section always means “this run tells us nothing”, never “this stack is empty”, and both readers of an inventory apply that rule:
    • the reindex leaves the index untouched, so Explore and the stack’s Resources tab (which read the index) keep the last snapshot that existed;
    • GetStackStateResources — which backs the stack’s Outputs tab and the dependency graph’s resource-detail modal, not the Resources tab — walks back to the newest run that does carry an inventory (up to five runs, stackStateRunCandidates) and reports that run as the source. Without the walk, the Outputs tab would read “This stack declares no outputs” under the failed run’s id, and every graph node’s detail would say the resource “isn’t in its stack’s most recently recorded state”.

The cross-stack dependency graph deliberately does not follow failed applies. It is derived from the checked-out HCL, which a failed apply did not change, and its run-completion reconcile only fires for successful runs — an authority that admitted more would make the 12h rebuild write edges nothing else ever writes.

Operational notes

  • Everything here is rebuildable from runs; the explore store may be lossy and is hosted off the critical primary (see datastores.md).
  • provider_config is captured at the runner, so existing indexed rows stay alias-less until each stack runs again on a runner image that includes the capture.