injection

package
v0.0.0-...-75da391 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Sep 5, 2019 License: Apache-2.0 Imports: 5 Imported by: 0

README

Knative Dependency Injection

This library supports the production of controller processes with minimal boilerplate outside of the reconciler implementation.

Building Controllers

To adopt this model of controller construction, implementations should start with the following controller constructor:

import (
	"context"

	"knative.dev/pkg/configmap"
	"knative.dev/pkg/controller"
	"knative.dev/pkg/logging"
)

func NewController(ctx context.Context, cmw configmap.Watcher) *controller.Impl {
	logger := logging.FromContext(ctx)

	// TODO(you): Access informers

	c := &Reconciler{
		// TODO(you): Pass listers, clients, and other stuff.
	}
	impl := controller.NewImpl(c, logger, "NameOfController")

	// TODO(you): Set up event handlers.

	return impl
}

Consuming Informers

Knative controllers use "informers" to set up the various event hooks needed to queue work, and pass the "listers" fed by the informers' caches to the nested "Reconciler" for accessing objects.

Our controller constructor is passed a context.Context onto which we inject any informers we access. The accessors for these informers are in little stub libraries, which we have hand rolled for Kubernetes (more on how to generate these below).

import (
	// These are how you access a client or informer off of the "ctx" passed
	// to set up the controller.
	"knative.dev/pkg/injection/clients/kubeclient"
	svcinformer "knative.dev/pkg/injection/informers/kubeinformers/corev1/service"

	// Other imports ...
)

func NewController(ctx context.Context, cmw configmap.Watcher) *controller.Impl {
	logger := logging.FromContext(ctx)

	// Access informers
	svcInformer := svcinformer.Get(ctx)

	c := &Reconciler{
		// Pass the lister and client to the Reconciler.
		Client:        kubeclient.Get(ctx),
		ServiceLister: svcInformer.Lister(),
	}
	impl := controller.NewImpl(c, logger, "NameOfController")

	// Set up event handlers.
	svcInformer.Informer().AddEventHandler(...)

	return impl
}

How it works: by importing the accessor for a client or informer we link it and trigger the init() method for its package to run at startup. Each of these libraries registers themselves similar to our init() and controller processes can leverage this to setup and inject all of the registered things onto a context to pass to your NewController().

Testing Controllers

Similar to injection.Default, we also have injection.Fake. While linking the normal accessors sets up the former, linking their fakes set up the latter.

import (
	"testing"

	// Link the fakes for any informers our controller accesses.
	_ "knative.dev/pkg/injection/informers/kubeinformers/corev1/service/fake"

	"k8s.io/client-go/rest"
	"knative.dev/pkg/injection"
	logtesting "knative.dev/pkg/logging/testing"
)

func TestFoo(t *testing.T) {
	ctx := logtesting.TestContextWithLogger(t)

	// Setup a context from all of the injected fakes.
	ctx, _ = injection.Fake.SetupInformers(ctx, &rest.Config{})
	cmw := configmap.NewStaticWatcher(...)
	ctrl := NewController(ctx, cmw)

	// Test the controller process.
}

The fake clients also support manually setting up contexts seeded with objects:

import (
	"testing"

	fakekubeclient "knative.dev/pkg/injection/clients/kubeclient/fake"

	"k8s.io/client-go/rest"
	"knative.dev/pkg/injection"
	logtesting "knative.dev/pkg/logging/testing"
)

func TestFoo(t *testing.T) {
	ctx := logtesting.TestContextWithLogger(t)

	objs := []runtime.Object{
		// Some list of initial objects in the client.
	}

	ctx, kubeClient := fakekubeclient.With(ctx, objs...)

	// The fake clients returned by our library are the actual fake type,
	// which enables us to access test-specific methods, e.g.
	kubeClient.AppendReactor(...)

	c := &Reconciler{
		Client: kubeClient,
	}

	// Test the reconciler...
}

Starting controllers

All we do is import the controller packages and pass their constructors along with a component name (single word) to our shared main. Then our shared main method sets it all up and runs our controllers.

package main

import (
	// The set of controllers this process will run.
	"github.com/knative/foo/pkg/reconciler/bar"
	"github.com/knative/baz/pkg/reconciler/blah"

	// This defines the shared main for injected controllers.
	"knative.dev/pkg/injection/sharedmain"
)

func main() {
	sharedmain.Main("componentname",
       bar.NewController,
       blah.NewController,
    )
}

Generating Injection Stubs.

To make generating stubs simple, we have harnessed the Kubernetes code-generation tooling to produce injection-gen. Similar to how you might ordinarily run the other foo-gen processed:

CODEGEN_PKG=${CODEGEN_PKG:-$(cd ${REPO_ROOT}; ls -d -1 ./vendor/k8s.io/code-generator 2>/dev/null || echo ../code-generator)}

${CODEGEN_PKG}/generate-groups.sh "deepcopy,client,informer,lister" \
  github.com/knative/sample-controller/pkg/client github.com/knative/sample-controller/pkg/apis \
  "samples:v1alpha1" \
  --go-header-file ${REPO_ROOT}/hack/boilerplate/boilerplate.go.txt

To run injection-gen you run the following (replacing the import path and api group):


KNATIVE_CODEGEN_PKG=${KNATIVE_CODEGEN_PKG:-$(cd ${REPO_ROOT}; ls -d -1 ./vendor/knative.dev/pkg 2>/dev/null || echo ../pkg)}

${KNATIVE_CODEGEN_PKG}/hack/generate-knative.sh "injection" \
  github.com/knative/sample-controller/pkg/client github.com/knative/sample-controller/pkg/apis \
  "samples:v1alpha1" \
  --go-header-file ${REPO_ROOT}/hack/boilerplate/boilerplate.go.txt

