Kubernetes — Interfaces#
Scale note: Kubernetes defines 2,448 non-vendor, non-test interfaces. This analysis samples the 5 most architecturally significant clusters. Trivial single-method markers and internal implementation-detail interfaces are skipped.
Interface catalog#
runtime.Object#
- Package:
k8s.io/apimachinery/pkg/runtime - File:
staging/src/k8s.io/apimachinery/pkg/runtime/interfaces.go:337 - Methods:
GetObjectKind() schema.ObjectKind DeepCopyObject() Object - Purpose: The root contract that every API resource type must satisfy.
GetObjectKindreturns the Group/Version/Kind metadata;DeepCopyObjectenables safe copy-on-write semantics in the informer cache (controllers always see a deep copy, never the shared cache object). - Implementations: All ~100 built-in API types (Pod, Deployment, Service, …) generated by
k8s.io/code-generator. CRD-backed types implement it via*unstructured.Unstructured. The generatedzz_generated.deepcopy.gofiles supply theDeepCopyObjectbody. - Design quality: Minimal by design — only 2 methods. Deliberately ISP-conformant: every other capability (defaulting, versioning, conversion) is a separate interface (
ObjectDefaulter,ObjectVersioner,ObjectConvertor). TheSchemeuses these narrow interfaces as combinable building blocks. The 2-method size is the “load-bearing” decision that makes the entire type system composable.
storage.Interface#
- Package:
k8s.io/apiserver/pkg/storage - File:
staging/src/k8s.io/apiserver/pkg/storage/interfaces.go:169 - Methods:
Versioner() Versioner Create(ctx, key string, obj, out runtime.Object, ttl uint64) error Delete(ctx, key string, out runtime.Object, preconditions *Preconditions, validateDeletion ValidateObjectFunc, cachedExistingObject runtime.Object, opts DeleteOptions) error Watch(ctx, key string, opts ListOptions) (watch.Interface, error) Get(ctx, key string, opts GetOptions, objPtr runtime.Object) error GetList(ctx, key string, opts ListOptions, listObj runtime.Object) error GuaranteedUpdate(ctx, key string, destination runtime.Object, ignoreNotFound bool, preconditions *Preconditions, tryUpdate UpdateFunc, cachedExistingObject runtime.Object) error Stats(ctx) (Stats, error) ReadinessCheck() error RequestWatchProgress(ctx) error GetCurrentResourceVersion(ctx) (uint64, error) EnableResourceSizeEstimation(KeysFunc) error CompactRevision() int64 - Purpose: The sole persistence abstraction for the API server. All REST storage handlers (one per API resource) call through this interface. The etcd3 backend is the only production implementation; a watch cache wrapper (
watchcache) sits in front of it. - Implementations:
etcd3.store(production), watch cache (cacher.Cacherwrapping etcd3 store), and in-memory fake for tests. - Design quality: Broad — 13 methods. The interface grew organically as etcd-specific concerns (watch progress, compaction, resource size estimation) leaked in; a TODO comment in the source flags
RequestWatchProgressas a temporary addition. The breadth violates ISP but reflects the pragmatic reality that all callers need the full contract. TheGuaranteedUpdatepattern (retry loop via callback) is elegant: it abstracts optimistic-concurrency retries without exposing CAS primitives to callers.
admission.Interface / MutationInterface / ValidationInterface#
- Package:
k8s.io/apiserver/pkg/admission - File:
staging/src/k8s.io/apiserver/pkg/admission/interfaces.go:123 - Methods:
// Interface (base marker) Handles(operation Operation) bool // MutationInterface embeds Interface Admit(ctx context.Context, a Attributes, o ObjectInterfaces) error // ValidationInterface embeds Interface Validate(ctx context.Context, a Attributes, o ObjectInterfaces) error - Purpose: The admission plugin contract. The API server assembles a chain of plugins; each plugin declares which operations it handles via
Handles(), then either mutates the object (Admit) or validates it (Validate). Separating mutation and validation into distinct interfaces (MutationInterfacevsValidationInterface) enforces the Kubernetes admission policy: mutating plugins run first (in a single pass), validating plugins run second (also in a single pass), and re-invocation of mutating plugins can follow. - Implementations: ~20 built-in admission plugins (
LimitRanger,ResourceQuota,PodSecurity,ServiceAccount, etc.) plus webhook-backed admission (MutatingWebhookConfiguration,ValidatingWebhookConfiguration). Custom admission plugins from operators use these interfaces via the aggregated API server framework. - Design quality: Well-segregated. The
Attributesinterface passed to each plugin carries the full request context (name, namespace, GVK, user info, dry-run flag, old and new objects) without exposing internal implementation details.ObjectInterfacesprovides the type-system tools (converter, defaulter, typer) the plugin may need. The cleanly separatedMutationInterface/ValidationInterfacetypes allow the framework to enforce ordering without runtime checks.
SharedInformer / SharedIndexInformer#
- Package:
k8s.io/client-go/tools/cache - File:
staging/src/k8s.io/client-go/tools/cache/shared_informer.go:144 - Methods:
// SharedInformer AddEventHandler(handler ResourceEventHandler) (ResourceEventHandlerRegistration, error) AddEventHandlerWithResyncPeriod(handler ResourceEventHandler, resyncPeriod time.Duration) (ResourceEventHandlerRegistration, error) AddEventHandlerWithOptions(handler ResourceEventHandler, options HandlerOptions) (ResourceEventHandlerRegistration, error) RemoveEventHandler(handle ResourceEventHandlerRegistration) error GetStore() Store GetController() Controller // deprecated Run(stopCh <-chan struct{}) RunWithContext(ctx context.Context) HasSynced() bool HasSyncedChecker() DoneChecker LastSyncResourceVersion() string SetWatchErrorHandler(handler WatchErrorHandler) error SetWatchErrorHandlerWithContext(handler WatchErrorHandlerWithContext) error SetTransform(handler TransformFunc) error IsStopped() bool // SharedIndexInformer embeds SharedInformer and adds: AddIndexers(indexers Indexers) error GetIndexer() Indexer - Purpose: The universal read path for all controllers. A
SharedInformerperforms a LIST+WATCH against the API server once and multiplexes events to N registeredResourceEventHandlercallbacks. The result is a local cache (aStore) that is eventually consistent with the API server. Controllers never call the API server directly for reads; they always query the informer’s cache.SharedIndexInformeradds secondary indexes (e.g., “pods by node”) on top of the base cache for O(1) lookups. - Implementations:
sharedIndexInformer(concrete struct in the same package).SharedInformerFactory(fromk8s.io/client-go/informers) creates one informer per GVR and ensures it is shared across all consumers in a process. - Design quality: Well-designed but sizeable (15 methods on
SharedInformer). The size is justified: lifecycle management (Run/Stop), event handler registration/removal, sync state queries (HasSynced), and error/transform hooks are all distinct concerns that cannot be cleanly split without fragmenting the interface. The introduction ofHasSyncedChecker()as a separateDoneCheckerinterface is a recent addition that allows polling without holding a reference to the informer itself — a clean ISP extension.
Scheduler Plugin hierarchy#
- Package:
k8s.io/kube-scheduler/framework - File:
staging/src/k8s.io/kube-scheduler/framework/interface.go:436 - Methods (selected extension point interfaces):(Full extension point set: PreEnqueue, QueueSort, PreFilter, Filter, PostFilter, PreScore, Score, Reserve, Permit, PreBind, Bind, PostBind, Sign, PlacementGenerate)
// Plugin — base marker Name() string // FilterPlugin Filter(ctx context.Context, state CycleState, pod *v1.Pod, nodeInfo NodeInfo) *Status PreFilterExtensions() PreFilterExtensions // ScorePlugin Score(ctx context.Context, state CycleState, p *v1.Pod, nodeInfo NodeInfo) (int64, *Status) ScoreExtensions() ScoreExtensions // ReservePlugin Reserve(ctx context.Context, state CycleState, p *v1.Pod, nodeName string) *Status Unreserve(ctx context.Context, state CycleState, p *v1.Pod, nodeName string) // PermitPlugin Permit(ctx context.Context, state CycleState, p *v1.Pod, nodeName string) (*Status, time.Duration) // BindPlugin Bind(ctx context.Context, state CycleState, p *v1.Pod, nodeName string) *Status - Purpose: The scheduler’s entire decision pipeline is expressed as a family of small, composable interfaces. Each interface corresponds to one extension point in the scheduling cycle. A plugin can implement any subset of extension points by implementing the corresponding interfaces. The
Pluginbase (marker withName()) provides the registration key; the framework uses type assertions to discover which extension points each plugin implements at startup. - Implementations: ~20 in-tree plugins (
NodeAffinity,VolumeBinding,PodTopologySpread,DefaultBinder, etc.) each implementing 1–5 extension point interfaces. Third-party schedulers extend this via the Scheduler Framework. - Design quality: Exemplary ISP application. Each extension point is a separate 1–3 method interface. Plugins opt in only to what they need.
CycleState(typed key-value store per scheduling cycle) provides inter-plugin communication without coupling plugins to each other directly. The introduction ofSignPlugin(for batching/caching scheduling results) is an example of the framework growing new extension points non-disruptively.
workqueue.TypedInterface[T] (bonus — architecturally foundational)#
- Package:
k8s.io/client-go/util/workqueue - File:
staging/src/k8s.io/client-go/util/workqueue/queue.go:30 - Methods:
Add(item T) Len() int Get() (item T, shutdown bool) Done(item T) ShutDown() ShutDownWithDrain() ShuttingDown() bool - Purpose: A generic (Go 1.18+), deduplicating, rate-limited FIFO queue. Every controller’s reconcile loop uses this: informer events add keys to the queue; worker goroutines call
Get()andDone(). Deduplication means that if an object changes 100 times before the worker processes it, the worker reconciles it once with the latest state. This is the mechanical foundation of level-triggered reconciliation. - Implementations:
processingWorkQueue(base),delayingQueue(adds delayed re-enqueue),rateLimitingQueue(wraps delaying with aTypedRateLimiter). The type parameterT comparablewas added in Go 1.18; the oldInterface = TypedInterface[any]alias maintains backward compatibility. - Design quality: Minimal, correct. The
Add/Get/Donetrio cleanly encodes the “at-least-once delivery with in-flight tracking” contract. The generics retrofit is clean: the alias provides backward compatibility without source breakage.
Interface patterns#
Size distribution#
- Marker interfaces (1 method): Very common —
Plugin.Name(),admission.Interface.Handles(),ObjectCreater.New(),ObjectDefaulter.Default(). Used as type-safe capability flags or registration keys. - Small (2–4 methods): The dominant style —
runtime.Object(2),MutationInterface(2),ValidationInterface(2),FilterPlugin(2),ScorePlugin(2),BindPlugin(1). Reflects strong ISP discipline in the newer scheduler framework. - Medium (5–10 methods):
Attributes(~13 getters — query object for admission decisions),SharedInformer(~15 lifecycle methods). - Large (10+ methods):
storage.Interface(13). These are legacy accumulation points or unavoidably cohesive contracts.
Embedding (composition)#
Interface embedding is a deliberate composition tool throughout:
SerializerembedsEncoder+DecoderSharedIndexInformerembedsSharedInformerMutationInterfaceandValidationInterfaceboth embedadmission.Interface- All scheduler extension point interfaces embed
Plugin NondeterministicEncoderextendsEncoderEquivalentResourceRegistryextendsEquivalentResourceMapper
This gives a clean “is-a” hierarchy while keeping each piece independently testable.
Implicit satisfaction#
Kubernetes relies entirely on Go’s structural typing — no explicit implements declarations anywhere. The scheduler framework exploits this: at plugin startup, the framework uses type assertions (if _, ok := plugin.(FilterPlugin); ok) to discover which extension points each plugin implements. This means plugins opt in silently, and the framework never needs a registration API beyond returning the plugin from a factory function.
stdlib interfaces used#
io.Writer,io.Reader,io.ReadCloser— used throughout the codec/serializer stack (Encoder.Encode(obj, io.Writer))io.Writer—Framer.NewFrameWritercontext.Context— pervasive; every storage, admission, and scheduler method takes a context as first argfmt.Stringer— not widely used in the core interfaces (Kubernetes uses structured logging)sort.Interface— not directly used; scheduling usesLessFuncinstead
Generics usage#
Introduced carefully in client-go:
workqueue.TypedInterface[T comparable]— the primary generic interface; oldInterfacebecomes a= TypedInterface[any]alias for backward compatibilityworkqueue.TypedRateLimiter[T comparable],TypedDelayingInterface[T comparable]admission/plugin/policy/internal/genericuses generics for its controller/lister/informer abstractions
The pattern: generics were applied to the work queue and its variants, which are the most reused primitives. Core API types remain non-generic because the Kubernetes type system predates Go 1.18 and backward compatibility is paramount.
Key abstractions#
runtime.Object— The 2-method root interface is the single most consequential design decision in the codebase. Every API type implements it; the entire type system (scheme, codec, conversion) is built on top of it. Its minimalism (only kind metadata + deep copy) is deliberate: all other type capabilities are separate narrow interfaces composed as needed.storage.Interface— The architectural firewall between the API server and persistence. All 100+ REST storage handlers call through exactly this interface; swapping backends (hypothetically) would require only a newstorage.Interfaceimplementation. In practice, only etcd3 exists, but the abstraction keeps persistence logic completely out of the REST layer.admission.Interface/MutationInterface/ValidationInterface— The three-part admission contract. The split between mutation and validation at the interface level enforces the two-phase admission pipeline in a way that is verified at compile time, not at runtime policy. Every admission security feature (PodSecurity, ResourceQuota, webhooks) is expressed through these interfaces.SharedInformer— The contract that makes Kubernetes controllers scalable. By mandating that all reads go through a shared, locally cached watch stream, the API server is shielded from N×M read load (N controllers × M objects). Any operator or controller framework built on client-go implicitly adopts this contract.Scheduler
Pluginhierarchy — The purest expression of ISP in the project. 14 extension points × 1–3 methods each, independently composable. This design is the reason the scheduler is extensible without forking: third parties implement only the interfaces they need and ignore the rest.
Interface-driven extensibility#
Kubernetes uses interfaces as the primary extension mechanism across all subsystems:
| Subsystem | Extension interface(s) | How plugins register |
|---|---|---|
| Admission | MutationInterface, ValidationInterface | Plugins.Register(name, factory) in pkg/admission/plugins.go |
| Authorization | authorizer.Authorizer | Registered via --authorization-modes flag; built and chained at startup |
| Authentication | authenticator.Request | Built from --authentication-* flags into a union authenticator |
| Storage | storage.Interface | Factory selected by --storage-backend (only etcd3 in practice) |
| Scheduler | Plugin family (14 interfaces) | OutOfTreeRegistry map of PluginFactory functions passed at startup |
| CRI (kubelet) | RuntimeService, ImageService (gRPC-defined) | gRPC client connected to --container-runtime-endpoint socket |
| Aggregated API | genericapiserver.DelegationTarget | CreateServerChain assembles the delegation chain of API servers |
The pattern is consistent: define a narrow interface, provide built-in implementations at startup from flags/config, allow external code to substitute implementations through the same interface. There is no plugin registry at runtime — all extension points are wired at process startup. This makes the system auditable and fast (no dynamic dispatch overhead beyond normal Go interface calls) at the cost of requiring a restart to change the extension set.