Kubernetes — Patterns#
Sampling note: Kubernetes is an XL project (~9,000+ Go files across the main repo plus staging modules). Pattern detection was performed via targeted grep across the full repository (excluding vendor/) to obtain counts and representative examples. Deep-reads were limited to the most architecturally significant subsystems:
pkg/controller/,staging/src/k8s.io/client-go/,staging/src/k8s.io/apiserver/, andstaging/src/k8s.io/apimachinery/.
Concurrency patterns#
Worker Pool (Controller reconcile workers)#
- Usage: Every built-in controller in
pkg/controller/uses this pattern. The controller spawnsNgoroutines (typically 1–25, configurable) each of which runs an identicalrunWorkerloop that pulls items from a rate-limited work queue. - Example:
pkg/controller/deployment/deployment_controller.go:193for i := 0; i < workers; i++ { go wait.UntilWithContext(ctx, dc.worker, time.Second) }workerdequeues one key fromdc.queue, callssyncDeployment, then callsqueue.Done(key). On error the item is re-queued with exponential back-off. - Assessment: Highly idiomatic. The number of workers is explicit and externally configurable. The work queue (
client-go/util/workqueue) provides deduplication (if the same key is enqueued twice before it is processed, it only processes once), rate limiting, and retry back-off — eliminating the need for per-worker synchronization.
Channel-based stop/done signalling#
- Usage: Pervasive — 1,317
make(chan …)calls, 884select {}blocks (excl. vendor). The dominant pattern isstopCh chan struct{}(orctx.Done()in newer code) as a broadcast shutdown signal. - Example:
staging/src/k8s.io/apimachinery/pkg/util/proxy/upgradeaware.goTwo goroutines signal completion through separate done channels; awriterComplete := make(chan struct{}) readerComplete := make(chan struct{})selectwaits for both before returning. - Assessment: The older idiom (pre–Go 1.7 context) uses
chan struct{}for lifecycle control. Newer code increasingly passescontext.Contextfor the same purpose. Both coexist throughout the codebase, which reflects the project’s decade-long history.
Context cancellation & propagation#
- Usage: 16,784
context.Contextparameters; 1,013context.WithCancel/WithTimeout/WithDeadlinecalls (excl. vendor). Context is threaded through virtually every public function. The API server’s handler chain attaches a request-scoped context at entry and cancels it when the connection closes. - Example:
staging/src/k8s.io/apiserver/pkg/server/signal.go:SetupSignalHandlerreturns a context that is cancelled onSIGTERM/SIGINT; this context is then passed toserver.Run(ctx)and propagated through all subsystems. - Assessment: Exemplary. Kubernetes treats
context.Contextas the canonical lifecycle handle for every long-running operation. Components that predate the context package have been progressively retrofitted.
Graceful shutdown via signal context#
- Usage:
staging/src/k8s.io/apiserver/pkg/server/signal.goregistersSIGTERM/SIGINThandlers that cancel a root context. All components checkctx.Done()rather than global flags. - Assessment: Clean and idiomatic. The pattern ensures all goroutines share a single cancellable root and avoids global mutable state for shutdown signalling.
errgroup (limited use)#
- Usage: Only 2 files use
golang.org/x/sync/errgroup:staging/src/k8s.io/cli-runtime/pkg/resource/visitor.go(fan-out over resource visitors) andpkg/controlplane/controller/leaderelection/leaderelection_controller.go. - Assessment: Not the preferred pattern in this codebase. Kubernetes predates errgroup and uses its own
wait.Group,sync.WaitGroup, and context-propagatingwait.UntilWithContexthelpers instead.
Categories summary#
| Category | Present | Notes |
|---|---|---|
| Worker pools | Yes | Every controller — for i := 0; i < workers loop |
| Fan-out/fan-in | Yes | Scheduler runs Filter plugins in parallel across nodes |
| Pipeline processing | Yes | API server handler chain, scheduler extension point pipeline |
| Context cancellation | Yes | Universal — 16k+ uses |
| Graceful shutdown | Yes | Signal → context → propagation |
| Rate limiting | Yes | client-go work queue with token-bucket rate limiter |
Error handling#
Style: Mixed, but tilting strongly toward modern Go 1.13+ wrapping. Three distinct layers coexist:
- Simple sentinel errors:
errors.New(...)— 1,729 uses. Used for package-level error variables (e.g.,ErrNotFound,ErrAlreadyExists). - Modern wrapped errors:
fmt.Errorf("...: %w", err)— 2,402 uses. The dominant style in all code written after 2020. - Domain-typed errors: Custom types carrying structured data (HTTP status code, resource GVK, etc.).
- Simple sentinel errors:
Error types defined:
StatusError(staging/src/k8s.io/apimachinery/pkg/api/errors/errors.go:35) — wraps anmetav1.Statusstruct; the standard wire-format error for all REST API responses. Every HTTP error from the API server is aStatusError.AmbiguousResourceError(staging/src/k8s.io/apimachinery/pkg/api/meta/errors.go:28) — returned when a short resource name maps to multiple GVRs.VolumeError(staging/src/k8s.io/api/storage/v1/types.go:241) — structured error embedded in API object status for volume attach/detach failures.field.Errorandfield.ErrorList(staging/src/k8s.io/apimachinery/pkg/util/validation/field/) — structured validation errors with field paths, used throughout admission and validation.
Wrapping approach:
fmt.Errorfwith%wfor the majority of new code.errors.Is(296 uses) anderrors.As(112 uses) for structured error inspection.Notable pattern —
utilruntime.Must: Used to panic on errors that “must not happen” during init (e.g., scheme registration). This is deliberate: if a type is not registered correctly the process should crash immediately rather than silently misbehave.utilruntime.Must(core.AddToScheme(scheme)) // panics if registration failsExamples:
- Validation:
staging/src/k8s.io/apimachinery/pkg/util/validation/field/errors.go—field.Required,field.Invalid, etc. return typed*field.Errorwith JSON path. - API errors:
apierrors.NewNotFound(resource, name)constructs aStatusErrorwith HTTP 404 and a standard reason string.
- Validation:
Configuration pattern#
Approach: Two-stage options→config pattern. No functional options at the component level; no Viper; no environment variable config.
Stage 1 —
*Optionsstruct (flag-backed):// cmd/kube-apiserver/app/options/options.go type ServerRunOptions struct { GenericServerRunOptions *genericoptions.ServerRunOptions Etcd *genericoptions.EtcdOptions SecureServing *genericoptions.SecureServingOptions // ... }Each field is a nested options sub-struct populated by pflag. Options structs only hold raw (string/int) values.
Stage 2 —
*Config/CompletedConfig(resolved types):func (s *ServerRunOptions) Complete(ctx context.Context) (*completedOptions, error) { // resolves string IPs to net.IP, loads TLS certs, creates client-go clients, etc. }The
Complete()method validates flag values and produces aCompletedConfigholding live objects. TheCompletedConfigtype is unexported except via thecomplete()method, enforcing the two-phase flow.Functional options (limited scope): Used in
SharedInformerFactory(the code-generated informer factory) withWith*functions:type SharedInformerOption func(*sharedInformerFactory) *sharedInformerFactory func WithCustomResyncConfig(...) SharedInformerOption { ... } func WithNamespace(namespace string) SharedInformerOption { ... }This is the generated pattern for informer factories and is not representative of the wider config style.
Feature gates: Runtime feature flags used pervasively to gate alpha/beta features:
if utilfeature.DefaultFeatureGate.Enabled(features.DynamicResourceAllocation) { // ... }Gates are registered in
pkg/features/kube_features.goand set via--feature-gates=Foo=true.
Dependency injection#
Approach: Manual, explicit constructor injection. No framework (no google/wire, no uber/dig, no uber/fx).
Evidence: Every major component is initialized via a
New(config)orNew(informers, clients, …)function that receives all dependencies as parameters:func NewDeploymentController( ctx context.Context, dInformer appsinformers.DeploymentInformer, rsInformer appsinformers.ReplicaSetInformer, podInformer coreinformers.PodInformer, client clientset.Interface, ) (*DeploymentController, error) { ... }Dependencies flow from top-level wiring code (e.g.,
cmd/kube-controller-manager/app/controllermanager.go) down through constructor chains. No container or service locator is used.Config structs as DI vehicle: The
ControllerContextstruct acts as the dependency container passed to every controller’sInitFunc. It holds the shared informer factory, client set, event recorder, etc.:type ControllerContext struct { ClientBuilder clientbuilder.ControllerClientBuilder InformerFactory informers.SharedInformerFactory ObjectOrMetaInformerFactory controllermanager.InformerFactory // ... }
Other notable patterns#
Scheme + SchemeBuilder registration (init-time registry)#
The runtime.Scheme is the central type registry mapping Go types to API Group/Version/Kind tuples. Every API package registers its types at init() time:
// staging/src/k8s.io/api/apps/v1/register.go
var localSchemeBuilder = &SchemeBuilder{AddToScheme}
var AddToScheme = localSchemeBuilder.AddToScheme
func init() {
localSchemeBuilder.Register(addKnownTypes)
}install packages then call AddToScheme(scheme) to populate a global scheme. The utilruntime.Must wrapper ensures registration failures are fatal. This pattern appears 827 times (func init()). It is a global mutable registry — a deliberate trade-off for API extensibility.
Table-driven tests (extremely heavy use)#
- Prevalence: 7,431 occurrences of
testCases,testcases, or[]struct{in*_test.gofiles. This is arguably the most uniformly applied pattern in the entire codebase. - Style: Anonymous struct slices with named fields are standard:
tests := []struct { name string input *v1.Pod wantErr bool expected int }{ { name: "nil pod", input: nil, wantErr: true }, // ... } for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { ... }) } - Assessment: Table-driven tests are the de-facto Kubernetes standard. They make test cases self-documenting, keep tests concise, and make it easy to add regression cases without duplicating setup code.
Type switch for runtime polymorphism#
- Usage: 369
switch obj.(type)constructs (excl. vendor), concentrated inapimachinery. Used to dispatch onruntime.Objectconcrete types without reflection. - Example:
staging/src/k8s.io/apimachinery/pkg/api/meta/meta.go:40switch t := obj.(type) { case *metav1.ObjectMeta: return t, nil case metav1.Object: return t, nil // ... } - Assessment: Necessary given the project’s pre-generics API type hierarchy. The type switch is used as a controlled polymorphism mechanism rather than abused for flow control.
Generics (selective, recent)#
- Usage: Generics appear in newer apimachinery validation utilities (
staging/src/k8s.io/apimachinery/pkg/api/validate/), introduced to reduce repetition in update validation:func UpdateValueByCompare[T comparable](ctx context.Context, op operation.Operation, ...) field.ErrorList func EachSliceVal[T any](ctx context.Context, ...) field.ErrorList - Assessment: Generics use is conservative and deliberate — applied only where type parameters genuinely eliminate duplication (validation helpers) without obscuring intent. The bulk of the codebase predates Go 1.18 and uses interfaces + type switches instead.
Interface embedding for protocol composition#
- Usage: 2,498 interface definitions in the non-vendor codebase. Heavy use of interface embedding to compose capabilities:Informer and client interfaces are similarly composed from narrower capability interfaces.
// staging/src/k8s.io/apiserver/pkg/storage/interfaces.go type Interface interface { Versioner() APIObjectVersioner Create(ctx context.Context, key string, obj, out runtime.Object, ttl uint64) error Delete(ctx context.Context, key string, out runtime.Object, ...) error Watch(ctx context.Context, key string, opts ListOptions) (watch.Interface, error) Get(ctx context.Context, key string, opts GetOptions, objPtr runtime.Object) error List(ctx context.Context, key string, opts ListOptions, listObj runtime.Object) error } - Assessment: The sheer number of interfaces reflects a deliberate design: every major collaboration point is abstracted behind an interface, enabling fakes/mocks in tests and swappable implementations in production (e.g., etcd3 backend vs. in-memory cache store).
Level-triggered reconciliation (the meta-pattern)#
This is the most architecturally significant behavioral pattern in Kubernetes, transcending individual Go idioms:
- Description: Controllers do not maintain local state about what changed; they always read current state from the cache, compute the delta to desired state, and apply corrections. A missed watch event, a controller restart, or a network blip does not cause divergence — the periodic re-sync timer triggers a full reconcile pass.
- Implementation: Each controller’s
syncX(key string)function is always a full “read-compute-write” cycle:func (dc *DeploymentController) syncDeployment(ctx context.Context, key string) error { d, err := dc.dLister.Deployments(namespace).Get(name) // read from cache // compute desired replicasets ... // call API if action needed } - Assessment: This pattern is not a Go idiom per se, but it shapes every Go pattern choice in the controllers: why the work queue is deduplicating (idempotent re-queuing is safe), why reconcile functions take only a string key (stateless input), and why SharedInformers use a local cache (cheap re-reads during reconcile).
Sync primitives usage#
- Count: 2,013 uses of
sync.Mutex,sync.RWMutex,sync.Once,sync.WaitGroup,sync.Map, oratomic.*(excl. vendor). - Dominant use:
sync.RWMutexguards in-memory caches within informer stores and the scheduler’s node cache.sync.Onceis used for lazy initialization of singletons (e.g., defaulting functions, codec registration).sync.WaitGroupcoordinates informer goroutine teardown in the SharedInformerFactory. - Assessment: Lock usage is localized — data structures acquire and release locks on specific methods rather than holding coarse global locks. The informer’s
RWMutex-protected store is a canonical example of Go’s preferred fine-grained locking strategy.