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.