Skip to content

Commit f8cd314

Browse files
authored
Merge pull request #1 from authzed/readme
initial readme
2 parents 6496f3c + 480b289 commit f8cd314

File tree

6 files changed

+269
-67
lines changed

6 files changed

+269
-67
lines changed

README.md

+206
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,206 @@
1+
# controller-idioms
2+
3+
[![Go Report Card](https://goreportcard.com/badge/github.com/authzed/controller-idioms)](https://goreportcard.com/report/github.com/authzed/controller-idioms)
4+
[![Go Documentation](https://pkg.go.dev/badge/github.com/authzed/controller-idioms)](https://pkg.go.dev/github.com/authzed/controller-idioms)
5+
[![Discord Server](https://img.shields.io/discord/844600078504951838?color=7289da&logo=discord "Discord Server")](https://discord.gg/jTysUaxXzM)
6+
[![Twitter](https://img.shields.io/twitter/follow/authzed?color=%23179CF0&logo=twitter&style=flat-square&label=@authzed "@authzed on Twitter")](https://twitter.com/authzed)
7+
8+
controller-idioms is a collection of generic libraries that complement and extend fundamental Kubernetes libraries (e.g. [controller-runtime]) to implement best practices for Kubernetes controllers.
9+
10+
These libraries were originally developed by [Authzed] to build the [SpiceDB Operator] and their internal projects powering [Authzed Dedicated].
11+
12+
[controller-runtime]: https://github.com/kubernetes-sigs/controller-runtime
13+
[Authzed]: https://authzed.com
14+
[SpiceDB Operator]: https://github.com/authzed/spicedb-operator
15+
[Authzed Dedicated]: https://authzed.com/pricing
16+
17+
Available idioms include:
18+
19+
- **[adopt]**: efficiently watch resources the controller doesn't own (e.g. references to a secret or configmap)
20+
- **[bootstrap]**: install required CRDs and default CRs typically for CD pipelines
21+
- **[component]**: manage and aggregate resources that are created on behalf of another resource
22+
- **[fileinformer]**: an InformerFactory that watches local files typically for loading config without restarting
23+
- **[hash]**: hashing resources to detect modifications
24+
- **[metrics]**: metrics for resources that implement standard `metav1.Condition` arrays
25+
- **[pause]**: handler that allows users stop the controller reconciling a particular resource without stopping the controller
26+
- **[static]**: controller for "static" resources that should always exist on startup
27+
28+
[adopt]: https://pkg.go.dev/github.com/authzed/controller-idioms/adopt
29+
[bootstrap]: https://pkg.go.dev/github.com/authzed/controller-idioms/bootstrap
30+
[component]: https://pkg.go.dev/github.com/authzed/controller-idioms/component
31+
[fileinformer]: https://pkg.go.dev/github.com/authzed/controller-idioms/fileinformer
32+
[hash]: https://pkg.go.dev/github.com/authzed/controller-idioms/hash
33+
[metrics]: https://pkg.go.dev/github.com/authzed/controller-idioms/metrics
34+
[pause]: https://pkg.go.dev/github.com/authzed/controller-idioms/pause
35+
[static]: https://pkg.go.dev/github.com/authzed/controller-idioms/static
36+
37+
Have questions? Join our [Discord].
38+
39+
Looking to contribute? See [CONTRIBUTING.md].
40+
41+
[Discord]: https://authzed.com/discord
42+
[CONTRIBUTING.md]: https://github.com/authzed/spicedb/blob/main/CONTRIBUTING.md
43+
44+
## Overview
45+
46+
### Handlers
47+
48+
A `Handler` is a small, composable, reusable piece of a controller's state machine.
49+
It has a simple, familiar signature:
50+
51+
```go
52+
func (h *MyHandler) Handle(context.Context) {
53+
// do some work
54+
}
55+
```
56+
57+
Handlers are similar to an [`http.Handler`](https://pkg.go.dev/net/http#Handler) or a [`grpc.UnaryHandler`](https://pkg.go.dev/google.golang.org/grpc#UnaryHandler), but can pass control to another handler as needed.
58+
This allows handlers to compose in nice ways:
59+
60+
```go
61+
func mainControlLoop(ctx context.Context) {
62+
handler.Chain(
63+
validateServiceAccount,
64+
handler.Parallel(
65+
createJob,
66+
createPersistentVolume,
67+
)
68+
).Handle(ctx)
69+
}
70+
```
71+
72+
The `handler` package contains utilities for building, composing, and decorating handlers, and for building large state machines with them.
73+
See the [docs]() for more details.
74+
75+
Handlers take some inspiration from [statecharts](https://statecharts.dev/) to deal with the complexity of writing and maintaining controllers, while staying close to golang idioms.
76+
77+
### Typed Context
78+
79+
Breaking a controller down into small pieces with `Handler`s means that each piece either needs to re-calculate results from other stages or fetch the previously computed result from `context`.
80+
81+
The `typedctx` package provides generic helpers for storing / retrieving values from a `context.Context`.
82+
83+
```go
84+
var CtxExpensiveObject = typedctx.NewKey[ExpensiveComputation]()
85+
86+
func (h *ComputeHandler) Handle(ctx context.Context) {
87+
ctx = CtxExpensiveObject.WithValue(ctx, myComputedExpensiveObject)
88+
}
89+
90+
func (h *UseHandler) Handle(ctx context.Context) {
91+
precomputedExpensiveObject = CtxExpensiveObject.MustValue(ctx)
92+
// do something with object
93+
}
94+
```
95+
96+
`Handlers` are typically chained in a way that preserves the context between handlers, but not always.
97+
98+
For example:
99+
```go
100+
var CtxExpensiveObject = typedctx.NewKey[ExpensiveComputation]()
101+
102+
func (h *ComputeHandler) Handle(ctx context.Context) {
103+
ctx = CtxExpensiveObject.WithValue(ctx, myComputedExpensiveObject)
104+
}
105+
106+
func (h *DecorateHandler) Handle(ctx context.Context) {
107+
ComputeHandler{}.Handle(ctx)
108+
109+
// this fails, because the ctx passed into the wrapped handler isn't passed back out
110+
CtxExpensiveObject.MustValue(ctx)
111+
}
112+
```
113+
114+
To deal with these cases, `typedctx` provides a `Boxed` context type that instead stores a pointer to the object, with additional helpers for making a "space" for the pointer to be filled in later.
115+
116+
```go
117+
var CtxExpensiveObject = typedctx.Boxed[ExpensiveComputation](nil)
118+
119+
func (h *ComputeHandler) Handle(ctx context.Context) {
120+
ctx = CtxExpensiveObject.WithValue(ctx, myComputedExpensiveObject)
121+
}
122+
123+
func (h *DecorateHandler) Handle(ctx context.Context) {
124+
// adds an empty pointer
125+
ctx = CtxExpensiveObject.WithBox(ctx)
126+
127+
// fills in the pointer - note that the implementation of ComputeHandler didn't change
128+
ComputeHandler{}.Handle(ctx)
129+
130+
// now this succeeds, and returns the unboxed value
131+
CtxExpensiveObject.MustValue(ctx)
132+
}
133+
```
134+
135+
### Typed Informers, Listers, Indexers
136+
137+
The `typed` package converts (dynamic) kube informers, listers, and indexers into typed counterparts via generics.
138+
139+
```go
140+
informerFactory := dynamicinformer.NewFilteredDynamicSharedInformerFactory(client, defaultResync, namespace, tweakListOptions)
141+
indexer := informerFactory.ForResource(corev1.SchemeGroupVersion.WithResource("secrets")).Informer().Indexer()
142+
secretIndexer := typed.NewIndexer[*corev1.Secret](indexer)
143+
144+
// secrets is []*corev1.Secret instead of unstructured
145+
secrets, err := secretIndexer.ByIndex("my-index-name", "my-index-value")
146+
```
147+
148+
### Controllers and Managers
149+
150+
The `manager` package provides an optional lightweight controller `Manager` abstraction (similar to kubernetes controller manager, or the manager from controller runtime). It also provides a simple `Controller` abstraction and some basic implementations. Both are optional, `ktrllib` can be used without these.
151+
152+
The rest of `ktrllib` can be used without using these if you are already using another solution.
153+
154+
#### `Manager`
155+
156+
- provides a way to start and stop a collection of controllers together
157+
- controllers can be dynamically added/removed after the manager has started
158+
- starts health / debug servers that tie into the health of the underlying controllers
159+
160+
#### `BasicController`
161+
162+
- provides default implementations of health and debug servers
163+
164+
#### `OwnedResourceController`
165+
166+
- implements the most common pattern for a controller: reconciling a single resource type via a workqueue
167+
- has a single queue managing a single type
168+
- on start, starts processing objects from the queue
169+
- doesn't start any informers
170+
171+
### Informer Factory Registry
172+
173+
It can be useful to access the informer cache of one controller from another place, so that multiple controllers in the same binary don't need to open separate connections against the kube apiserver and maintain separate caches of the same objects.
174+
175+
The `typed` package provides a `Registry` that synchronizes access to shared informer factories across multiple controllers.
176+
177+
### Queue
178+
179+
The `queue` package provides helpers for working with client-go's `workqueues`.
180+
181+
`queue.OperationsContext` can be used from within a `Handler` to control the behavior of the queue that has called the handler.
182+
183+
The queue operations are:
184+
185+
- Done (stop processing the current key)
186+
- Requeue (requeue the current key)
187+
- RequeueAfter (wait for some period of time before requeuing the current key)
188+
- ReqeueueErr (record an error and requeue)
189+
- RequeueAPIError (requeue after waiting according to the priority and fairness response from the apiserver)
190+
191+
If calling these controls from a handler, it's important to `return` immediately so that the handler does not continue processing a key that the queue thinks has stopped.
192+
193+
194+
### Middleware
195+
196+
Middleware can be injected between handlers with the `middleware` package.
197+
198+
```go
199+
middleware.ChainWithMiddleware(
200+
middleware.NewHandlerLoggingMiddleware(4),
201+
)(
202+
c.checkPause,
203+
c.adoptSecret,
204+
c.validate
205+
).Handle(ctx)
206+
```

handler/chain.go

+18
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
package handler
2+
3+
// BuilderComposer is a function that composes sets of handler.Builder into
4+
// one handler.Builder, see `Chain` and `Parallel`.
5+
type BuilderComposer func(builder ...Builder) Builder
6+
7+
// Chain chains a set of handler.Builder together
8+
func Chain(children ...Builder) Builder {
9+
return func(...Handler) Handler {
10+
next := NoopHandler
11+
for i := len(children) - 1; i >= 0; i-- {
12+
next = children[i](next)
13+
}
14+
return next
15+
}
16+
}
17+
18+
var _ BuilderComposer = Chain

handler/parallel.go

+34
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
package handler
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"strings"
7+
"sync"
8+
)
9+
10+
// Parallel creates a new handler.Builder that runs a set of handler.Builder in
11+
// parallel
12+
func Parallel(children ...Builder) Builder {
13+
ids := make([]string, 0, len(children))
14+
for _, c := range children {
15+
ids = append(ids, string(c(NoopHandler).ID()))
16+
}
17+
return func(next ...Handler) Handler {
18+
return NewHandler(ContextHandlerFunc(func(ctx context.Context) {
19+
var g sync.WaitGroup
20+
for _, c := range children {
21+
c := c
22+
g.Add(1)
23+
go func() {
24+
c(NoopHandler).Handle(ctx)
25+
g.Done()
26+
}()
27+
}
28+
g.Wait()
29+
Handlers(next).MustOne().Handle(ctx)
30+
}), Key(fmt.Sprintf("parallel[%s]", strings.Join(ids, ","))))
31+
}
32+
}
33+
34+
var _ BuilderComposer = Parallel

middleware/chain.go

-20
This file was deleted.

middleware/middleware.go

+11-7
Original file line numberDiff line numberDiff line change
@@ -10,17 +10,21 @@ type HandlerMiddleware func(handler.Handler) handler.Handler
1010
// BuilderMiddleware returns a new (wrapped) Builder given a Builder
1111
type BuilderMiddleware func(handler.Builder) handler.Builder
1212

13-
// BuilderComposer is a function that composes sets of handler.Builder into
14-
// one handler.Builder, see `Chain` and `Parallel`.
15-
type BuilderComposer func(builder ...handler.Builder) handler.Builder
16-
1713
// Middleware operates on BuilderComposer (to wrap all underlying builders)
18-
type Middleware func(BuilderComposer) BuilderComposer
14+
type Middleware func(handler.BuilderComposer) handler.BuilderComposer
15+
16+
func ChainWithMiddleware(middleware ...Middleware) handler.BuilderComposer {
17+
return WithMiddleware(handler.Chain, middleware...)
18+
}
19+
20+
func ParallelWithMiddleware(middleware ...Middleware) handler.BuilderComposer {
21+
return WithMiddleware(handler.Parallel, middleware...)
22+
}
1923

2024
// MakeMiddleware generates the corresponding Middleware for HandlerMiddleware
2125
func MakeMiddleware(w HandlerMiddleware) Middleware {
2226
builderWrapper := MakeBuilderMiddleware(w)
23-
return func(f BuilderComposer) BuilderComposer {
27+
return func(f handler.BuilderComposer) handler.BuilderComposer {
2428
return func(builder ...handler.Builder) handler.Builder {
2529
wrapped := make([]handler.Builder, 0, len(builder))
2630
for _, b := range builder {
@@ -42,7 +46,7 @@ func MakeBuilderMiddleware(w HandlerMiddleware) BuilderMiddleware {
4246
}
4347

4448
// WithMiddleware returns a new BuilderComposer with all middleware applied
45-
func WithMiddleware(composer BuilderComposer, middleware ...Middleware) BuilderComposer {
49+
func WithMiddleware(composer handler.BuilderComposer, middleware ...Middleware) handler.BuilderComposer {
4650
out := composer
4751
for _, m := range middleware {
4852
out = m(out)

middleware/parallel.go

-40
This file was deleted.

0 commit comments

Comments
 (0)