knative.dev/pkg@v0.0.0-20260602142205-ac97e43f6622/injection/README.md (about) 1 # Knative Dependency Injection 2 3 This library supports the production of controller processes with minimal 4 boilerplate outside of the reconciler implementation. 5 6 ## Building Controllers 7 8 To adopt this model of controller construction, implementations should start 9 with the following controller constructor: 10 11 ```go 12 import ( 13 "context" 14 15 "knative.dev/pkg/configmap" 16 "knative.dev/pkg/controller" 17 "knative.dev/pkg/logging" 18 kindreconciler "knative.dev/<repo>/pkg/client/injection/reconciler/<clientgroup>/<version>/<resource>" 19 ) 20 21 func NewController(ctx context.Context, cmw configmap.Watcher) *controller.Impl { 22 logger := logging.FromContext(ctx) 23 24 // TODO(you): Access informers 25 26 r := &Reconciler{ 27 // TODO(you): Pass listers, clients, and other stuff. 28 } 29 impl := kindreconciler.NewImpl(ctx, r) 30 31 // TODO(you): Set up event handlers. 32 33 return impl 34 } 35 ``` 36 37 ### Generated Reconcilers 38 39 A code generator is available for simple subset of reconciliation requirements. 40 A label above the API type will signal to the injection code generator to 41 generate a strongly typed reconciler. Use `+genreconciler` to generate the 42 reconcilers. 43 44 ```go 45 // +genclient 46 // +genreconciler 47 // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object 48 49 type ExampleType struct { 50 ... 51 } 52 ``` 53 54 `+genreconciler` will produce a helper method to get a controller impl. 55 56 Update `NewController` as follows: 57 58 ```go 59 "knative.dev/pkg/controller" 60 ... 61 impl := controller.NewContext(ctx, c, controller.ControllerOptions{ 62 Logger: logger, 63 WorkQueueName: "NameOfController", 64 }) 65 ``` 66 67 becomes 68 69 ```go 70 kindreconciler "knative.dev/<repo>/pkg/client/injection/reconciler/<clientgroup>/<version>/<resource>" 71 ... 72 impl := kindreconciler.NewImpl(ctx, c) 73 ``` 74 75 See 76 [Generated Reconciler Responsibilities](#generated-reconciler-responsibilities) 77 for more information. 78 79 ## Implementing Reconcilers 80 81 Type `Reconciler` is expected to implement `Reconcile`: 82 83 ```go 84 func (r *Reconciler) Reconcile(ctx context.Context, key string) error { 85 ... 86 } 87 ``` 88 89 ### Generated Reconcilers 90 91 If generated reconcilers are used, Type `Reconciler` is expected to implement 92 `ReconcileKind`: 93 94 ```go 95 func (r *Reconciler) ReconcileKind(ctx context.Context, o *samplesv1alpha1.AddressableService) reconciler.Event { 96 ... 97 } 98 ``` 99 100 And if finalizers are required, 101 102 ```go 103 func (r *Reconciler) FinalizeKind(ctx context.Context, o *samplesv1alpha1.AddressableService) reconciler.Event { 104 ... 105 } 106 ``` 107 108 See 109 [Generated Reconciler Responsibilities](#generated-reconciler-responsibilities) 110 for more information. 111 112 ## Consuming Informers 113 114 Knative controllers use "informers" to set up the various event hooks needed to 115 queue work, and pass the "listers" fed by the informers' caches to the nested 116 "Reconciler" for accessing objects. 117 118 Our controller constructor is passed a `context.Context` onto which we inject 119 any informers we access. The accessors for these informers are in little stub 120 libraries, which we have hand rolled for Kubernetes (more on how to generate 121 these below). 122 123 ```go 124 import ( 125 // These are how you access a client or informer off of the "ctx" passed 126 // to set up the controller. 127 "knative.dev/pkg/client/injection/kube/client" 128 svcinformer "knative.dev/pkg/client/injection/kube/informers/core/v1/service" 129 130 // Other imports ... 131 ) 132 133 func NewController(ctx context.Context, cmw configmap.Watcher) *controller.Impl { 134 logger := logging.FromContext(ctx) 135 136 // Access informers 137 svcInformer := svcinformer.Get(ctx) 138 139 c := &Reconciler{ 140 // Pass the lister and client to the Reconciler. 141 Client: kubeclient.Get(ctx), 142 ServiceLister: svcInformer.Lister(), 143 } 144 logger = logger.Named("NameOfController") 145 impl := controller.NewContext(ctx, c, controller.ControllerOptions{ 146 Logger: logger, 147 WorkQueueName: "NameOfController", 148 }) 149 150 // Set up event handlers. 151 svcInformer.Informer().AddEventHandler(...) 152 153 return impl 154 } 155 156 ``` 157 158 > How it works: by importing the accessor for a client or informer we link it 159 > and trigger the `init()` method for its package to run at startup. Each of 160 > these libraries registers themselves similar to our `init()` and controller 161 > processes can leverage this to setup and inject all of the registered things 162 > onto a context to pass to your `NewController()`. 163 164 ## Testing Controllers 165 166 Similar to `injection.Default`, we also have `injection.Fake`. While linking the 167 normal accessors sets up the former, linking their fakes set up the latter. 168 169 ```go 170 import ( 171 "testing" 172 173 // Link the fakes for any informers our controller accesses. 174 _ "knative.dev/pkg/client/injection/kube/informers/core/v1/service/fake" 175 176 "k8s.io/client-go/rest" 177 "knative.dev/pkg/injection" 178 logtesting "knative.dev/pkg/logging/testing" 179 ) 180 181 func TestFoo(t *testing.T) { 182 ctx := logtesting.TestContextWithLogger(t) 183 184 // Setup a context from all of the injected fakes. 185 ctx, _ = injection.Fake.SetupInformers(ctx, &rest.Config{}) 186 cmw := configmap.NewStaticWatcher(...) 187 ctrl := NewController(ctx, cmw) 188 189 // Test the controller process. 190 } 191 ``` 192 193 The fake clients also support manually setting up contexts seeded with objects: 194 195 ```go 196 import ( 197 "testing" 198 199 fakekubeclient "knative.dev/pkg/client/injection/kube/client/fake" 200 201 "k8s.io/client-go/rest" 202 "knative.dev/pkg/injection" 203 logtesting "knative.dev/pkg/logging/testing" 204 ) 205 206 func TestFoo(t *testing.T) { 207 ctx := logtesting.TestContextWithLogger(t) 208 209 objs := []runtime.Object{ 210 // Some list of initial objects in the client. 211 } 212 213 ctx, kubeClient := fakekubeclient.With(ctx, objs...) 214 215 // The fake clients returned by our library are the actual fake type, 216 // which enables us to access test-specific methods, e.g. 217 kubeClient.AppendReactor(...) 218 219 c := &Reconciler{ 220 Client: kubeClient, 221 } 222 223 // Test the reconciler... 224 } 225 ``` 226 227 ## Starting controllers 228 229 All we do is import the controller packages and pass their constructors along 230 with a component name (single word) to our shared main. Then our shared main 231 method sets it all up and runs our controllers. 232 233 ```go 234 package main 235 236 import ( 237 // The set of controllers this process will run. 238 "github.com/knative/foo/pkg/reconciler/bar" 239 "github.com/knative/baz/pkg/reconciler/blah" 240 241 // This defines the shared main for injected controllers. 242 "knative.dev/pkg/injection/sharedmain" 243 ) 244 245 func main() { 246 sharedmain.Main("componentname", 247 bar.NewController, 248 blah.NewController, 249 ) 250 } 251 252 ``` 253 254 ## Generating Injection Stubs. 255 256 To make generating stubs simple, we have harnessed the Kubernetes 257 code-generation tooling to produce `injection-gen`. Similar to how you might 258 ordinarily run the other `foo-gen` processed: 259 260 To run `injection-gen` you run the following (replacing the import path and api 261 group): 262 263 ```shell 264 265 KNATIVE_CODEGEN_PKG=${KNATIVE_CODEGEN_PKG:-$(cd ${REPO_ROOT}; ls -d -1 ./vendor/knative.dev/pkg 2>/dev/null || echo ../pkg)} 266 267 ${KNATIVE_CODEGEN_PKG}/hack/generate-knative.sh "injection" \ 268 github.com/knative/sample-controller/pkg/client github.com/knative/sample-controller/pkg/apis \ 269 "samples:v1alpha1" \ 270 --go-header-file ${REPO_ROOT}/hack/boilerplate/boilerplate.go.txt 271 272 ``` 273 274 To ensure the appropriate tooling is vendored, add the following to 275 `Gopkg.toml`: 276 277 ```toml 278 required = [ 279 "knative.dev/pkg/codegen/cmd/injection-gen", 280 ] 281 282 # .. Constraints 283 284 # Keeps things like the generate-knative.sh script 285 [[prune.project]] 286 name = "knative.dev/pkg" 287 unused-packages = false 288 non-go = false 289 ``` 290 291 ## Generated Reconciler Responsibilities 292 293 The goal of generating the reconcilers is to provide the controller implementer 294 a strongly typed interface, and ensure correct reconciler behaviour around 295 status updates, Kubernetes event creation, and queue management. 296 297 We have already helped the queue management with libraries in this repo. But 298 there was a gap in support and standards around how status updates (and retries) 299 are performed, and when Kubernetes events are created for the resource. 300 301 The general flow with generated reconcilers looks like the following: 302 303 ``` 304 [k8s] -> [watches] -> [reconciler enqeueue] -> [Reconcile(key)] -> [ReconcileKind(resource)] 305 ^-- you set up. ^-- generated ^-- stubbed and you customize 306 ``` 307 308 Optionally, support for finalizers: 309 310 ``` 311 [Reconcile(key)] -> <resource deleted?> - no -> [ReconcileKind(resource)] 312 ` 313 (optional) 314 `- yes -> [FinalizeKind(resource)] 315 ``` 316 317 - `ReconcileKind` is only called if the resource's deletion timestamp is empty. 318 - `FinalizeKind` is optional, and if implemented by the reconciler will be 319 called when the resource's deletion timestamp is set. 320 321 The responsibility and consequences of using the generated 322 `ReconcileKind(resource)` method are as follows: 323 324 - In `NewController`, set up watches and reconciler enqueue requests as before. 325 - Implementing `ReconcileKind(ctx, resource)` to handle active resources. 326 - Implementing `FinalizeKind(ctx, resource)` to finalize deleting active 327 resources. 328 - NOTE: Implementing `FinalizeKind` will result in the reconciler using 329 finalizers on the resource. 330 - Resulting changes from `Reconcile` calling `ReconcileKind(ctx, resource)`: 331 - DO NOT edit the spec of `resource`, it will be ignored. 332 - DO NOT edit the metadata of `resource`, it will be ignored. 333 - If `resource.status` is changed, `Reconcile` will synchronize it back to the 334 API Server. 335 - Note: the watches setup for `resource.Kind` will see the update to status 336 and cause another reconciliation. 337 - `ReconcileKind(ctx, resource)` returns a 338 [`reconciler.Event`](../reconciler/events.go) results in: 339 - If `event` is an `error` (`reconciler.Event` extends `error` internally), 340 `Reconciler` will produce a `Warning` kubernetes event with _reason_ 341 `InternalError` and the body of the error as the message. 342 - Additionally, the `error` will be returned from `Reconciler` and `key` will 343 requeue back into the reconciler key queue. 344 - If `event` is a `reconciler.Event`, `Reconciler` will log a typed and reasoned 345 Kubernetes Event based on the contents of `event`. 346 - `event` is not considered an error for requeue and nil is returned from 347 `Reconciler`. 348 - If additional events are required to be produced, an implementation can pull a 349 recorder from the context: `recorder := controller.GetEventRecorder(ctx)`. 350 351 Future features to be considered: 352 353 - Document how we leverage `configStore` and specifically 354 `ctx = r.configStore.ToContext(ctx)` inside `Reconcile`. 355 - Adjust `+genreconciler` to allow for generated reconcilers to be made without 356 annotating the type struct. 357 - Add class-based annotation filtering. 358 359 ### ConfigStore 360 361 Config store is used to decorate the context with a snapshot of configmaps to be 362 used in a reconciler method. 363 364 To add this feature to the generated reconciler, it will have to be passed in on 365 `reconciler<kind>.NewImpl` like so: 366 367 ```go 368 kindreconciler "knative.dev/<repo>/pkg/client/injection/reconciler/<clientgroup>/<version>/<resource>" 369 ... 370 impl := kindreconciler.NewImpl(ctx, c, func(impl *controller.Impl) controller.Options { 371 // Setup options that require access to a controller.Impl. 372 configsToResync := []interface{}{ 373 &some.Config{}, 374 } 375 resyncOnConfigChange := configmap.TypeFilter(configsToResync...)(func(string, interface{}) { 376 impl.FilteredGlobalResync(myFilterFunc, kindInformer.Informer()) 377 }) 378 configStore := config.NewStore(c.Logger.Named("config-store"), resyncOnConfigChange) 379 configStore.WatchConfigs(cmw) 380 381 // Return the controller options. 382 return controller.Options{ 383 ConfigStore: configStore, 384 } 385 }) 386 ``` 387 388 ### Filtering on controller promotion 389 390 The generated controllers implement the 391 [LeaderAwareFuncs](https://github.com/knative/pkg/blob/main/reconciler/leader.go#L66-L72) 392 interface to support HA controller deployments. When a generated controller is 393 promoted, by default it will trigger [a re-reconcile of every resource it 394 manages](https://github.com/knative/pkg/blob/main/client/injection/apiextensions/reconciler/apiextensions/v1/customresourcedefinition/controller.go#L68-L81). 395 To filter which objects get reconciled, pass a `PromoteFilterFunc` to the 396 controller's constructor: 397 398 ```go 399 kindreconciler "knative.dev/<repo>/pkg/client/injection/reconciler/<clientgroup>/<version>/<resource>" 400 pkgreconciler "knative.dev/pkg/reconciler" 401 ... 402 impl := kindreconciler.NewImpl(ctx, c, func(impl *controller.Impl) controller.Options { 403 return controller.Options{ 404 PromoteFilterFunc: pkgreconciler.LabelFilterFunc("mylabel", "myvalue", false), 405 } 406 }) 407 ``` 408 409 410 ### Artifacts 411 412 The artifacts are targeted to the configured `client/injection` directory: 413 414 ```go 415 kindreconciler "knative.dev/<repo>/pkg/client/injection/reconciler/<clientgroup>/<version>/<kind>" 416 ``` 417 418 Controller related artifacts: 419 420 - `NewImpl` - gets an injection based client and lister for `<kind>`, sets up 421 Kubernetes Event recorders, and delegates to `controller.NewContext` for queue 422 management. 423 424 ```go 425 impl := reconciler.NewImpl(ctx, reconcilerInstance) 426 ``` 427 428 Reconciler related artifacts: 429 430 - `Interface` - defines the strongly typed interfaces to be implemented by a 431 controller reconciling `<kind>`. 432 433 ```go 434 // Check that our Reconciler implements Interface 435 var _ addressableservicereconciler.Interface = (*Reconciler)(nil) 436 ``` 437 438 - `Finalizer` - defines the strongly typed interfaces to be implemented by a 439 controller finalizing `<kind>`. 440 441 ```go 442 // Check that our Reconciler implements Interface 443 var _ addressableservicereconciler.Finalizer = (*Reconciler)(nil) 444 ``` 445 446 #### Annotation based class filters 447 448 Sometimes a reconciler only wants to reconcile a class of resource identified by 449 a special annotation on the Custom Resource. 450 451 This behavior can be enabled in the generators by adding the annotation class 452 key to the type struct: 453 454 ```go 455 // +genreconciler:class=example.com/filter.class 456 ``` 457 458 The `genreconciler` generator code will now have the addition of 459 `classValue string` to `NewImpl` and `NewReconciler` (for tests): 460 461 ```go 462 NewImpl(ctx context.Context, r Interface, classValue string, optionsFns ...controller.OptionsFn) *controller.Impl 463 ``` 464 465 ```go 466 NewReconciler(ctx context.Context, logger *zap.SugaredLogger, client versioned.Interface, lister pubv1alpha1.BarLister, recorder record.EventRecorder, r Interface, classValue string, options ...controller.Options) controller.Reconciler 467 ``` 468 469 `ReconcileKind` and `FinalizeKind` will NOT be called for resources that DO NOT 470 have the provided `+genreconciler:class=<key>` key annotation. Additionally the 471 value of the `<key>` annotation on a resource must match the value provided to 472 `NewImpl` (or `NewReconcile`) for `ReconcileKind` or `FinalizeKind` to be called 473 for that resource. 474 475 #### Annotation based common logic 476 477 **krshapedlogic=false may be used to omit common reconciler logic** 478 479 Reconcilers can handle common logic for resources that conform to the KRShaped 480 interface. This allows the generated code to automatically increment 481 ObservedGeneration. 482 483 ```go 484 // +genreconciler 485 ``` 486 487 Setting this annotation will emit the following in the generated reconciler. 488 489 ```go 490 reconciler.PreProcessReconcile(ctx, resource) 491 492 reconcileEvent = r.reconciler.ReconcileKind(ctx, resource) 493 494 reconciler.PostProcessReconcile(ctx, resource, oldResource) 495 ``` 496 497 #### Stubs 498 499 To enable stubs generation, add the stubs flag: 500 501 ```go 502 // +genreconciler:stubs 503 ``` 504 505 Or with the class annotation: 506 507 ```go 508 // +genreconciler:class=example.com/filter.class,stubs 509 ``` 510 511 The stubs are intended to be used to get started, or to use as reference. It is 512 intended to be copied out of the `client` dir. 513 514 `knative.dev/<repo>/pkg/client/injection/reconciler/<clientgroup>/<version>/<kind>/stubs/controller.go` 515 516 - A basic implementation of `NewController`. 517 518 `knative.dev/<repo>/pkg/client/injection/reconciler/<clientgroup>/<version>/<kind>/stubs/reconciler.go` 519 520 - A basic implementation of `type Reconciler struct {}` and 521 `Reconciler.ReconcileKind`. 522 - A commented out example of a basic implementation of 523 `Reconciler.FinalizeKind`. 524 - An example `reconciler.Event`: `newReconciledNormal` 525 526 ### Examples 527 528 Please look at 529 [`sample-controller`](http://github.com/knative/sample-controller) or 530 [`sample-source`](http://github.com/knative/sample-source) for working 531 integrations of the generated geconciler code.