Open-Source Wikis

/

CoreDNS

/

How to contribute

/

Patterns and conventions

coredns/coredns

Patterns and conventions

This page collects the conventions every plugin and core package follows. New code that ignores these patterns will fail review.

Plugin layout

Every in-tree plugin lives at plugin/<name>/ and follows a consistent layout:

plugin/<name>/
├── README.md            # User-facing docs (also rendered to man/coredns-<name>.7)
├── setup.go             # plugin.Register("name", setup) and Caddyfile parsing
├── <name>.go            # Handler with ServeDNS
├── *_test.go            # Unit tests
└── (optional)
    ├── metrics.go       # Prometheus collectors
    ├── log_test.go      # log assertions
    ├── fuzz.go          # Fuzz harness

The package name matches the directory (e.g. package forward for plugin/forward/). Names are lowercase, no underscores in directory names except a small set already grandfathered in (grpc_server, k8s_external).

A new plugin must also be added to plugin.cfg so directives_generate.go includes it. The position in the file is its execution position in the chain.

Plugin registration

A plugin's init() calls plugin.Register (which is a re-export of caddy.RegisterPlugin):

func init() {
    plugin.Register("forward", setup)
}

setup(c *caddy.Controller) error parses Caddyfile arguments via c.Next(), c.Val(), c.RemainingArgs(), and c.NextBlock(). Errors must be wrapped with plugin.Error("name", err) so the diagnostic includes the plugin name.

The setup function appends to the active plugin chain:

dnsserver.GetConfig(c).AddPlugin(func(next plugin.Handler) plugin.Handler {
    f.Next = next
    return f
})

If a plugin can only be configured once per server block, it returns plugin.ErrOnce on the second invocation.

The Handler interface

type Handler interface {
    ServeDNS(ctx context.Context, w dns.ResponseWriter, r *dns.Msg) (int, error)
    Name() string
}

The contract from plugin/plugin.go:

  • ServeDNS returns (rcode, err). If err != nil, CoreDNS responds SERVFAIL.
  • The rcode tells CoreDNS whether the plugin already wrote to the client. SERVFAIL, REFUSED, FORMERR, NOTIMP mean no reply was written. Any other value means a reply has been written.
  • To delegate to the next plugin, call plugin.NextOrFailure(p.Name(), p.Next, ctx, w, r). It also starts an OpenTracing child span and wraps the writer in a pluginWriter so metrics attribute the response to the plugin that wrote it.
  • Plugins that mutate a response must Copy() it first. The dns.Msg from a downstream plugin may be backed by zone data and mutating it corrupts that data.

Logging

Use plugin/pkg/log with the plugin name attached:

import clog "github.com/coredns/coredns/plugin/pkg/log"

var log = clog.NewWithPlugin("forward")

log.Info("started")            // [INFO] plugin/forward: started
log.Errorf("upstream %s: %v", a, err)
log.Debug("only when debug plugin is loaded")

Levels are Info, Warning, Error, Debug (each has a *f variant). Debug output requires the debug plugin in the Corefile. From plugin.md: keep logging to a minimum and prefer returning errors so an upstream errors plugin can format them consistently.

Errors

  • Errors returned from setup must be wrapped with plugin.Error(name, err).
  • Errors returned from ServeDNS propagate to the errors plugin, which logs them.
  • Constants ErrOnce, ErrNoHealthy, etc. are defined per-plugin where it makes sense.

Metrics

  • The Prometheus namespace is plugin.Namespace (= "coredns").
  • The subsystem is the plugin name. So a counter for cache becomes coredns_cache_….
  • Histograms reuse plugin.TimeBuckets (16 exponential buckets from 0.25ms to 8s) or plugin.SlimTimeBuckets for low-cardinality cases.
  • Native histogram bucket factor is plugin.NativeHistogramBucketFactor (1.05).
  • Register collectors with promauto so they're added to the default registry. The prometheus plugin (plugin/metrics/) reads from that registry.
  • Each plugin's README.md should have a ## Metrics section listing its exposed metrics.

Readiness

