Documentation
¶
Overview ¶
Package check answers "would this configuration move under live resource markers, and if not, what stops it" using only the configuration: no backend, no state, no cloud reads, and no provider process beyond reading schemas.
It is the analysis half of two GitHub issues that turned out to be one program with two front ends, which is what #114 asked for in as many words:
- #102 wants a corpus of real OpenTofu configurations measured, so that the work queue is ranked by which refusals actually block real configurations rather than by which are easiest to notice. Its front end is tools/corpus-gen, which runs Analyze over many directories and folds the results with Corpus.Add.
- #114 wants an evaluator to point the same question at their own repo and get a verdict. Its front end is "choudoufu live-check", in internal/command.
Keeping one analysis under both is not tidiness. If they diverge, the compatibility claim the project publishes and the verdict a user gets on their own configuration eventually disagree, and the user is right.
What it checks, and what it does not ¶
Two passes run here: lint.CheckWith and identity.ResolveWith. That is the whole of what can be decided from a configuration. Stamping, discovery and projection all need a cloud, and this package never contacts one, so their refusals are invisible to it. Every report says so (see Report.Unchecked and [Layers]) because a clean verdict that reads as "this will work" while three layers went unexamined would be the same defect #101 spent a campaign removing.
Where the refusal set comes from ¶
Nothing here hand-lists a refusal. The catalog is assembled from lint.Rules and identity.Refusals, the two test-enforced tables the packages already keep, and a finding is keyed by lint.Rule or by the identity diagnostic's Summary. Prose is never matched: roughly fifteen of those messages were rewritten during #101 and #110's second half will rewrite more, so an instrument that read them would measure its own staleness. See Catalog.
Index ¶
- Constants
- func HasConfigFiles(dir string) bool
- type ClassCount
- type Context
- type Corpus
- type CorpusEntry
- type CorpusEntryRef
- type CorpusRefusal
- type CorpusTotals
- type EntryProfile
- type EntryProviderSchema
- type EntryRefusal
- type Finding
- type LadderSummary
- type Layer
- type LoadResult
- type Manifest
- type ManifestFetch
- type ManifestSource
- type OnboardingClass
- type PartialLayer
- type PopulationTotals
- type Refusal
- type Report
- type Site
- type TypeCount
- type TypeDemand
- type UnresolvedModule
- type VarFileLayout
Constants ¶
const ( RefusalCyclicIdentities = "Cyclic parent-derived identities" RefusalEmptyImportIdentity = "Empty import identity" )
The projection refusals Analyze computes offline. See PartiallyCheckedLayers.
const ( RaisedByLint = "internal/live/lint" RaisedByIdentity = "internal/live/identity" RaisedByDataread = "internal/live/dataread" RaisedByStamp = "internal/live/stamp" RaisedByDiscovery = "internal/live/discovery" RaisedByProjection = "internal/live/projection" )
The live registries' RaisedBy values. They are import paths rather than layer names because the pass-through registry's values are import paths - internal/configs, internal/addrs, hcl - and a column mixing "identity" with "internal/configs" reads as two different kinds of fact.
const ReadsAsRanking = "ranking"
ReadsAsRanking is PopulationTotals.ReadsAs for the fixture and module-example populations: their blocked count orders work, it does not estimate compatibility. The only other value is "rate", reserved for an estate-shaped population - whole configurations describing one deployment - which #147's published-deployment population is. TestPopulationsClaimNoRate holds the line: a population claiming "rate" must be in rateCapableOrigins, with its provenance recorded in the manifest like every other origin.
Variables ¶
This section is empty.
Functions ¶
func HasConfigFiles ¶
HasConfigFiles reports whether a directory holds any OpenTofu configuration files directly.
This defers to configs.IsEmptyDir rather than restating the loader's accepted suffixes as a second hand-list: an earlier version matched only ".tf" and ".tf.json", so a ".tofu"-only directory was invisible to the corpus - not refused, not counted, simply missing from every denominator, with no test catching it because none compared this filter against the loader's own (issue #256 item 2). configs.IsEmptyDir walks through (*Parser).dirFiles, the same file-recognition path LoadConfigDir uses, so this now always matches what the loader would actually accept.
Types ¶
type ClassCount ¶
type ClassCount struct {
Class OnboardingClass `json:"class"`
Configs int `json:"configs"`
}
ClassCount is one rung's share of the profiled entries.
type Context ¶
type Context struct {
// Schemas are the provider's managed resource type schemas, keyed by
// type name.
//
// Running without them is supported and is materially less accurate,
// in one direction that matters to this instrument specifically: a
// type absent from the generated admission table is refused as
// unadmitted when the provider's own identity schema would have
// settled it. That single rule would then top any ranking built from
// the result, which is the one outcome #102 exists to prevent. So
// [Report.Schemas] records whether they were present and every front
// end says so.
Schemas map[string]providers.Schema
// ProviderManagedTypes is which managed resource types each provider's
// own schema declares, attributed to the provider - which [Schemas],
// merged and provider-less by construction, cannot say.
//
// It exists so that this instrument and a real live-plan draw the
// data-read phase's provider boundary in the same place. That boundary
// (see [dataread.LiveProviders]) refuses a data source whose provider
// serves no managed resource type at all - hashicorp/external, whose
// read runs a program named by its own arguments, is the shape it is
// there for - and it can only be evaluated by a caller that has the
// per-provider schemas. Without this, live-check reports such a site as
// "Resolves at plan time via a data-source read" for a plan that then
// refuses it, which is the polarity this package works hardest to avoid.
//
// Nil is supported and means today's answer: the boundary fails open,
// exactly as it does for any provider a run has no schema for. The two
// corpus instruments (tools/refusal-probe, tools/corpus-gen) leave it
// nil today, so their count for that refusal is a floor.
ProviderManagedTypes map[addrs.Provider]map[string]bool
// ManagedResults are values a caller obtained from the provider for
// resources whose computed attributes the configuration references -
// [projection.PlanInstances] produces them, keyed by resource address.
//
// This package stays provider-free on purpose. Analyze is the offline
// instrument that tools/refusal-probe and tools/corpus-gen fold into a
// ranking, and a pass that reached out to a provider on its own would
// make every one of those runs depend on a subprocess. So the caller
// that already has a provider in hand does the asking, and hands the
// answers in - the same seam [identity.Context.ManagedResults] has had
// since #187, under the same name.
//
// Empty is the ordinary case and means exactly today's behaviour: a
// for_each over a computed attribute refuses, as it always has.
ManagedResults map[string]cty.Value
}
Context is what a caller can tell the analysis about the world outside the configuration. It is lint.Context and identity.Context's shared field, under one name, because both passes must be told the same thing: a type the schemas admit has to read the same answer at both, or a lint refusal and a resolution refusal will disagree about the same type.
type Corpus ¶
type Corpus struct {
// Entries are the per-configuration results, in the order added.
Entries []CorpusEntry `json:"entries"`
// Refusals is every refusal in [Catalog], ranked, including the ones
// that fired nowhere.
//
// Carrying the zeroes is the point of building this from the catalog
// rather than from what was observed. A refusal that blocks nothing in
// a corpus of real configurations is evidence about where not to spend
// the next campaign, and it is invisible to any instrument that only
// records what it saw fire.
Refusals []CorpusRefusal `json:"refusals"`
// Totals are the corpus-wide counts.
Totals CorpusTotals `json:"totals"`
// Populations are the per-origin counts, where blocked and clean live.
Populations []PopulationTotals `json:"populations"`
// Ladder is #175's roll-up over the rate-capable entries: counts per
// onboarding class and the unadmitted-type demand table. Nil when the
// corpus has no rate-capable population.
Ladder *LadderSummary `json:"ladder,omitempty"`
// Checked, Partial and Unchecked are the layers behind every number
// above, repeated here so the artifact carries its own scope. See
// [PartiallyCheckedLayers] for why there are three lists rather than
// two.
//
// Partial is the schema-backed list - [PartiallyCheckedLayers], "2 of
// 27" for projection - which is what every [CorpusEntry] whose own
// Schemas is true actually ran. PartialWithoutSchemas
// ([PartiallyCheckedLayersFor](false), "1 of 27") is what an entry with
// Schemas false ran instead: GitHub issue #265, one list read as true of
// every entry overstated the ones this corpus measured with no provider
// at all. Totals.WithoutSchemas ("configs_without_schemas") is how many
// of Entries that was; this corpus can and does mix both in one run, so
// neither list alone describes it, and the artifact carries both rather
// than picking one to be silently wrong about the other's entries.
Checked []Layer `json:"checked_layers"`
Partial []PartialLayer `json:"partially_checked_layers"`
PartialWithoutSchemas []PartialLayer `json:"partially_checked_layers_without_schemas"`
Unchecked []Layer `json:"unchecked_layers"`
}
Corpus folds many [Report]s into the ranked table GitHub issue #102 asks for: which refusal fired, in how many configurations, at how many sites.
The ranking is by configurations blocked, not by raw site count. #102's own design note settles this and the reason is worth keeping next to the code: a raw site count is dominated by whichever configuration happened to be largest, so one 400-resource estate with an unadmitted type would outrank a refusal that stops every configuration in the corpus dead. Both numbers are emitted, because both are cheap and they answer different questions - "how many people does this stop" and "how much work is it to fix once you are stopped".
func (*Corpus) Add ¶
Add folds one configuration's report in under the given name and origin.
varFiles is optional and, when given, is recorded on the entry as the tfvars files it was measured with, in order (#183) - the caller's own record of what it passed to Dir, not anything this method reads itself.
func (*Corpus) Finish ¶
func (c *Corpus) Finish()
Finish fills in the refusals that fired nowhere, computes the totals and sorts the table. Call it once, after the last Corpus.Add.
func (*Corpus) LastEntry ¶
func (c *Corpus) LastEntry() *CorpusEntry
LastEntry returns a pointer to the most recently [Corpus.Add]ed entry, for a caller that needs to attach something computed after Add's own bookkeeping - tools/corpus-gen's per-provider schema status (#211), known only once Add has already turned the report into a row. The pointer is only valid until the next Add call, which may grow Entries' backing array and leave it pointing at a stale copy; a caller wanting more than one entry's worth must use it before adding the next.
type CorpusEntry ¶
type CorpusEntry struct {
// Name identifies the configuration, as the runner was told it.
Name string `json:"name"`
// Origin is the population this configuration belongs to (the
// manifest's own vocabulary: "in-repo fixture", "terraform-aws-modules",
// ...). Populations are totalled separately; see [PopulationTotals].
Origin string `json:"origin,omitempty"`
// Blocked is whether anything refused it.
Blocked bool `json:"blocked"`
// Loaded is whether the configuration could be read at all. A false
// here is itself a measurement (see [Load]), not a skipped row.
Loaded bool `json:"loaded"`
// LoadError is the first load diagnostic, when Loaded is false.
LoadError string `json:"load_error,omitempty"`
// Instances is how many managed resource instances resolved.
Instances int `json:"instances"`
// Sites is how many refused sites it had.
Sites int `json:"sites"`
// UnresolvedModules is how many module calls could not be read without
// installing them. Their contents are not measured in this row.
UnresolvedModules int `json:"unresolved_modules"`
// UnsetVariables is how many required root variables had no value.
UnsetVariables int `json:"unset_variables"`
// Schemas is whether provider schemas were available for this entry.
Schemas bool `json:"schemas"`
// Shadowed is how many identity refusals landed on a construct lint
// had already refused. See [Report.Shadowed].
Shadowed int `json:"shadowed,omitempty"`
// VarFiles are the repository-root-relative tfvars files this entry
// was measured with, in the order applied, when the manifest source
// named or derived any (#183). Empty means the entry was measured
// bare, with only whatever tfvars sit in its own directory - the
// ordinary case for every entry but the ones the corpus runner
// supplied files for. Present so a reader of this artifact can never
// mistake a vars-supplied measurement for what a plain checkout of
// that directory produces.
VarFiles []string `json:"var_files,omitempty"`
// Profile is the per-estate section (#175): refusal IDs with site
// counts, unset-variable attribution, unadmitted types and the
// onboarding class. Present exactly when the entry's origin is in
// rateCapableOrigins - the ranking-only populations measure fixtures
// and module examples, and a per-entry profile over them would measure
// the fixtures.
Profile *EntryProfile `json:"profile,omitempty"`
// ProviderSchemas is the schema-acquisition status for every provider
// this entry's own configuration declared or implied (#211's fix: a
// corpus run used to acquire hashicorp/aws schemas alone and hand every
// entry that same map regardless of what it actually required, so a
// google_* or tfe_* type read as unadmitted for a reason belonging to
// the run rather than to the corpus - the schema fallback
// (identity.SynthesizeTypeIdentity) never got a chance to run for it.
// One row per provider [configs.Config.ProviderRequirements] resolved
// for this entry (built-in providers excluded - they need no schema
// fetch), Present true when the fallback actually had that provider's
// schemas available. Empty when the run carried no schemas at all - see
// the artifact-level schema note for that case instead of a blank row
// repeated on every entry.
ProviderSchemas []EntryProviderSchema `json:"provider_schemas,omitempty"`
}
CorpusEntry is one configuration's line in the table.
type CorpusEntryRef ¶
type CorpusEntryRef struct {
// Name identifies the configuration in the artifact, relative to the
// repository root and slash-separated so it reads the same on every
// platform.
Name string
// Dir is where to read it from.
Dir string
// Origin is its source's origin string.
Origin string
// VarFiles is its source's [ManifestSource.VarFiles] plus whatever
// [ManifestSource.VarFileLayout] derived for this specific directory,
// repository-root relative and unresolved - the caller joins each
// entry against whatever root it is itself running under, the same
// way it already does for Name. Nil when the source named neither and
// its layout (if any) derived nothing that exists.
VarFiles []string
}
CorpusEntryRef is one directory the corpus runner will measure.
type CorpusRefusal ¶
type CorpusRefusal struct {
Layer Layer `json:"layer"`
ID string `json:"id"`
Title string `json:"title"`
// What describes the shape that trips it, where the source table has
// one.
What string `json:"what,omitempty"`
// DocsRef is the shipped document explaining it. Empty is a documented
// gap, not an oversight here; see [Refusal.DocsRef].
DocsRef string `json:"docs_ref,omitempty"`
// RaisedBy names the package that constructs the diagnostic, which is
// not always the layer it surfaces in. See [Refusal.RaisedBy].
RaisedBy string `json:"raised_by,omitempty"`
// Configs is how many corpus configurations this refusal blocked, and
// the number the table is ranked by.
Configs int `json:"configs"`
// Sites is how many places it fired across the whole corpus.
Sites int `json:"sites"`
// Types is the per-resource-type breakdown, for the type-shaped rules
// only. See [Finding.Types].
Types []TypeCount `json:"types,omitempty"`
// Examples are a few "file:line" sites, for a reader who wants to see
// one. Capped: this artifact is a work queue, not a log.
Examples []string `json:"examples,omitempty"`
// Registered is false for a refusal that fired but is in neither
// source table.
//
// These exist. identity's TestRefusalsRegistered scans that package's
// own source for Summary literals, so a diagnostic raised by the
// static evaluator and passed through - "Unknown variable", for one -
// reaches a user as a refusal while appearing in no registry. Counting
// them here is how the gap becomes a number instead of a surprise;
// closing it is GitHub issue #110's.
Registered bool `json:"registered"`
}
CorpusRefusal is one refusal's row in the ranked table.
type CorpusTotals ¶
type CorpusTotals struct {
Configs int `json:"configs"`
Loaded int `json:"loaded"`
Instances int `json:"instances"`
Sites int `json:"sites"`
RefusalsFired int `json:"refusals_fired"`
RefusalsInSet int `json:"refusals_in_set"`
WithoutSchemas int `json:"configs_without_schemas"`
// Unregistered is how many refusals fired that are in neither source
// table. See [CorpusRefusal.Registered].
Unregistered int `json:"refusals_unregistered"`
}
CorpusTotals are the corpus-wide counts. Deliberately absent: a blocked or clean count. Those live per population (PopulationTotals), because a corpus-wide blocked-over-configs figure reads as a compatibility rate, and no population this corpus has yet supports one - module examples lean on variables, conditionals and dynamic blocks far harder than an ordinary estate, and the in-repo fixtures measure this repo's own assumptions. Issue #118; the day an estate-shaped population exists, its row may claim otherwise (see ReadsAsRanking).
type EntryProfile ¶
type EntryProfile struct {
// Class is the entry's [OnboardingClass].
Class OnboardingClass `json:"onboarding_class"`
// Refusals is every refusal that fired on this entry, sorted by layer
// then ID.
Refusals []EntryRefusal `json:"refusals,omitempty"`
// UnadmittedTypes are the resource types this entry's own configuration
// declares that the checker refused as unadmitted, sorted.
//
// They are read from the lint-layer unadmitted-type finding's per-site
// types, which is the loaded configuration seen through the same
// admission judgment (generated table plus provider identity schemas)
// the verdict itself used - not a regex over files. That choice bounds
// what the list can cover in both directions. Covered: the root module
// and every child module the loader resolved locally, so module-sourced
// resources within the tree are counted. Not covered: resources inside
// module calls the loader could not read without installing them (the
// entry's unresolved_modules count bounds that), and .tf files on disk
// that no module block references - the loader never reads them, and
// neither does any live-path verdict.
UnadmittedTypes []string `json:"unadmitted_types,omitempty"`
}
EntryProfile is one rate-capable entry's per-estate section: what refused it, whether each refusal is an artifact of unset variables, which of its own declared types are unadmitted, and the rung all of that puts it on.
type EntryProviderSchema ¶
type EntryProviderSchema struct {
// Provider is the FQN this entry's own configuration resolved to, for
// display ("hashicorp/google" - the default registry's hostname is
// omitted the same way [addrs.Provider.ForDisplay] omits it).
Provider string `json:"provider"`
// Present is whether this entry's report actually received this
// provider's resource type schemas.
Present bool `json:"present"`
// Error is why not, when Present is false and a fetch was genuinely
// attempted for this provider and failed - the third state #211 asks
// for, distinct from "schemas were off for the whole run" (Error empty)
// and from "the fallback ran and the type still isn't admitted" (Present
// true, and the type shows up in unadmitted_types anyway).
Error string `json:"error,omitempty"`
}
EntryProviderSchema is one provider's schema-acquisition status for a single corpus entry (#211) - the honest, per-entry answer to "did the schema fallback get a chance to run for what THIS configuration actually declares", as opposed to a single global note that cannot tell a type the fallback tried and lost apart from a type the fallback was never given a provider to try against.
type EntryRefusal ¶
type EntryRefusal struct {
Layer Layer `json:"layer"`
ID string `json:"id"`
// Sites is how many places it fired in this entry.
Sites int `json:"sites"`
// UnsetVarOnly is true when every one of this refusal's sites references
// a required root variable that had no value - the whole refusal may be
// an artifact of running without the operator's tfvars rather than a
// fact about the configuration. Set only when the caller ran
// [Report.AttributeUnsetVariables] before folding the report in; false
// otherwise, which understates rather than invents.
UnsetVarOnly bool `json:"unset_var_only,omitempty"`
// Categories breaks this refusal's sites down by reference-subject
// category (see [configs.ReferenceCategory]) - populated only for
// [configs.staticScopeData.StaticValidateReferences]'s refusals, and omitted entirely
// when none of this refusal's sites carried one, so every other rule's
// row in the artifact stays exactly as small as it was before #178.
Categories map[configs.ReferenceCategory]int `json:"categories,omitempty"`
}
EntryRefusal is one refusal's row in an EntryProfile.
type Finding ¶
type Finding struct {
Refusal
// Sites are where it fired, ordered by file and line.
Sites []Site
// Registered is false when the refusal's identity was not in
// [Catalog]. It means this package and identity's registry have
// drifted, and the finding is reported under its raw summary rather
// than dropped.
Registered bool
// UnsetVarRefs are the required input variables with no value that this
// refusal's sites reference, sorted, and UnsetVarSites how many of its
// sites reference one. Both are set by
// [Report.AttributeUnsetVariables] and are zero until it runs.
//
// UnsetVarSites < len(Sites) is the interesting case and the reason
// this is a count rather than a flag: a refusal that fires in six
// places, two of which read an unset variable, is still a real refusal
// in four.
UnsetVarRefs []string
UnsetVarSites int
}
Finding is one refusal and every place it fired in this configuration.
func (Finding) Remedy ¶
Remedy is what to do about this refusal, which is the first site's detail.
The refusal registry's What is preferred where it has one, since it is written once per rule rather than once per site. Neither is authored here: #101 audited every one of these messages into saying what is actually true, and restating them in a report would be a second place for them to go stale.
func (Finding) Types ¶
Types summarizes a type-shaped finding as a count per resource type, sorted by count and then name.
It returns nothing for every other rule. This exists because the two type-shaped rules are the ones that produce hundreds of near-identical sites in a real configuration, and #114 asks for them summarized rather than enumerated - the resolution layer's unadmitted-type refusal once interpolated the whole admitted list into a single 25KB error, and a report that listed every site would be that mistake with more steps.
type LadderSummary ¶
type LadderSummary struct {
// Origins are the rate-capable origins the summary is computed over.
Origins []string `json:"origins"`
// Classes counts profiled entries per rung, in rung order, zeros
// included so the shape is stable across regenerations.
Classes []ClassCount `json:"classes"`
// UnadmittedDemand is the admission shortlist: each unadmitted type,
// with how many profiled entries declare it, sorted by that count and
// then by name. Demand is counted over every profiled entry - a
// language-blocked estate's types still count, because admitting them
// is still work that estate needs.
UnadmittedDemand []TypeDemand `json:"unadmitted_demand,omitempty"`
}
LadderSummary is the artifact's roll-up over every profiled entry: #175's ladder table and its usage-weighted admission shortlist, regenerated with the corpus instead of hand-computed from a scratch run.
type Layer ¶
type Layer string
Layer is one of the analysis passes this package runs, or one it deliberately does not.
const ( // LayerLint is [lint.CheckWith]: is this configuration inside the // stateless subset at all. LayerLint Layer = "lint" // LayerIdentity is [identity.ResolveWith]: can every managed resource // instance's identity be computed from the configuration alone. LayerIdentity Layer = "identity" // LayerDataread is [dataread.Analyze]: for every data source identity // resolution demands, can a live-plan read it before resolution. The // analysis is offline - eligibility classification only, no read - so // this instrument can run it without breaking its no-cloud-calls // contract; the read itself remains a plan-time act this instrument // did not perform, which is why an eligible finding is not "clean". LayerDataread Layer = "dataread" // LayerStamp is internal/live/stamp, which rewrites resource bodies to // carry ownership markers. // // Run here since GitHub issue #224: [stamp.Stamp] takes req.Config, // req.Schemas, req.Estate and req.NeedsDiscovery and touches no live // provider handle anywhere in its signature or body (req.Slots is the // one live-derived input, and it degrades safely to "write no tofu-slot // tag" when absent - see stamp.go's own doc comment). Its refusals // compare the configuration's own tag values, or the taggability of the // provider's schema, against what a run would compute; neither reads a // live object. The doc comment this replaced said the opposite - "what a // live object already carries" - which was false for all four of the // refusals a corpus run can actually trip, and is exactly the kind of // false load-bearing claim this repository has been burned by before. LayerStamp Layer = "stamp" // LayerDiscovery is internal/live/discovery, which lists live objects // and binds the ones carrying this estate's markers. Not run here: it // is the cloud read this package exists to avoid. LayerDiscovery Layer = "discovery" // LayerProjection is internal/live/projection, which materializes prior // state from what discovery bound. // // Partly run here, which is why it is in [PartiallyCheckedLayers] rather // than in either of the other two lists. Most of the stage is a live // read and cannot be seen offline, but two of its refusals are decided // before any provider is asked for anything - see // [projection.CyclicIdentityDiagnostics] and // [projection.EmptyImportIdentityDiagnostics], both of which take // resolutions (and, for the second, a schema) and no provider handle. // Listing the whole stage as unchecked understated what this instrument // can see; listing it as checked would overstate it by twenty-five // refusals. LayerProjection Layer = "projection" )
func CheckedLayers ¶
func CheckedLayers() []Layer
CheckedLayers are the passes Analyze runs in full. Everything a report says about these four is derived from running them and nothing else.
func LayersWithRegistries ¶
func LayersWithRegistries() []Layer
LayersWithRegistries is every Layer AllRefusals draws from.
It exists so that TestEveryLayerHasARegistry can compare it against CheckedLayers and UncheckedLayers. The first version of AllRefusals added stamp and discovery and stopped, describing them in a commit message as "the other two" when UncheckedLayers had always returned three - so projection's twenty-six refusals stayed in no table, AllRefusals was smaller than its own doc comment claimed, and nothing said so. A list an author can forget to extend is exactly the shape that needs the test.
func UncheckedLayers ¶
func UncheckedLayers() []Layer
UncheckedLayers are the live-path stages a configuration still has to survive that Analyze cannot see at all, because they need a cloud.
This list is the reason a clean report is a narrow claim rather than a promise. It is asserted against the packages that actually exist by TestLayersClassifyEveryLivePackage, so a new stage cannot appear in internal/live without someone deciding whether this instrument sees it.
type LoadResult ¶
type LoadResult struct {
// Config is the configuration tree, or nil when loading failed. A nil
// Config with diagnostics is an ordinary outcome for the corpus: a
// configuration written for a Terraform version this fork's parser
// does not accept is a measurement, not a crash.
Config *configs.Config
// Diags are the parse and build diagnostics. Errors here mean Config is
// nil.
Diags hcl.Diagnostics
// UnresolvedModules are the module calls that could not be read without
// installing them, sorted by module address. Their contents are absent
// from every count in the report that follows.
UnresolvedModules []UnresolvedModule
// contains filtered or unexported fields
}
LoadResult is what one directory produced.
func Load ¶
func Load(ctx context.Context, dir string, varFiles ...string) LoadResult
Load builds a configuration tree from one directory without installing anything and without contacting anything.
It exists because both front ends must work on a directory that has never heard of this fork: #114's whole point is an evaluator pointing the command at a repo they may not have written, and #102's corpus is other people's configurations. Requiring "tofu init" first would make the instrument measure only the configurations someone had already committed to enough to install.
Module sources are resolved in the two ways that need no network:
- A local source ("./modules/vpc") is read straight off disk.
- Any other source is read from .terraform/modules, if a previous init left a manifest there.
A module that neither resolves is recorded in LoadResult.UnresolvedModules and skipped, and the walk continues. That is #102's third option for registry modules, chosen because it is the honest one: a configuration whose modules cannot be read without a network is a real onboarding cost, and a measurement that discarded those configurations would report a subset that had already passed the hardest step. The cost of the choice is that a skipped module's contents are unmeasured, which every report states. varFiles, when given, name additional tfvars files applied on top of what the directory holds, highest precedence, in the order given - the same override order stock OpenTofu applies to repeated -var-file arguments. tools/corpus-gen is the only caller that ever passes one: it is the corpus's own opt-in measurement input (#183), read from live/corpus-manifest.json, not the directory or anyone's real -var-file. Every other caller passes none, and Load then behaves exactly as before.
func LoadOverlay ¶
func LoadOverlay(ctx context.Context, dir string, overlay map[string][]byte, varFiles ...string) LoadResult
LoadOverlay is Load with an in-memory overlay of the configuration files: a map of path to content, where each path is the one the loader would join itself (filepath.Join(dir, name)). A path already on disk is read as the overlay's content instead; a path that is not becomes a file the directory appears to hold. Nothing else about the directory changes, and nothing is written anywhere.
It exists for internal/live/onboard, which computes the source edit that turns a state-backed module into a live one so that the ONBOARDED form of a configuration can be measured. Every other way of doing that is worse:
- Editing in place would write into a corpus checkout shared by every concurrent worktree, and a dirty checkout silently contaminates every later measurement.
- Copying the module aside breaks it. A corpus entry is a directory inside somebody's repository and its module sources reach out of it - "../../" is how every terraform-aws-modules example calls the module it demonstrates - so a copy of the entry alone resolves none of them and would report a different configuration under the same name.
The overlay reaches the configuration parser only. Variable values and the .terraform module manifest are still read from disk, which is correct: the onboarding edit touches neither, and a module install is an input to the measurement rather than part of it.
func (LoadResult) Sources ¶
func (r LoadResult) Sources() map[string]*hcl.File
Sources are the configuration files this load parsed, by filename.
func (LoadResult) UnsetVariables ¶
func (r LoadResult) UnsetVariables() []string
UnsetVariables are the required root input variables no value was found for, sorted.
Each was evaluated as an unknown value, so an expression depending on one is not statically evaluable, and the refusals that fire on it may be an artifact of the missing value rather than of the configuration. Reports say so rather than silently ranking them.
It is a method rather than a field because it is only complete once the analysis has run: see the vars field.
type Manifest ¶
type Manifest struct {
Sources []ManifestSource `json:"sources"`
}
Manifest is the corpus definition: which configurations get measured, where each came from, and - for the ones that are not in this repository - the exact commit they were taken at.
It lives here rather than in either tool because two programs read it: tools/corpus-fetch materializes the sources it pins, and tools/corpus-gen measures whatever the globs then match. One definition, so a source cannot be fetched under one description and measured under another.
func ReadManifest ¶
ReadManifest reads and validates a corpus manifest.
func (Manifest) Resolve ¶
func (m Manifest) Resolve(root string) ([]CorpusEntryRef, error)
Resolve expands every glob into the directories that hold a configuration.
A matched path that is not a directory, or that holds no .tf files, is skipped rather than counted: an empty directory in the ranking would read as a configuration nothing refused, which is a lie about coverage.
type ManifestFetch ¶
type ManifestFetch struct {
// Dir is where the source is materialized, relative to the repository
// root. It is expected to be ignored by git: the corpus is pinned by
// commit, so checking the contents in as well would only add weight
// and third-party licenses.
Dir string `json:"dir"`
// Repo is the clone URL.
Repo string `json:"repo"`
// Tag is the human-recognizable version, e.g. "v6.6.1". Empty for a
// source that publishes no tags; the commit alone pins it then.
Tag string `json:"tag,omitempty"`
// Commit is the exact object fetched: what Tag must resolve to when
// one is recorded, or the checkout target itself when there is none.
Commit string `json:"commit"`
}
ManifestFetch pins one external source to an exact commit.
When the source tags releases, both the tag and the commit are recorded, and tools/corpus-fetch checks out the tag and then verifies it resolves to the commit. That is deliberate redundancy: the tag is what a human recognizes and the commit is what makes a run reproducible, and a tag that has been moved to a different commit is something the fetch should refuse rather than silently measure. A corpus that changed under the artifact would turn every number in it into a claim about an unknown input.
A source that publishes no tags at all - true of every repository in the published-deployment population (#147) - is pinned by commit alone, and the fetch checks the commit out directly. The reproducibility property lives entirely in the commit either way; the tag only ever added the human-readable name and the moved-tag tripwire.
type ManifestSource ¶
type ManifestSource struct {
// Glob is a filepath.Glob pattern, relative to the repository root
// unless absolute. Globs rather than a list of paths, because a
// hand-maintained path list is the kind of manual wiring this
// repository's charter is against: adding a configuration to the
// corpus should mean adding the configuration.
Glob string `json:"glob"`
// Origin says where these configurations came from, in words a reader
// can weigh: "in-repo fixture", "terraform-aws-modules", "internal
// estate". It is copied into the artifact and never interpreted.
//
// It is the field that decides what a ranking is worth. Configurations
// this project wrote measure this project's own idea of what works;
// only third-party ones measure the product promise. An artifact that
// did not carry this would let the two be confused later, which is how
// a fixture count becomes a compatibility claim.
Origin string `json:"origin"`
// Fetch, when set, is how a source outside this repository is
// obtained. Absent for in-repo sources.
Fetch *ManifestFetch `json:"fetch,omitempty"`
// VarFiles, when set, names tfvars files - each relative to the
// repository root, like Glob - applied on top of whatever the matched
// directory itself holds, the corpus runner's equivalent of repeated
// "-var-file" arguments (#183), later entries taking precedence over
// earlier ones exactly as stock OpenTofu applies repeated -var-file.
//
// Every file it names must be the estate's own, discovered from its
// repository layout - never a value this project composed (the
// parity ruling of #178/#183: an operator runs stock OpenTofu with
// -var-file pointing at files their repo contains, and that is the
// bar). For a layout expressible as a rule, prefer [VarFileLayout]
// over hand-listing files here: a per-directory VarFiles table does
// not survive the next corpus refresh, and a layout rule does.
//
// Scoped to the one directory the source names, not to every directory
// its Glob matches: a wildcard source's var files would silently reach
// every sibling directory it happens to expand to. To aim var files at
// one estate under a wildcarded source, add a second, narrower source
// for that one directory (Glob with no wildcard, same Origin, no
// Fetch) ahead of the wildcard source in this list - [Manifest.Resolve]
// dedupes by matched path in list order, so the narrow source's entry
// wins and the wildcard source fills in everything else unchanged.
VarFiles []string `json:"var_files,omitempty"`
// VarFileLayout, when set, derives each matched directory's var files
// from its own name, rather than naming them by hand. It implements
// one shape: a directory named "<name>" pairs with
// "<VariablesDir>/<Env>/<name>.tfvars" (the deployment's own file) and
// "<VariablesDir>/<Env>/common.tfvars" (the environment's shared
// file), each included only when the estate actually ships it -
// common first, then the deployment's own file, so the deployment's
// value wins over the shared one for anything both set, the same
// later-wins order stock -var-file applies. See [VarFileLayout] for
// which of the repository's own layouts this shape fits.
//
// Applies to every directory the source's Glob matches - unlike
// VarFiles, a layout rule is meant to reach a whole wildcarded source,
// because it reads each directory's own files rather than composing
// anything.
VarFileLayout *VarFileLayout `json:"var_file_layout,omitempty"`
}
ManifestSource is one glob and its provenance.
type OnboardingClass ¶
type OnboardingClass string
OnboardingClass is one rung of #175's ladder: what stands between a published deployment and a clean live-check verdict, derived from the refusal IDs that fired on it and from nothing else.
The classification rule, verbatim from #175:
- clean: the configuration loaded and nothing refused it.
- backend-only: every finding is state-backend - the documented one-line onboarding edit.
- admissions-only: findings are a subset of {state-backend, unadmitted-type, logical-resource}, and no higher rung applies. Ratifying the types it declares (or, for a logical type, declaring a record_store) is all that stands in the way.
- data-read-eligible (issue #179): findings are a subset of the admissions set plus the data-read pass's eligible-read finding, at least one of which fired, and no higher rung applies. Not clean - a read can still fail at plan time, and this instrument did not perform it - and no longer language-blocked: no edit to the configuration is needed, a live-plan reads the values itself.
- language-blocked: anything else fired - at least one static-evaluability or structural refusal, the frontier the operational brief names as binding.
OnboardingUnreadable is the one value outside the ladder, and the one not derived from refusal IDs: a configuration the loader itself refused has no findings to classify, and calling it clean would read "nothing could be read" as "nothing refused this" - the exact confusion Report.Readable exists to prevent.
A sixth rung, backend-plus-remote-state, existed before #179 stage 3: findings that were a subset of {state-backend, remote-state} and not backend-only, for an estate reading another stack's outputs through terraform_remote_state while lint's RuleRemoteState still banned the construct outright. Stage 3 gave terraform_remote_state the same eligibility and read pipeline every other data source has and retired RuleRemoteState, so lint can no longer produce a "remote-state" finding for this rule to key on - the rung's condition became unreachable, not merely rarer, and it folds away rather than staying as a rung nothing can land on. What a remote-state reference now produces instead is either nothing (an eligible read resolves silently, same as any other data source) or a data-read finding, both already covered by the data-read-eligible and language-blocked rungs below.
const ( OnboardingClean OnboardingClass = "clean" OnboardingBackendOnly OnboardingClass = "backend-only" OnboardingAdmissionsOnly OnboardingClass = "admissions-only" OnboardingDataReadEligible OnboardingClass = "data-read-eligible" OnboardingLanguageBlocked OnboardingClass = "language-blocked" OnboardingUnreadable OnboardingClass = "unreadable" )
func ClassifyOnboarding ¶
func ClassifyOnboarding(loaded bool, ids []string) OnboardingClass
ClassifyOnboarding computes an entry's rung from whether it loaded and the set of refusal IDs that fired on it. The IDs named in the rule are lint rules; an identity-layer refusal's ID is a prose Summary ("Unresolvable identity") and can never collide with a lint rule slug, so the bare ID set is enough. See OnboardingClass for the rule.
func OnboardingClasses ¶
func OnboardingClasses() []OnboardingClass
OnboardingClasses is every class in rung order, the order the summary table carries them in. Unreadable last: it is not a rung, it is the entry the instrument could not see.
type PartialLayer ¶
type PartialLayer struct {
// Layer is the stage.
Layer Layer `json:"layer"`
// Refusals are the refusal IDs [Analyze] computes offline, sorted. A
// finding under this layer is always one of these; anything else in the
// stage's registry is still unseen.
Refusals []string `json:"refusals"`
// Total is how many refusals the stage's registry carries, so a reader
// of a report can see the share rather than take "partly" on trust.
Total int `json:"total"`
}
PartialLayer is one live-path stage Analyze runs part of.
func PartiallyCheckedLayers ¶
func PartiallyCheckedLayers() []PartialLayer
PartiallyCheckedLayers are the stages Analyze runs some of, when a provider schema is available. It is PartiallyCheckedLayersFor(true); see that function for the schema-less case, which drops one of the two named refusals.
The two-bucket split this replaces made every headline number carry a caveat that was wrong in both directions at once: "projection is unchecked" was the sentence, and it was true of twenty-five of that stage's refusals and false of two that Analyze can and now does compute. A stage is rarely all-or-nothing, so the report says which part.
The IDs are spelled here rather than exported from the stage because the stage raises them as diagnostic summaries; TestPartialLayerRefusalsAreRegistered pins each one against that stage's own registry, so a rename there is a test failure here rather than a finding that silently reads unregistered.
func PartiallyCheckedLayersFor ¶
func PartiallyCheckedLayersFor(hasSchemas bool) []PartialLayer
PartiallyCheckedLayersFor is PartiallyCheckedLayers narrowed to what a single run actually computed. hasSchemas is whether that run had any provider schema at all (a non-empty [flatSchemas]).
GitHub issue #265: projection.EmptyImportIdentityDiagnostics (this package's RefusalEmptyImportIdentity) reads schemas.ResourceTypeConfig for every resource and skips any with a nil result, so a schema-less run produces nothing for it, for every configuration - a zero that is the absence of evidence, not a clean pass. PartiallyCheckedLayers's constant "2 of 27" was true of what the code CAN compute with a schema and false of what a schema-less run - the default for tools/refusal-probe without -schemas, for TestIdentityGolden, and for "choudoufu live-check" with no provider available - actually did. This is the narrow fix: the same layer, the same Total, one fewer refusal when there was no schema to decide it with. RefusalCyclicIdentities needs no schema (projection.CyclicIdentityDiagnostics takes resolutions alone), so it stays either way.
type PopulationTotals ¶
type PopulationTotals struct {
Origin string `json:"origin"`
Configs int `json:"configs"`
Loaded int `json:"loaded"`
Blocked int `json:"blocked"`
Clean int `json:"clean"`
Instances int `json:"instances"`
Sites int `json:"sites"`
// ReadsAs says what Blocked/Configs means for this population.
ReadsAs string `json:"reads_as"`
}
PopulationTotals are one origin's counts, kept apart from the corpus-wide totals so a ranking over module examples and a would-be rate over estates are never read off the same number.
type Refusal ¶
type Refusal struct {
// Layer is which pass produces it.
Layer Layer
// ID is the refusal's stable identity: a [lint.Rule] string, or the
// Summary an identity diagnostic carries. Reports group and rank on
// this, never on message text.
ID string
// Title is the one-line summary a user sees at the head of the
// refusal: [lint.Rule.Summary] for a lint rule, the Summary itself for
// an identity refusal, which is already written as one.
Title string
// What describes the configuration shape that trips it, where the
// source table has one. [identity.Refusal.What] fills this; lint keeps
// its equivalent per-issue rather than per-rule, so a lint refusal
// leaves it empty and the report shows the first site's detail
// instead.
What string
// DocsRef is the shipped document that explains it, in the form all
// three source tables use.
//
// It was once empty for most identity refusals, and the count of empties
// was how the repository measured its own documentation gap. Since #110
// every refusal has an entry in live/LIMITATIONS.md - generated from
// these same tables - so the field is always set and the check that
// matters is whether the entry it names exists. That is
// TestEveryRefusalDocsRefIsResolvable, which is a stronger contract than
// counting empties ever was: a reference to a heading nobody wrote used
// to pass.
DocsRef string
// RaisedBy is the import path of the package that constructs the
// diagnostic.
//
// For the two live registries it restates [Refusal.Layer] and carries
// nothing new. It exists for the third: a pass-through refusal surfaces
// during identity resolution but is written by internal/configs' static
// evaluator, internal/addrs' reference parser or HCL itself, and that
// distinction is the whole reason those refusals were invisible to
// every instrument until #110. A reader of the artifact should be able
// to see it without knowing the history.
RaisedBy string
}
Refusal is one thing the live path can refuse, in a shape that does not care which package produced it.
Both source tables already carry these fields under their own names, and this type exists only so that a report can rank a lint rule against an identity refusal in one list. Nothing here is authored: see Catalog.
func AllRefusals ¶
func AllRefusals() []Refusal
AllRefusals is every refusal the whole live path can produce, including the two stages this instrument cannot run (discovery and projection - see #224 for why stamp moved out of that set).
Catalog is deliberately narrower, and the two must not be conflated. A zero in the corpus artifact means "measured over 105 configurations and blocked none of them", which is a finding. A discovery or projection refusal has never been measured by anything, because measuring the bulk of either needs a cloud, and giving it a zero in the same column would turn "unknown" into "harmless" - the exact confusion the artifact's checked/unchecked layer lists exist to prevent. So the corpus ranks Catalog, and this is what documentation is generated from.
#110's first acceptance criterion is every hard refusal in the live path, which is this set rather than that one.
func Catalog ¶
func Catalog() []Refusal
Catalog is every refusal the checked layers can produce, sorted by layer then ID.
It is assembled from lint.Rules, identity.Refusals and passthrough.Refusals on every call, which is the property that matters: a refusal cannot be added to any of the three tables and be missing here, and one cannot be listed here that none of them has. #114's fourth acceptance criterion is exactly this, and it is why the corpus artifact can report the refusals that fired nowhere - the interesting end of that table, and one no instrument assembled from observed output can ever contain.
The third source is GitHub issue #110's work. Before it, the largest single blocker in the corpus was in no table at all, and neither were two more of the top seven: they are diagnostics identity resolution passes through from the static evaluator, so a document generated from the two live registries alone omitted the top of its own list. They are catalogued under LayerIdentity, because that is the pass a user hits them in, with Refusal.RaisedBy naming the package that actually wrote them.
func (Refusal) Documented ¶
Documented reports whether any shipped document explains this refusal.
func (Refusal) Passthrough ¶
Passthrough reports whether this refusal is one the live path shows a user without having written it. See internal/live/passthrough.
type Report ¶
type Report struct {
// Findings are the refusals that fired, ranked by how many sites each
// blocks. Empty means every checked pass accepted the configuration.
Findings []Finding
// Warnings are the non-fatal diagnostics, ranked the same way. They do
// not affect [Report.Blocked].
Warnings []Finding
// Instances is how many managed resource instances resolved.
Instances int
// Identities is what those instances resolved TO: every
// [identity.Resolution] the run produced, ordered by address.
//
// It exists because Instances is a count, and so is every other field
// on this struct. The whole of this package, the corpus ranking, the
// cohort ratchet and the refusal probe measure predicates over a
// verdict - "did we refuse?", "how many sites?" - and the product's
// actual output is none of those things. It is a string written into a
// cloud tag and handed to an import. A resolution that renders the
// wrong string is not a refusal, so it raises no finding, moves no
// count, and reads as a success everywhere: GitHub issue #251 turned a
// refusal into a marker naming a queue that does not exist, and
// Instances went UP.
//
// The resolution was already computed here and discarded after
// result.Len() was read. Carrying it out is what lets a test assert on
// the value; see TestIdentityGolden.
Identities []identity.Resolution
// Shadowed is how many identity refusals fell on a construct lint had
// already refused, and were therefore not counted as findings. It is
// reported rather than dropped silently so that the dedupe rule can be
// checked against a run instead of taken on trust.
Shadowed int
// Schemas records whether provider schemas were available. See
// [Context.Schemas] for why a report is worth less without them.
Schemas bool
// Load carries what loading the directory cost, when the caller used
// [Load]. Unresolved modules and unset variables both bound what the
// findings below can be trusted to cover.
Load LoadResult
// Checked, Partial and Unchecked are the live-path stages this analysis
// ran in full, ran part of, and did not run. All three are reported,
// always: a verdict that named only what passed would read as a promise
// about stages nobody looked at, and one that called a stage unchecked
// when it computes two of that stage's refusals understates itself in
// the other direction. See [PartiallyCheckedLayers].
Checked []Layer
Partial []PartialLayer
Unchecked []Layer
}
Report is one configuration's verdict.
func Analyze ¶
Analyze runs the configuration-only passes over one loaded configuration and returns what refused it.
This is the whole of the shared instrument. "choudoufu live-check" renders one of these for a human; tools/corpus-gen folds many into a ranking. Both see the same findings from the same passes, which is what keeps the project's published compatibility claim and a user's own verdict from drifting apart.
func Dir ¶
Dir loads one directory and analyzes it: the entry point both front ends call, and the reason they cannot drift.
A directory that will not load comes back as a report with no findings and a Report.Load carrying the diagnostics, which callers must check before reading "no findings" as "nothing refused it". Report.Readable is that check.
varFiles is passed straight through to Load; see its doc comment.
func (*Report) AttributeUnsetVariables ¶
AttributeUnsetVariables marks every site whose own source text references a required input variable that had no value, and returns how many it marked.
It runs after Analyze rather than inside it, and must: the static evaluator is lazy, so most variables are first read during identity resolution and the unset set is not complete until the analysis has finished. See LoadResult.UnsetVariables.
What it reads is the refusal's own range - the offending construct as the author wrote it - and not the whole file. A refusal on line 40 is not excused by an unset variable used on line 12.
func (Report) Blocked ¶
Blocked reports whether this configuration can move under live markers at all, as far as the checked passes can tell.
The rule is not this package's opinion: it is what LivePlanCommand already does with the same two results. Any error-severity lint issue is fatal there (via lint.HasErrors, GitHub issue #210 - a warning-severity one, such as lint.RuleStateBackend, is rendered and does not stop the run, and lands in Report.Warnings rather than here), and any identity error diagnostic is fatal there, because a partial identity map makes the plan propose creating objects that already exist.
func (Report) Readable ¶
Readable reports whether the configuration could be loaded at all.
It matters because an unreadable directory also has no findings, and the two must never render the same way: "nothing refused this" and "nothing could be read" are opposite answers to the question a user asked.
type Site ¶
type Site struct {
// Address is the offending construct in address form, where the rule
// has one. For a lint issue this is [lint.Issue.Construct]; for an
// identity or data-read refusal (GitHub issue #290) it is the resource
// instance address recovered from the diagnostic's
// [identity.InstanceFailure] tag, in [addrs.AbsResourceInstance.String]
// form, when the diagnostic carries one.
Address string
// Type is the managed resource type. Set for the type-shaped lint
// rules (see [lint.Issue.Type] and [Finding.Types]), and, since issue
// #290, for any identity or data-read site whose diagnostic carries an
// [identity.InstanceFailure] - which is every error identity.ResolveWith
// raises while working on one instance. Left empty rather than guessed
// when neither source applies.
Type string
// Module is the module path the site is in; empty for the root.
Module string
// Detail is the per-site explanation, which is also the remedy text
// #101's audit put into these messages.
Detail string
// Category classifies the reference-subject shape behind this site -
// [configs.ReferenceCategory], read off the raising diagnostic's Extra
// field rather than parsed from Detail. Set only for
// [configs.staticScopeData.StaticValidateReferences]'s refusals; every
// other rule leaves it empty rather than guessing. See #178.
Category configs.ReferenceCategory
// File, Line and Column locate it.
File string
Line int
Column int
// StartByte and EndByte bound the offending construct in File, kept so
// that its source text can be recovered after the fact. Line and Column
// locate it for a reader; these locate it for a program.
StartByte int
EndByte int
// UnsetVarRefs are the required input variables with no value that this
// site's own source text references, sorted. Non-empty means this
// refusal may be an artifact of the missing value rather than of the
// configuration - see [Report.AttributeUnsetVariables].
UnsetVarRefs []string
}
Site is one place a refusal fired.
type TypeDemand ¶
TypeDemand is one unadmitted type's demand row: how many profiled entries declare it. Configs, not sites - "how many real deployments need this", the number #175 ranks admission work by.
type UnresolvedModule ¶
type UnresolvedModule struct {
// Path is the module call's address, as "vpc" or "vpc.subnets".
Path string
// Source is the source address as written.
Source string
// Reason says which of the two resolutions failed, in a form a report
// can print without rewording.
Reason string
}
UnresolvedModule is one module call this loader could not read.
type VarFileLayout ¶
type VarFileLayout struct {
// VariablesDir is the repository-root-relative directory holding one
// subdirectory per environment, e.g.
// ".corpus/govuk-infrastructure/terraform/variables".
VariablesDir string `json:"variables_dir"`
// Env selects one of VariablesDir's environment subdirectories. Which
// environment to read is a convention choice, not something derivable
// from the layout alone - see live/corpus-manifest.json's own comment
// for which one this manifest picks and why.
Env string `json:"env"`
}
VarFileLayout names where an environment-keyed variables tree lives, so Manifest.Resolve can derive var files for every directory a source matches instead of a hand-listed table.
It fits alphagov/govuk-infrastructure's layout exactly: "terraform/deployments/<name>/" pairs with "terraform/variables/<env>/<name>.tfvars" plus a per-env "terraform/variables/<env>/common.tfvars" - see live/corpus-manifest.json's own comment for why the other three tfvars-shipping corpus repositories (govuk-aws, mastino, k8s.io, cloud-platform-infrastructure) do not use this field.