To ensure the appropriate tooling is vendored, add the following to Gopkg.toml:

required = [
  "knative.dev/pkg/codegen/cmd/injection-gen",
]

# .. Constraints

# Keeps things like the generate-knative.sh script
[[prune.project]]
  name = "knative.dev/pkg"
  unused-packages = false
  non-go = false

Documentation

Overview

Package injection defines the mechanisms through which clients, informers and shared informer factories are injected into a shared controller binary implementation.

There are two primary contexts where the usage of the injection package is interesting. The first is in the context of implementations of `controller.Reconciler` being wrapped in a `*controller.Impl`:

import (
  // Simply linking this triggers the injection of the informer, which links
  // the factory triggering its injection, and which links the client,
  // triggering its injection.  All you need to know is that it works :)
  deployinformer "knative.dev/pkg/injection/informers/kubeinformers/appsv1/deployment"
  "knative.dev/pkg/injection"
)

func NewController(ctx context.Context) *controller.Impl {
  deploymentInformer := deployinformer.Get(ctx)
  // Pass deploymentInformer.Lister() to Reconciler
  ...
  // Set up events on deploymentInformer.Informer()
  ...
}

Then in `package main` the entire controller process can be set up via:

package main

import (
	// The set of controllers this controller process runs.
   // Linking these will register their transitive dependencies, after
   // which the shared main can set up the rest.
	"github.com/knative/foo/pkg/reconciler/matt"
	"github.com/knative/foo/pkg/reconciler/scott"
	"github.com/knative/foo/pkg/reconciler/ville"
	"github.com/knative/foo/pkg/reconciler/dave"

	// This defines the shared main for injected controllers.
	"knative.dev/pkg/injection/sharedmain"
)

func main() {
	sharedmain.Main("mycomponent",
      // We pass in the list of controllers to construct, and that's it!
      // If we forget to add this, go will complain about the unused import.
      matt.NewController,
      scott.NewController,
      ville.NewController,
      dave.NewController,
   )
}

If you want to adapt the above to run the controller within a single namespace, you can instead do something like:

package main

import (
	// The set of controllers this controller process runs.
   // Linking these will register their transitive dependencies, after
   // which the shared main can set up the rest.
	"github.com/knative/foo/pkg/reconciler/matt"
	"github.com/knative/foo/pkg/reconciler/scott"
	"github.com/knative/foo/pkg/reconciler/ville"
	"github.com/knative/foo/pkg/reconciler/dave"

	// This defines the shared main for injected controllers.
	"knative.dev/pkg/injection/sharedmain"

   // These are used to set up the context.
	"knative.dev/pkg/injection"
	"knative.dev/pkg/signals"
)

func main() {
   // Scope the shared informer factories to the provided namespace.
   ctx := injection.WithNamespace(signals.NewContext(), "the-namespace")

   // Use our initial context when setting up the controllers.
	sharedmain.MainWithContext(ctx, "mycomponent",
      // We pass in the list of controllers to construct, and that's it!
      // If we forget to add this, go will complain about the unused import.
      matt.NewController,
      scott.NewController,
      ville.NewController,
      dave.NewController,
   )
}

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func GetNamespaceScope

func GetNamespaceScope(ctx context.Context) string

GetNamespaceScope accesses the namespace associated with the provided context. This should be called when the injection logic is setting up shared informer factories.

func HasNamespaceScope

func HasNamespaceScope(ctx context.Context) bool

HasNamespaceScope determines whether the provided context has been scoped to a particular namespace.

func WithNamespaceScope

func WithNamespaceScope(ctx context.Context, namespace string) context.Context

WithNamespaceScope associates a namespace scoping with the provided context, which will scope the informers produced by the downstream informer factories.

Types

type ClientInjector

type ClientInjector func(context.Context, *rest.Config) context.Context

ClientInjector holds the type of a callback that attaches a particular client type to a context.

type ControllerConstructor

type ControllerConstructor func(context.Context, configmap.Watcher) *controller.Impl

type InformerFactoryInjector

type InformerFactoryInjector func(context.Context) context.Context

InformerFactoryInjector holds the type of a callback that attaches a particular factory type to a context.

type InformerInjector

type InformerInjector func(context.Context) (context.Context, controller.Informer)

InformerInjector holds the type of a callback that attaches a particular informer type to a context.

type Interface

type Interface interface {
	// RegisterClient registers a new injector callback for associating
	// a new client with a context.
	RegisterClient(ClientInjector)

	// GetClients fetches all of the registered client injectors.
	GetClients() []ClientInjector

	// RegisterInformerFactory registers a new injector callback for associating
	// a new informer factory with a context.
	RegisterInformerFactory(InformerFactoryInjector)

	// GetInformerFactories fetches all of the registered informer factory injectors.
	GetInformerFactories() []InformerFactoryInjector

	// RegisterInformer registers a new injector callback for associating
	// a new informer with a context.
	RegisterInformer(InformerInjector)

	// GetInformers fetches all of the registered informer injectors.
	GetInformers() []InformerInjector

	// SetupInformers runs all of the injectors against a context, starting with
	// the clients and the given rest.Config.  The resulting context is returned
	// along with a list of the .Informer() for each of the injected informers,
	// which is suitable for passing to controller.StartInformers().
	// This does not setup or start any controllers.
	SetupInformers(context.Context, *rest.Config) (context.Context, []controller.Informer)
}

Interface is the interface for interacting with injection implementations, such as our Default and Fake below.

var (

	// Default is the injection interface with which informers should register
	// to make themselves available to the controller process when reconcilers
	// are being run for real.
	Default Interface = &impl{}

	// Fake is the injection interface with which informers should register
	// to make themselves available to the controller process when it is being
	// unit tested.
	Fake Interface = &impl{}
)

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL