Open-Source Wikis

/

Terraform

/

How to contribute

/

Patterns and conventions

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; Append accepts another Diagnostics, a single Diagnostic, an error, or an hcl.Diagnostics.
  • diags.HasErrors() — true if any Error-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 from cty.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 in internal/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.Config returns a cty.Value only after evaluation against an hcl.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 around states.State. Acquire it via EvalContext.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 panic for user errors. Always tfdiags. Panics in core will propagate to crash.log.
  • Do not break the JSON output formats without a major-version plan. Anything emitted by -json is 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.

Patterns and conventions – Terraform wiki | Factory