hashicorp/terraform
Patterns and conventions
The Terraform codebase has a small number of pervasive idioms. Knowing these makes most of internal/ readable.
tfdiags.Diagnostics everywhere
Most public functions return tfdiags.Diagnostics, not error. Diagnostics carry severity (Error or Warning), a summary, an optional detail, an optional source range, and an optional set of "extra" structured payloads.
func (l *Loader) LoadConfig(...) (*configs.Config, tfdiags.Diagnostics) {
var diags tfdiags.Diagnostics
if something {
diags = diags.Append(tfdiags.Sourceless(
tfdiags.Error,
"Something went wrong",
"Detailed explanation here.",
))
}
return cfg, diags
}Patterns:
diags = diags.Append(other)— diagnostics are values;Appendaccepts anotherDiagnostics, a singleDiagnostic, anerror, or anhcl.Diagnostics.diags.HasErrors()— true if anyError-severity diagnostics are present. Most code paths bail early when this is true after finishing the current step.tfdiags.Sourceless(severity, summary, detail)— for messages that aren't tied to a source range.tfdiags.AttributeValue(...)— for messages that point to a specific attribute via traversal.
The package lives at internal/tfdiags.
cty.Value is the value currency
Anything that comes from user configuration or that gets sent to providers is a cty.Value. Key operations:
cty.NilVal— the zero value, distinct fromcty.NullVal(...).v.IsNull(),v.IsKnown()— null vs unknown vs both vs neither.v.GoString()— debug printing.cty.ObjectVal(map[string]cty.Value)— common in tests.- Marks:
v.Mark(marks.Sensitive),v.HasMark(marks.Ephemeral),cty.UnmarkDeep(v). Marks live ininternal/lang/marks/.
Documentation for cty is at https://github.com/zclconf/go-cty. Terraform's own helpers (sensitive paths, structural conversions) are in internal/configs/configschema/ and internal/lang/.
Addresses are values
Anything that names a configuration or runtime object uses a typed address from internal/addrs. Never sling around string for resource paths. Common types:
addrs.Resource,addrs.AbsResource,addrs.AbsResourceInstance.addrs.ModuleInstance,addrs.Module.addrs.Provider(fully qualified, e.g.registry.terraform.io/hashicorp/aws),addrs.LocalProviderConfig(the user-visible name).addrs.Reference(a reference target).addrs.OutputValue,addrs.LocalValue,addrs.InputVariable.
Conversion helpers like addrs.ParseAbsResourceStr exist for the JSON/state boundary.
Configurable blocks and HCL2 leftover bodies
Configuration parsing is two-pass. The first pass produces configs.Module with typed fields; a few attributes (the body of provider "..." {} blocks, dynamic blocks) remain as hcl.Body until evaluation has the data they refer to. Patterns:
decoded.Configreturns acty.Valueonly after evaluation against anhcl.EvalContext.lang.Scope.EvalBlock(body, schema)evaluates an entire block.lang.Scope.EvalExpr(expr, type)evaluates one expression with an expected type.
Hooks for progress
The graph engine emits hooks at major milestones (PreApply, PostApply, PreDiff, etc.). The interface is terraform.Hook (internal/terraform/hook.go). Hooks can return HookActionHalt to stop the walk.
The CLI registers hooks (see internal/command/) that drive the view layer; this is how progress lines appear during apply.
Views for output
CLI commands never write directly to os.Stdout. They write to a view. Each command has its own view interface in internal/command/views/, with two implementations:
Human— colorized text for terminals.JSON— machine-readable, selected by-json.
When adding a new user-visible output, add a method to the view interface and implement it in both the human and JSON variants. Tests (*_test.go in internal/command/views/) typically use a fake view that records calls.
Concurrent state and plan access
The graph walk is concurrent. Shared state and changes use sync wrappers:
states.SyncState(internal/states/sync.go) — mutex-protected wrapper aroundstates.State. Acquire it viaEvalContext.State().plans.ChangesSync(internal/plans/changes_sync.go) — same pattern for the in-progress plan.
Don't reach past these wrappers; the underlying *states.State and *plans.Changes are not safe for concurrent use.
Provider/Provisioner factories
Anything that loads a plugin returns a Factory:
type Factory func() (providers.Interface, error)The factory is invoked once per execution per provider. The MockProvider pattern in tests sets factory := func() (providers.Interface, error) { return p, nil }.
Errors via tfdiags.Diag from errors
When wrapping a Go error for the user, prefer diags = diags.Append(err). If you need to transform an error before surfacing it, wrap it in a tfdiags.Diagnostic explicitly. Avoid fmt.Errorf chains as the only error type — they don't carry source range info.
Interfaces over concrete types
internal/providers/, internal/provisioners/, internal/states/statemgr/, and internal/backend/ all expose interfaces consumed by the rest of the codebase. Mocks for tests usually live in the same package as the interface.
configs.Config is read-mostly
After configload.Loader.LoadConfig, the resulting configs.Config tree is treated as immutable. Mutations during graph building happen on the graph and on EvalContext-scoped state, not on the configuration model.
Naming conventions
- File names: lowercase with underscores (
graph_builder_plan.go,node_resource_apply.go). - Test files mirror the source file:
graph_builder_plan_test.go. - "Stringer" generated files end in
_string.go(actiontriggerevent_string.go). Don't edit them by hand. - Mock files end in
_mock.go(hook_mock.go,provisioner_mock.go).
Documentation comments
Public types and functions have doc comments. Comments on engine internals tend toward longer, narrative text — see the package-level comments in internal/terraform/eval_context.go or internal/states/state.go for the shape.
What not to do
- Do not introduce a new top-level dependency without discussing it. Licensing has a strict policy (MIT/MPL2/BSD); see the contribution guide.
- Do not use
panicfor user errors. Alwaystfdiags. Panics in core will propagate tocrash.log. - Do not break the JSON output formats without a major-version plan. Anything emitted by
-jsonis part of the API surface.
Built by Factory AutoWiki from public repository content. It is a generated preview for codebase exploration, not source-maintained documentation.