istio.io/istio@v0.0.0-20240520182934-d79c90f27776/pkg/kube/krt/README.md (about) 1 # `krt`: **K**ubernetes Declarative Controller **R**un**t**ime 2 3 `krt` provides a framework for building _declarative_ controllers. 4 See the [design doc](https://docs.google.com/document/d/1-ywpCnOfubqg7WAXSPf4YgbaFDBEU9HIqMWcxZLhzwE/edit#heading=h.ffjmk8byb9gt) and [KubeCon talk](https://sched.co/1R2oY) for more background. 5 6 The framework aims to solve a few problems with writing controllers: 7 * Operate on any types, from any source. Kubernetes provides informers, but these only work on Kubernetes types read from Kubernetes objects. 8 * `krt` can accept any object type, from any source, and handle them the same. 9 * Provide high level abstractions. 10 * Controller authors can write simple transformation functions from `Input` -> `Output` (with dependencies); the framework handles all the state automatically. 11 12 ## Key Primitives 13 14 The most important primitive provided is the `Collection` interface. 15 This is basically an `Informer`, but not tied to Kubernetes. 16 17 Currently, there are three ways to build a `Collection`: 18 * Built from an `Informer` with `WrapClient` or `NewInformer`. 19 * Statically configured with `NewStatic`. 20 * Derived from other collections (more information on this below). 21 22 Unlike `Informers`, these primitives work on arbitrary objects. 23 However, these objects are expected to have some properties, depending on their usage. 24 These are *not* expressed as generic constraints due to limitations in Go's type system. 25 26 * Each object `T` must have a unique `Key[T]` (which is just a typed wrapper around `string`) that uniquely identifies the object. 27 Default implementations exist for Kubernetes objects, Istio `config.Config` objects, and `ResourceName() string` implementations. 28 * `Equals(k K) bool` may be implemented to provide custom implementations to compare objects. Comparison is done to detect if changes were made. 29 Default implementations are available for Kubernetes and protobuf objects, and will fallback to `reflect.DeepEqual`. 30 * A `Name`, `Namespace`, `Labels`, and `LabelSelector` may optionally be included, for use with filters (see below). 31 32 ## Derived Collections 33 34 The core of the framework is in the ability to derive collections from others. 35 36 In general, these are built by providing some `func(inputs...) outputs...` (called "transformation" functions). 37 While more could be expressed, there are currently three forms implemented. 38 39 * `func() *O` via `NewSingleton` 40 * This generates a collection that has a single value. An example would be some global configuration. 41 * `func(input I) *O` via `NewCollection` 42 * This generates a one-to-one mapping of input to output. An example would be a transformation from a `Pod` type to a generic `Workload` type. 43 * `func(input I) []O` via `NewManyCollection` 44 * This generates a one-to-many mapping of input to output. An example would be a transformation from a `Service` to a _set_ of `Endpoint` types. 45 * The order of the response does not matter. Each response must have a unique key. 46 47 The form used and input type only represent the _primary dependencies_, indicating the cardinality. 48 Each transformation can additionally include an arbitrary number of dependencies, fetching data from other collections. 49 50 For example, a simple `Singleton` example that keeps track of the number of `ConfigMap`s in the cluster: 51 52 ```go 53 ConfigMapCount := krt.NewSingleton[int](func(ctx krt.HandlerContext) *int { 54 cms := krt.Fetch(ctx, ConfigMaps) 55 return ptr.Of(len(cms)) 56 }) 57 ``` 58 59 The `Fetch` operation enables querying against other collections. 60 If the result of the `Fetch` operation changes, the collection will automatically be recomputed; the framework handles the state and event detection. 61 In the above example, the provided function will be called (at least) every time there is a change to a configmap. 62 The `ConfigMapCount` collection will produce events only when the count changes. 63 The framework will use generic Equals on the underlying object to determine whether or not to recompute collections. 64 65 ### Picking a collection type 66 67 There are a variety of collection types available. 68 Picking these is about simplicity, usability, and performance. 69 70 The `NewSingleton` form (`func() *O`), in theory, could be used universally. 71 Consider a transformation from `Pod` to `SimplePod`: 72 73 ```go 74 SimplePods := krt.NewSingleton[SimplePod](func(ctx krt.HandlerContext) *[]SimplePod { 75 res := []SimplePod{} 76 for _, pod := range krt.Fetch(ctx, Pod) { 77 res = append(res, SimplePod{Name: pod.Name}) 78 } 79 return &res 80 }) // Results in a Collection[[]SimplePod] 81 ``` 82 83 While this *works*, it is inefficient and complex to write. 84 Consumers of SimplePod can only query the entire list at once. 85 Anytime *any* `Pod` changes, *all* `SimplePod`s must be recomputed. 86 87 A better approach would be to lift `Pod` into a primary dependency: 88 89 ```go 90 SimplePods := krt.NewCollection[SimplePod](func(ctx krt.HandlerContext, pod *v1.Pod) *SimplePod { 91 return &SimplePod{Name: pod.Name} 92 }) // Results in a Collection[SimplePod] 93 ``` 94 95 Not only is this simpler to write, its far more efficient. 96 Consumers can more efficiently query for `SimplePod`s using label selectors, filters, etc. 97 Additionally, if a single `Pod` changes we only recompute one `SimplePod`. 98 99 Above we have a one-to-one mapping of input and output. 100 We may have one-to-many mappings, though. 101 In these cases, usually its best to use a `ManyCollection`. 102 Like the above examples, its *possible* to express these as normal `Collection`s, but likely inefficient. 103 104 Example computing a list of all container names across all pods: 105 106 ```go 107 ContainerNames := krt.NewManyCollection[string](func(ctx krt.HandlerContext, pod *v1.Pod) (res []string) { 108 for _, c := range pod.Spec.Containers { 109 res = append(res, c.Name) 110 } 111 return res 112 }) // Results in a Collection[string] 113 ``` 114 115 Example computing a list of service endpoints, similar to the Kubernetes core endpoints controller: 116 117 ```go 118 Endpoints := krt.NewManyCollection[Endpoint](func(ctx krt.HandlerContext, svc *v1.Service) (res []Endpoint) { 119 for _, c := range krt.Fetch(ctx, Pods, krt.FilterLabel(svc.Spec.Selector)) { 120 res = append(res, Endpoint{Service: svc.Name, Pod: pod.Name, IP: pod.status.PodIP}) 121 } 122 return res 123 }) // Results in a Collection[Endpoint] 124 ``` 125 126 As a rule of thumb, if your `Collection` type is a list, you most likely should be using a different type to flatten the list. 127 An exception to this would be if the list represents an atomic set of items that are never queried independently; 128 in these cases, however, it is probably best to wrap it in a struct. 129 For example, to represent the set of containers in a pod, we may make a `type PodContainers struct { Name string, Containers []string }` and have a 130 `Collection[PodContainers]` rather than a `Collection[[]string]`. 131 132 In theory, other forms could be expressed such as `func(input1 I1, input2 I2) *O`. 133 However, there haven't yet been use cases for these more complex forms. 134 135 ### Transformation constraints 136 137 In order for the framework to properly handle dependencies and events, transformation functions must adhere by a few properties. 138 139 Basically, Transformations must be stateless and idempotent. 140 * Any querying of other `Collection`s _must_ be done through `krt.Fetch`. 141 * Querying other data stores that may change is not permitted. 142 * Querying external state (e.g. making HTTP calls) is not permitted. 143 * Transformations _may_ be called at any time, including many times for the same inputs. Transformation functions should not make any assumptions about calling patterns. 144 145 Violation of these properties will result in undefined behavior (which would likely manifest as stale data). 146 147 ### Fetch details 148 149 In addition to simply fetching _all_ resources from a collection, a filter can be provided. 150 This is more efficient than filtering outside of `Fetch`, as the framework can filter un-matched objects earlier, skipping redundant work. 151 The following filters are provided 152 153 * `FilterName(name, namespace)`: filters an object by Name and Namespace. 154 * `FilterNamespace(namespace)`: filters an object by Namespace. 155 * `FilterKey(key)`: filters an object by key. 156 * `FilterLabel(labels)`: filters to only objects that match these labels. 157 * `FilterSelects(labels)`: filters to only objects that **select** these labels. An empty selector matches everything. 158 * `FilterSelectsNonEmpty(labels)`: filters to only objects that **select** these labels. An empty selector matches nothing. 159 * `FilterGeneric(func(any) bool)`: filters by an arbitrary function. 160 161 Note that most filters may only be used if the objects being `Fetch`ed implement appropriate functions to extract the fields filtered against. 162 Failures to meet this requirement will result in a `panic`. 163 164 ## Library Status 165 166 This library is currently "experimental" and is not used in Istio production yet. 167 The intent is this will be slowly rolled out to controllers that will benefit from it and are lower risk; 168 likely, the ambient controller will be the first target. 169 170 While its _plausible_ all of Istio could be fundamentally re-architected to fully embrace `krt` throughout (replacing things like `PushContext`), 171 it is not yet clear this is desired. 172 173 ### Performance 174 175 Compared to a perfectly optimized hand-written controller, `krt` adds some overhead. 176 However, writing a perfectly optimized controller is hard, and often not done. 177 As a result, for many scenarios it is expected that `krt` will perform on-par or better. 178 179 This is similar to a comparison between a high level programming language compared to assembly; 180 while its always possible to write better code in assembly, smart compilers can make optimizations humans are unlikely to, 181 such as loop unrolling. 182 Similarly, `krt` can make complex optimizations in one place, so each controller implementation doesn't, which is likely to increase 183 the amount of optimizations applied. 184 185 The `BenchmarkControllers` puts this to the test, comparing an *ideal* hand-written controller to one written in `krt`. 186 While the numbers are likely to change over time, at the time of writing the overhead for `krt` is roughly 10%: 187 188 ```text 189 name time/op 190 Controllers/krt-8 13.4ms ±23% 191 Controllers/legacy-8 11.4ms ± 6% 192 193 name alloc/op 194 Controllers/krt-8 15.2MB ± 0% 195 Controllers/legacy-8 12.9MB ± 0% 196 ``` 197 198 ### Future work 199 200 #### Object optimizations 201 202 One important aspect of `krt` is its ability to automatically detect if objects have changed, and only trigger dependencies if so. 203 This works better when we only compare fields we actually use. 204 Today, users can do this manually by making a transformation from the full object to a subset of the object. 205 206 This could be improved by: 207 * Automagically detecting which subset of the object is used, and optimize this behind the scenes. This seems unrealistic, though. 208 * Allow a lightweight form of a `Full -> Subset` transformation, that doesn't create a full new collection (with respective overhead), but rather overlays on top of an existing one. 209 210 #### Internal dependency optimizations 211 212 Today, the library stores a mapping of `Input -> Dependencies` (`map[Key[I]][]dependency`). 213 Often times, there are common dependencies amongst keys. 214 For example, a namespace filter probably has many less unique values than unique input objects. 215 Other filters may be completely static and shared by all keys. 216 217 This could be improved by: 218 * Optimize the data structure to be a bit more advanced in sharing dependencies between keys. 219 * Push the problem to the user; allow them to explicitly set up static `Fetch`es. 220 221 #### Debug tooling 222 223 `krt` has an opportunity to add a lot of debugging capabilities that are hard to do elsewhere, because it would require 224 linking up disparate controllers, and a lot of per-controller logic. 225 226 Some debugging tooling ideas: 227 * Add OpenTelemetry tracing to controllers ([prototype](https://github.com/howardjohn/istio/commits/experiment/cv2-tracing)). 228 * Automatically generate mermaid diagrams showing system dependencies. 229 * Automatically detect violations of [Transformation constraints](#transformation-constraints).