istio/istio
krt (Kubernetes declarative controller runtime)
Active contributors: howardjohn, stevenctl, hzxuzhonghu, keithmattix
Purpose
krt is Istio's home-grown declarative controller framework. It provides a way to write controllers as func(input) -> output transformations and lets the framework manage state, dependencies, and event delivery. The framework was added to the codebase in December 2023 (Initial krt library, ("Building Better Controllers") (#47891)) specifically to support Ambient mode's cross-cutting state machines, and it has since become the substrate for new controller code throughout the project.
The package is pkg/kube/krt/. It has its own README with the original design doc and a KubeCon talk linked from the top.
Why it exists
Hand-written Kubernetes controllers all share the same shape: an informer per type, a workqueue, a reconciler that pulls from the queue and reads the current state from informers. That pattern works for "watch X, do Y", but it falls apart when you need to compose state from many sources. The Ambient registry needs to combine Pods, Services, ServiceEntries, Gateway API resources, namespaces, AuthorizationPolicies, and a half-dozen others into a single denormalized output. Doing that with workqueues meant manual dependency tracking, manual fan-out, and manual deduplication.
krt replaces that pattern with declarative transforms:
ConfigMapCount := krt.NewSingleton[int](func(ctx krt.HandlerContext) *int {
cms := krt.Fetch(ctx, ConfigMaps)
return ptr.Of(len(cms))
})The framework tracks that ConfigMapCount depends on ConfigMaps. When ConfigMaps changes, ConfigMapCount recomputes automatically; consumers get an event with the new value.
Directory layout
pkg/kube/krt/
├── README.md # The user-facing intro
├── core.go # Collection interface, Event, HandlerContext
├── collection.go # The default Collection implementation (~900 lines)
├── informer.go # Build a Collection from a Kubernetes informer
├── static.go # Build a Collection from a static slice
├── singleton.go # Single-value collections
├── join.go # Join two collections by key
├── mergejoin.go # Merge collections with optional types
├── nestedjoinmerge.go # Nested joins (rare; powerful)
├── map.go # Map a Collection through another function
├── index.go # Secondary indexes
├── filter.go # Fetch filters (by name, labels, generic predicate)
├── fetch.go # Fetch operation
├── recomputetrigger.go # Manual triggers (escape hatch)
├── status.go # Status reconciliation helpers
├── helpers.go / processor.go / sync.go / options.go
├── debug.go # Runtime introspection
├── assert_*.go # Optional invariant assertions
└── krttest/ # Tests helpers (TestController etc.)Key abstractions
| Symbol | File | Role |
|---|---|---|
Collection[T] |
core.go |
The central interface: an observable set of T |
Singleton[T] |
singleton.go |
A Collection of one |
HandlerContext |
core.go |
Passed to transforms; tracks dependencies |
Fetch[T] |
fetch.go |
Read inputs and register a dependency |
NewCollection[I, O] |
collection.go |
One-to-one: each I becomes one O (or none) |
NewManyCollection[I, O] |
collection.go |
One-to-many |
NewSingleton[O] |
singleton.go |
Pure: depends only on Fetch'd collections |
WrapClient[T] |
informer.go |
Wrap a Kubernetes informer |
JoinCollection, Map, MergeJoin, NestedJoinMerge |
various | Composition combinators |
Index |
index.go |
Secondary index over a Collection |
How it works
graph LR
inf[Kube informer] --> wrap[WrapClient]
wrap --> coll[Collection]
coll --> fetch[Fetch in handler]
fetch --> tx[Transform fn]
tx --> derived[Derived Collection]
derived --> ev[Event handlers]
classDef in fill:#cef,stroke:#08f
classDef out fill:#fec,stroke:#f80
class inf,coll in
class derived,ev outObject requirements
Every object T in a Collection[T] needs:
- A unique
Key[T](a typed string). Default implementations exist for Kubernetes types, Istioconfig.Config, and any type with aResourceName() string. - An
Equals(T) boolmethod. Default implementations exist for Kubernetes objects (comparesResourceVersion), protobuf messages (compares marshaled output), and any other type viareflect.DeepEqual. - Optionally a
Name,Namespace,Labels, andLabelSelectorfor filter support.
Three transform shapes
// One-to-one: pod → workload (or nil)
Workloads := krt.NewCollection[Pod, Workload](Pods, func(ctx krt.HandlerContext, p Pod) *Workload {
ns := krt.FetchOne(ctx, Namespaces, krt.FilterKey(p.Namespace))
if ns == nil || !ns.AmbientEnabled() {
return nil
}
return &Workload{Name: p.Name, Identity: p.SAIdentity()}
})
// One-to-many: service → list of endpoints
Endpoints := krt.NewManyCollection[Service, Endpoint](Services, func(ctx krt.HandlerContext, s Service) []Endpoint {
pods := krt.Fetch(ctx, Pods, krt.FilterLabel(s.Selector))
return mapPodsToEndpoints(pods)
})
// Pure: depends only on what Fetch reads
Total := krt.NewSingleton[int](func(ctx krt.HandlerContext) *int {
n := len(krt.Fetch(ctx, Workloads))
return &n
})Dependency tracking
Each Fetch call records a dependency on the source collection. When a fetch is filtered (e.g. FilterLabel), the framework remembers the filter so it can decide whether a given input change should re-run the transform. This is what makes krt cheaper than naïve "recompute everything on every change" — most events touch only a handful of dependent transforms.
Status
status.go provides the pattern Istio uses to reconcile status fields back to a CR: declare a StatusCollection keyed by the CR, compute the desired status as a transform, and the framework writes it back via the kube client. This replaced ad-hoc status writers in several controllers.
Static and singleton
For controllers that need an in-process source of truth (e.g. mesh config), NewStatic produces a Collection from a slice and exposes setters. Tests use this heavily.
Where it's used
The biggest user is the Ambient registry. Other adopters include:
- The Kubernetes Gateway API translator (
pilot/pkg/config/kube/gateway/). - Multicluster controller (
pkg/kube/multicluster/). - Status reconcilers (
pilot/pkg/status/). - The ipallocate controller (
pilot/pkg/controllers/ipallocate/). pilot/pkg/xds/krtxds.go— feeds krt-derived data into the xDS push pipeline.
Sidecar-mode networking still uses hand-written informers and queues. There is no plan to rewrite that code; the rule of thumb is "use krt for new state machines".
Limitations
- Generic constraints are not expressed in the Go type system. The
Key/Equalsrequirements are enforced at runtime, not compile time. - The framework is single-process. There is no distributed equivalent.
- Stage-by-stage debugging requires the
debug.gointrospection endpoints; printf debugging is harder than with a queue-based controller. - For "fire and forget" controllers (read X, do Y, no derived state), informers + workqueues are still simpler.
Performance
krt's bench suite is in pkg/kube/krt/bench_test.go. The framework adds overhead vs hand-written informers — typically tens of microseconds per event in steady state — but the trade-off is worth it for any controller with 3+ inputs. The Ambient registry, with about a dozen sources, would be substantially more code without it.
Entry points for modification
- New combinator → look at
join.go/mergejoin.gofor examples; add tests in the same package. - New filter —
filter.go. Filters compose viaFilterAnd/FilterOr. - Performance tuning — the hot path is
Collection.run()incollection.go. Profile underbench_test.go.
Key source files
| File | Purpose |
|---|---|
pkg/kube/krt/README.md |
The intro |
pkg/kube/krt/core.go |
Public interface |
pkg/kube/krt/collection.go |
Default Collection impl |
pkg/kube/krt/informer.go |
Kube-informer adapter |
pkg/kube/krt/singleton.go |
Single-value collections |
pkg/kube/krt/join.go |
Join combinator |
pkg/kube/krt/index.go |
Secondary indexes |
pkg/kube/krt/fetch.go |
Fetch operation |
pkg/kube/krt/conformance_test.go |
The cross-implementation conformance suite |
pkg/kube/krt/bench_test.go |
Microbenchmarks |
See also
- systems/ambient-mesh — the largest user.
- systems/service-discovery — krt-driven Ambient registry.
Built by Factory AutoWiki from public repository content. It is a generated preview for codebase exploration, not source-maintained documentation.