If a plugin can be temporarily unhealthy, implement ready.Readiness:

func (p *Plugin) Ready() bool { /* ... */ }

The ready plugin returns 200 only when every registered readiness check passes.

Sockets

Don't call net.Listen directly. Use plugin/pkg/reuseport.Listen and ListenPacket so reload events can transfer sockets between processes.

Context values

Plugins may read these context values:

Key Type Source Notes
dnsserver.Key{} *Server Server.ServeDNS Used by metrics to identify the server
dnsserver.LoopKey{} int Server.ServeDNS Loop detection counter for the file plugin
dnsserver.ViewKey{} string Server.ServeDNS The view name when the matched config has one
dnsserver.HTTPRequestKey{} *http.Request ServerHTTPS.ServeHTTP Original DoH request

Fallthrough

When a plugin is responsible for only a subset of the names within its zone, it should accept a fallthrough directive that lists optional zones. The shared plugin/pkg/fallthrough package parses and matches these zones. The convention: the plugin handles names it owns and calls the next plugin for everything else.

Documentation style

Per plugin.md, plugin READMEs follow the Unix man-page style:

  • Plugin names in prose are italicised: *forward*.
  • User-supplied arguments are uppercase, in code spans when literal: **FROM**, **TO**.
  • Optional text in block quotes: [optional].
  • Variable-length arguments use ...: arg....
  • Examples must use example.org or example.net.

The README must contain ## Name, ## Description, ## Syntax, ## Examples. Metrics, Ready, and Bugs are added when applicable. The make.doc workflow regenerates man/coredns-<name>.7 from the README on every PR.

Coding style

.golangci.yml enables a focused linter set. The relevant rules:

  • gofmt is the source of truth for formatting.
  • revive enforces context-as-argument, error-naming, error-strings, errorf, early-return, superfluous-else, range, unused-parameter, use-any. Tests are exempt from gosec and perfsprint.
  • staticcheck, govet/nilness, unused, prealloc, wastedassign, intrange, modernize round out the bug-finding set.
  • nakedret and whitespace keep returns explicit and prevent stray blank lines.

Conventional small things:

  • Configuration parse functions are named parse (not parseFooConfig).
  • The plugin's main type is named after the plugin: Forward, Cache, Hosts.
  • Package comments live above package and explain what the plugin does.
  • Logged time durations should be in seconds (d.Seconds()).

Testing conventions

  • Unit tests sit next to the code (foo_test.go).
  • Integration tests live in test/. They use test.Server from test/server.go to drive a Corefile end-to-end.
  • Tests must use example.org / example.net domains and ephemeral ports (:0).
  • Race detection is enabled in CI for unit tests; some tests need -race to surface goroutine bugs (especially in cache, forward, kubernetes).
  • Fuzz harnesses live in */fuzz.go and run via the cifuzz.yml workflow.
  • Tests that require network access or external services are guarded behind environment variables.

Mutating a response

Quoting plugin.md:

If a plugin mutates a response it MUST make a copy of the entire response before doing so. A response is a pointer to a dns.Msg and as such you will be manipulating the original response, which could have been generated from a data store.

Mutating plugins (e.g. rewrite, cache's ResponseWriter) wrap the downstream dns.ResponseWriter and override WriteMsg to call res.Copy() first.

DCO sign-off

Every commit must carry a Signed-off-by trailer. Use git commit -s. Probot enforces this on PRs. See CONTRIBUTING.md.

Adding a new plugin to the main repo

A new plugin is roughly 1000 lines. Per CONTRIBUTING.md the recommended sequence is:

  1. Open a PR with just the README.md to settle the name and configuration surface.
  2. Follow up with setup.go and the ServeDNS handler.
  3. Add tests, metrics, and (if applicable) readiness.
  4. Add the plugin to plugin.cfg in the right position; run make gen.
  5. Update CODEOWNERS (or rely on owners_generate.go).

plugin.md's "Qualifying for Main Repo" section lists the criteria: useful to others, sufficiently distinct, supports IPv4 and IPv6, has tests, has a README.

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

Patterns and conventions – CoreDNS wiki | Factory