Open-Source Wikis

/

Consul

/

How to contribute

/

Patterns and conventions

hashicorp/consul

Patterns and conventions

Conventions used throughout the codebase. Match these when sending PRs.

Code layout

  • agent/ holds everything an agent needs at runtime. The central Agent struct in agent/agent.go owns sub-systems (HTTP, DNS, local state, proxy config, checks, ...).
  • agent/consul/ holds server-only logic: Raft, the state store, RPC endpoints, leader loops, peering, gateways.
  • internal/ holds the v2 resource framework (controllers, storage, generic resource machinery) and its consumers.
  • command/ holds CLI subcommands; each subcommand is its own package.
  • api/, sdk/, envoyextensions/, proto-public/, troubleshoot/ are public Go modules (each with its own go.mod).

When adding a new subsystem, prefer creating a sub-package under agent/ (or agent/consul/ for server-only) rather than growing agent/agent.go.

File naming

  • xxx.go — implementation
  • xxx_test.go — tests
  • xxx_ce.go / xxx_ce_test.go — Community Edition specific (ifdef pattern). Enterprise has matching _ent.go files in HashiCorp's private fork; they get merged into a unified codebase at build time.
  • xxx_endpoint.go — RPC or HTTP endpoint definitions
  • xxx_endpoint_test.go — endpoint tests

Imports

Group imports as follows (enforced by the contributing guide):

import (
    "context"
    "fmt"
    "net/http"

    "github.com/hashicorp/go-cleanhttp"
    "github.com/go-viper/mapstructure/v2"

    "github.com/hashicorp/consul/api"
    "github.com/hashicorp/consul/lib"
)

Run goimports -local github.com/hashicorp/consul/ (or make fmt) to enforce.

Error handling

  • Errors are wrapped with fmt.Errorf("...: %w", err) so callers can errors.Is / errors.As them.
  • Sentinel errors live next to the package they belong to (e.g., agent/structs/errors.go).
  • ACL errors use acl.ErrPermissionDenied and friends; the RPC layer maps them to user-visible messages.
  • Returned status codes from HTTP endpoints follow Go's net/http conventions, mapped in agent/http.go (HTTPHandlers.wrap).

Concurrency

  • Background goroutines own a context and exit when it's cancelled. Use lib/routine or agent/routine-leak-checker/ patterns.
  • Long-running loops use jittered tickers (lib/retry.Jitter) and respect the agent's shutdown channel.
  • Locks: prefer narrow critical sections; many state-store operations use the immutable MemDB pattern (read snapshot, write transaction).

Blocking queries and watches

When adding a new RPC that returns mutable data, support blocking queries:

err := s.blockingQuery(
    &args.QueryOptions,
    &reply.QueryMeta,
    func(ws memdb.WatchSet, state *state.Store) error {
        idx, value, err := state.MyThing(ws, args.Key)
        if err != nil {
            return err
        }
        reply.Index = idx
        reply.Value = value
        return nil
    },
)

The helper lives in agent/blockingquery/. The pattern is everywhere — search for s.blockingQuery( for examples.

RPC and gRPC

  • Internal RPC uses net/rpc with msgpack codec on port 8300 and is registered in agent/consul/server.go (s.rpcServer.RegisterName).
  • gRPC services come in two flavors: internal (agent/grpc-internal/) used by Consul-to-Consul traffic, and external (agent/grpc-external/) used by clients (dataplane, public APIs).
  • Public proto schemas live in proto-public/; internal schemas in proto/.

When adding a new RPC, follow the existing XEndpoint struct pattern in agent/consul/x_endpoint.go. Each endpoint method is a request/reply pair on the registered struct.

Config entries

When adding a config-entry kind:

  1. Add the struct in agent/structs/config_entry_<kind>.go and register it in agent/structs/config_entry.go (the MakeConfigEntry switch).
  2. Add the JSON ↔ HCL decode rules.
  3. Add validation, ACL evaluation, and DeepCopy.
  4. Update agent/consul/config_endpoint.go if the kind needs special handling.
  5. Update agent/consul/state/config_entry.go to register the schema.
  6. Apply changes to api/config_entry.go for the public client.
  7. Wire it into the discovery chain (agent/consul/discoverychain/) if applicable.

The full checklist is docs/contributing/checklist-adding-config-entry.md.

Adding agent config fields

See docs/config/checklist-adding-config-fields.md. The agent config is split between user-facing structs (agent/config/source_*.go), the in-memory RuntimeConfig (agent/config/runtime.go), and the builder that maps one to the other (agent/config/builder.go).

CE / Enterprise split

The _ce.go suffix marks files that contain only the Community Edition implementation. Enterprise has counterpart _ent.go files (not in this repo). Common patterns:

  • acl_ce.go: stub functions that return "not implemented in CE" or no-op for partition/namespace logic.
  • *_ce_test.go: tests that only exist in CE.

When changing a _ce.go file, check whether the analogous Enterprise file exists; maintainers will point this out in review.

Logging style

logger := agentLogger.Named("rpc")
logger.Info("server forwarded RPC", "method", method, "to", server)
logger.Debug("query metadata", "index", idx, "duration", elapsed)

Always use structured key/value pairs after the message. Pre-format only when computing the value is cheap.

Tests

  • Use require for fatal assertions and assert for non-fatal. Both from github.com/stretchr/testify.
  • t.Run sub-tests for table-driven cases.
  • Use t.Parallel() only after verifying the test doesn't share global state.
  • Build helpers in testing.go files (not _test.go) when they need to be shared with other packages.

Built by Factory AutoWiki from public repository content. It is a generated preview for codebase exploration, not source-maintained documentation.

Patterns and conventions – Consul wiki | Factory