containerd/containerd
Patterns and conventions
This page captures the recurring patterns a contributor needs to know to write code that "looks like" the rest of containerd.
Package and import structure
- Every Go file starts with the Apache 2.0 license header (matching
cmd/containerd/main.go). - Imports are grouped: stdlib, then third-party, then
github.com/containerd/..., then local module imports.goimportsenforces this. - Avoid
_and repeated parent-directory words in package names. The package is named after its directory (package serverincmd/containerd/server). - Don't add new files at the repository root — the v2 reorg moved everything into
core/,pkg/,internal/, etc.
Errors
- Wrap errors with context using
fmt.Errorf("...: %w", err). The codebase consistently passes the inner error through. - For sentinel errors that callers compare against, use
github.com/containerd/errdefs(e.g.errdefs.ErrNotFound,errdefs.ErrAlreadyExists). The errdefs package maps these to gRPC status codes at the boundary. - Don't return raw
errors.New(...)for cross-package error contracts; either define a typed error or wrap an errdefs sentinel.
Example (recurring throughout core/):
if err := checkArg(name); err != nil {
return fmt.Errorf("invalid name %q: %w", name, errdefs.ErrInvalidArgument)
}Contexts
- Every public method takes a
context.Contextfirst. The context carries the namespace, log fields, and the OpenTelemetry span. - Use
namespaces.NamespaceRequired(ctx)(inpkg/namespaces) at the top of services that require a namespace. - Don't store contexts in structs. Pass them through explicitly.
Logging
log.G(ctx).WithFields(...)is the standard form. TheGhelper attaches the trace ID from the context.- Use structured fields rather than
fmt.Sprintfinto the message:WithField("id", id)instead ofErrorf("error for %s", id). - Severity rules of thumb:
Infofor lifecycle (plugin loaded, task started),Warnfor recoverable misconfig,Errorfor unexpected failures.
Plugin registration
A built-in plugin's init() looks like:
func init() {
registry.Register(&plugin.Registration{
Type: plugins.SnapshotPlugin,
ID: "overlayfs",
InitFn: func(ic *plugin.InitContext) (any, error) {
return overlay.NewSnapshotter(ic.Properties[plugins.PropertyRootDir])
},
})
}- The
IDis the plugin's name in config ([plugins."io.containerd.snapshotter.v1.overlayfs"]). Requiresenumerates dependencies by plugin type; the plugin manager ensures they're initialized first.Configdeclares the plugin's TOML config struct. Useplugin.ConfigTypesocontainerd config defaultknows about it.- Skipping initialization (e.g. unsupported platform) should return
plugin.ErrSkipPlugin; required plugins error out, optional ones are silently dropped.
Concurrency
- Long-running goroutines must respect
ctx.Done()and propagate cancellation. - Locking strategy: prefer
sync.RWMutexfor read-heavy maps (the metadata cache, plugin registry). - The codebase uses
errgroup.Groupfor fan-out work but is restrained about it; new uses should wait on completion before returning.
Filesystem helpers
pkg/syswraps platform-dependent syscalls (MkdirAllWithACL,OpenLockedFile, …). Don't reimplement these.pkg/atomicfilefor atomic write-rename.pkg/ioutilfor stream helpers (AtomicWriteFile,LimitWriter).core/mountis the only place that manages mounts; never callunix.Mountdirectly from code outsidecore/mount.
Cross-platform code
_linux.go,_windows.go,_freebsd.go,_darwin.go,_other.gobuild-tag suffixes are how the codebase splits per-OS code. Keep platform-specific syscalls inside these files.- The Makefile drives
GOOS=...builds; CI exercises Linux + Windows + (best-effort) FreeBSD/macOS.
Public API stability
- Anything in
api/is part of the gRPC contract; breaking changes are gated bybuf-breaking.yml. - Anything in
client/is consumed by external embedders; deprecations follow the rules inRELEASES.md. internal/is fair game for refactor.
Built by Factory AutoWiki from public repository content. It is a generated preview for codebase exploration, not source-maintained documentation.