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. GetObjectKind returns the Group/Version/Kind metadata; DeepCopyObject enables 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 generated zz_generated.deepcopy.go files supply the DeepCopyObject body.
  • Design quality: Minimal by design — only 2 methods. Deliberately ISP-conformant: every other capability (defaulting, versioning, conversion) is a separate interface (ObjectDefaulter, ObjectVersioner, ObjectConvertor). The Scheme uses 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.Cacher wrapping 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 RequestWatchProgress as a temporary addition. The breadth violates ISP but reflects the pragmatic reality that all callers need the full contract. The GuaranteedUpdate pattern (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 (MutationInterface vs ValidationInterface) 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 Attributes interface 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. ObjectInterfaces provides the type-system tools (converter, defaulter, typer) the plugin may need. The cleanly separated MutationInterface/ValidationInterface types 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 SharedInformer performs a LIST+WATCH against the API server once and multiplexes events to N registered ResourceEventHandler callbacks. The result is a local cache (a Store) that is eventually consistent with the API server. Controllers never call the API server directly for reads; they always query the informer’s cache. SharedIndexInformer adds 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 (from k8s.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 of HasSyncedChecker() as a separate DoneChecker interface 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):
    // 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
    (Full extension point set: PreEnqueue, QueueSort, PreFilter, Filter, PostFilter, PreScore, Score, Reserve, Permit, PreBind, Bind, PostBind, Sign, PlacementGenerate)
  • 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 Plugin base (marker with Name()) 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 of SignPlugin (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() and Done(). 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 a TypedRateLimiter). The type parameter T comparable was added in Go 1.18; the old Interface = TypedInterface[any] alias maintains backward compatibility.
  • Design quality: Minimal, correct. The Add/Get/Done trio 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:

  • Serializer embeds Encoder + Decoder
  • SharedIndexInformer embeds SharedInformer
  • MutationInterface and ValidationInterface both embed admission.Interface
  • All scheduler extension point interfaces embed Plugin
  • NondeterministicEncoder extends Encoder
  • EquivalentResourceRegistry extends EquivalentResourceMapper

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.WriterFramer.NewFrameWriter
  • context.Context — pervasive; every storage, admission, and scheduler method takes a context as first arg
  • fmt.Stringer — not widely used in the core interfaces (Kubernetes uses structured logging)
  • sort.Interface — not directly used; scheduling uses LessFunc instead

Generics usage#

Introduced carefully in client-go:

  • workqueue.TypedInterface[T comparable] — the primary generic interface; old Interface becomes a = TypedInterface[any] alias for backward compatibility
  • workqueue.TypedRateLimiter[T comparable], TypedDelayingInterface[T comparable]
  • admission/plugin/policy/internal/generic uses 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#

  1. 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.

  2. 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 new storage.Interface implementation. In practice, only etcd3 exists, but the abstraction keeps persistence logic completely out of the REST layer.

  3. 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.

  4. 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.

  5. Scheduler Plugin hierarchy — 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:

SubsystemExtension interface(s)How plugins register
AdmissionMutationInterface, ValidationInterfacePlugins.Register(name, factory) in pkg/admission/plugins.go
Authorizationauthorizer.AuthorizerRegistered via --authorization-modes flag; built and chained at startup
Authenticationauthenticator.RequestBuilt from --authentication-* flags into a union authenticator
Storagestorage.InterfaceFactory selected by --storage-backend (only etcd3 in practice)
SchedulerPlugin 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 APIgenericapiserver.DelegationTargetCreateServerChain 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.