README

"Pod Spec"-able Bindings

The psbinding package provides facilities to make authoring Bindings whose subjects adhere to duckv1.PodSpecable easier. The Bindings doc mentions two key elements of the controller architecture:

  1. The standard controller,
  2. The mutating webhook (or "admission controller")

This package provides facilities for bootstrapping both of these elements. To leverage the psbinding package, folks should adjust their Binding types to implement psbinding.Bindable, which contains a variety of methods that will look familiar to Knative controller authors with two new key methods: Do and Undo (aka the "mutation" methods).

The mutation methods on the Binding take in (context.Context, *duckv1.WithPod), and are expected to alter the *duckv1.WithPod appropriately to achieve the semantics of the Binding. So for example, if the Binding's runtime contract is the inclusion of a new environment variable FOO with some value extracted from the Binding's spec then in Do() the duckv1.WithPod would be altered so that each of the containers: contains:

env:
  - name: "FOO"
    value: "<from Binding spec>"

... and Undo() would remove these variables. Do is invoked for active Bindings, and Undo is invoked when they are being deleted, but their subjects remain.

We will walk through a simple example Binding whose runtime contract is to mount secrets for talking to Github under /var/bindings/github. See also on which this is based.

Do and Undo

The Undo method itself is simply: remove the named secret volume and any mounts of it:

func (fb *GithubBinding) Undo(ctx context.Context, ps *duckv1.WithPod) {
	spec := ps.Spec.Template.Spec

	// Make sure the PodSpec does NOT have the github volume.
	for i, v := range spec.Volumes {
		if v.Name == github.VolumeName {
			ps.Spec.Template.Spec.Volumes = append(spec.Volumes[:i], spec.Volumes[i+1:]...)
			break
		}
	}

	// Make sure that none of the [init]containers have the github volume mount
	for i, c := range spec.InitContainers {
		for j, vm := range c.VolumeMounts {
			if vm.Name == github.VolumeName {
				spec.InitContainers[i].VolumeMounts = append(vm[:j], vm[j+1:]...)
				break
			}
		}
	}
	for i, c := range spec.Containers {
		for j, vm := range c.VolumeMounts {
			if vm.Name == github.VolumeName {
				spec.Containers[i].VolumeMounts = append(vm[:j], vm[j+1:]...)
				break
			}
		}
	}
}

The Do method is the dual of this: ensure that the volume exists, and all containers have it mounted.

func (fb *GithubBinding) Do(ctx context.Context, ps *duckv1.WithPod) {

	// First undo so that we can just unconditionally append below.
	fb.Undo(ctx, ps)

	// Make sure the PodSpec has a Volume like this:
	volume := corev1.Volume{
		Name: github.VolumeName,
		VolumeSource: corev1.VolumeSource{
			Secret: &corev1.SecretVolumeSource{
				SecretName: fb.Spec.Secret.Name,
			},
		},
	}
	ps.Spec.Template.Spec.Volumes = append(ps.Spec.Template.Spec.Volumes, volume)

	// Make sure that each [init]container in the PodSpec has a VolumeMount like this:
	volumeMount := corev1.VolumeMount{
		Name:      github.VolumeName,
		ReadOnly:  true,
		MountPath: github.MountPath,
	}
	spec := ps.Spec.Template.Spec
	for i := range spec.InitContainers {
		spec.InitContainers[i].VolumeMounts = append(spec.InitContainers[i].VolumeMounts, volumeMount)
	}
	for i := range spec.Containers {
		spec.Containers[i].VolumeMounts = append(spec.Containers[i].VolumeMounts, volumeMount)
	}
}

Note: if additional context is needed to perform the mutation, then it may be attached-to / extracted-from the supplied context.Context.

The standard controller

For simple Bindings (such as our GithubBinding), we should be able to implement our *controller.Impl by directly leveraging *psbinding.BaseReconciler to fully implement reconciliation.

// NewController returns a new GithubBinding reconciler.
func NewController(
	ctx context.Context,
	cmw configmap.Watcher,
) *controller.Impl {
	logger := logging.FromContext(ctx)

	ghInformer := ghinformer.Get(ctx)
	dc := dynamicclient.Get(ctx)
	psInformerFactory := podspecable.Get(ctx)

	c := &psbinding.BaseReconciler{
		GVR: v1alpha1.SchemeGroupVersion.WithResource("githubbindings"),
		Get: func(namespace string, name string) (psbinding.Bindable, error) {
			return ghInformer.Lister().GithubBindings(namespace).Get(name)
		},
		DynamicClient: dc,
		Recorder: record.NewBroadcaster().NewRecorder(
			scheme.Scheme, corev1.EventSource{Component: controllerAgentName}),
	}
	logger = logger.Named("GithubBindings")
	impl := controller.NewImpl(c, logger, "GithubBindings")

	logger.Info("Setting up event handlers")

	ghInformer.Informer().AddEventHandler(controller.HandleAll(impl.Enqueue))

	c.Tracker = tracker.New(impl.EnqueueKey, controller.GetTrackerLease(ctx))
	c.Factory = &duck.CachedInformerFactory{
		Delegate: &duck.EnqueueInformerFactory{
			Delegate:     psInformerFactory,
			EventHandler: controller.HandleAll(c.Tracker.OnChanged),
		},
	}

	// If our `Do` / `Undo` methods need additional context, then we can
	// setup a callback to infuse the `context.Context` here:
	//    c.WithContext = ...
	// Note that this can also set up additional informer watch events to
	// trigger reconciliation when the infused context changes.

	return impl
}

Note: if customized reconciliation logic is needed (e.g. synthesizing additional resources), then the psbinding.BaseReconciler may be embedded and a custom Reconcile() defined, which can still take advantage of the shared Finalizer handling, Status manipulation or Subject-reconciliation.

The mutating webhook

Setting up the mutating webhook is even simpler:

func NewWebhook(ctx context.Context, cmw configmap.Watcher) *controller.Impl {
		return psbinding.NewAdmissionController(ctx,
			// Name of the resource webhook.
			"githubbindings.webhook.bindings.mattmoor.dev",

			// The path on which to serve the webhook.
			"/githubbindings",

			// How to get all the Bindables for configuring the mutating webhook.
			ListAll,

			// How to setup the context prior to invoking Do/Undo.
			func(ctx context.Context, b psbinding.Bindable) (context.Context, error) {
				return ctx, nil
			},
		)
	}
}

// ListAll enumerates all of the GithubBindings as Bindables so that the webhook
// can reprogram itself as-needed.
func ListAll(ctx context.Context, handler cache.ResourceEventHandler) psbinding.ListAll {
	ghInformer := ghinformer.Get(ctx)

	// Whenever a GithubBinding changes our webhook programming might change.
	ghInformer.Informer().AddEventHandler(handler)

	return func() ([]psbinding.Bindable, error) {
		l, err := ghInformer.Lister().List(labels.Everything())
		if err != nil {
			return nil, err
		}
		bl := make([]psbinding.Bindable, 0, len(l))
		for _, elt := range l {
			bl = append(bl, elt)
		}
		return bl, nil
	}
}
Putting it together

With the above defined, then in our webhook's main.go we invoke sharedmain.MainWithContext passing the additional controller constructors:

	sharedmain.MainWithContext(ctx, "webhook",
		// Our other controllers.
		// ...

		// For each binding we have our controller and binding webhook.
		githubbinding.NewController, githubbinding.NewWebhook,
	)
Subresource reconciler

Sometimes we might find the need for controlling not only psbinding.Bindable and duckv1.WithPod, but also other resources. We can achieve this by implementing psbinding.SubResourcesReconcilerInterface and injecting it in the psbinding.BaseReconciler.

For example we can implement a SubResourcesReconciler to create/delete k8s resources:

type FooBindingSubResourcesReconciler struct {
    Client kubernetes.Interface
}

func (fr *FooBindingSubresourcesReconciler) Reconcile(ctx context.Context, fb psbinding.Bindable) error {
    // Logic to create k8s resources here
    return err
}

func (fr *FooBindingSubresourcesReconciler) ReconcileDeletion(ctx context.Context, fb psbinding.Bindable) error {
    // Logic to delete k8s resources related to our Bindable
    return err
}

The SubResourcesReconciler can be then injected in the psbinding.BaseReconciler as follows:

kclient := kubeclient.Get(ctx)
srr := FooBindingSubResourcesReconciler{
    Client: kclient,
}
c := &psbinding.BaseReconciler{
		...
        SubresourcesReconciler: srr
	}
Expand ▾ Collapse ▴

Documentation

Overview

    Package psbinding provides facilities to make authoring Bindings that work with "Pod Spec"-able subjects easier. There are two key components

    1. The AdmissionController, which lives in psbinding.go (controller.go)
      sets it up.
    2. The BaseReconciler, which lives in reconciler.go and can either be
      used directly as a Reconciler, or it can be wrapped as a base
      implementation for a customized reconciler that wants to take advantage
      of its facilities (e.g. for updating status and manipulating finalizers).
    

    The core concept to consuming psbinding is the Bindable interface. By implementing Bindable on your binding resource, you enable the BaseReconciler to take over a significant amount of the boilerplate reconciliation (maybe all of it). A lot of the interface methods will seem pretty standard to Knative folks, but the two key methods to call our are Do and Undo. These "mutation" methods carry the business logic for the "Pod Spec"-able binding.

    The mutation methods have the signature:

    func(context.Context, *v1alpha1.WithPod)
    

    These methods are called to have the Binding perform its mutation on the supplied WithPod (our "Pod Spec"-able wrapper type). However, in some cases the binding may need additional context. Similar to apis.Validatable and apis.Defaultable these mutations take a context.Context, and similar to our "resourcesemantics" webhook, the "psbinding" package provides a hook to allow consumers to infuse this context.Context with additional... context. The signature of these hooks is BindableContext, and they may be supplied to both the AdmissionController and the BaseReconciler.

    Index

    Constants

    This section is empty.

    Variables

    View Source
    var (
    	ExclusionSelector = metav1.LabelSelector{
    		MatchExpressions: []metav1.LabelSelectorRequirement{{
    			Key:      duck.BindingExcludeLabel,
    			Operator: metav1.LabelSelectorOpNotIn,
    			Values:   []string{"true"},
    		}},
    	}
    	InclusionSelector = metav1.LabelSelector{
    		MatchExpressions: []metav1.LabelSelectorRequirement{{
    			Key:      duck.BindingIncludeLabel,
    			Operator: metav1.LabelSelectorOpIn,
    			Values:   []string{"true"},
    		}},
    	}
    )

      We need to specifically exclude our deployment(s) from consideration, but this provides a way of excluding other things as well.

      Functions

      func NewAdmissionController

      func NewAdmissionController(
      	ctx context.Context,
      	name, path string,
      	gla GetListAll,
      	withContext BindableContext,
      	reconcilerOptions ...ReconcilerOption,
      ) *controller.Impl

        NewAdmissionController constructs the webhook portion of the pair of reconcilers that implement the semantics of our Binding.

        Types

        type BaseReconciler

        type BaseReconciler struct {
        	pkgreconciler.LeaderAwareFuncs
        
        	// The GVR of the "primary key" resource for this reconciler.
        	// This is used along with the DynamicClient for updating the status
        	// and managing finalizers of the resources being reconciled.
        	GVR schema.GroupVersionResource
        
        	// Get is a callback that fetches the Bindable with the provided name
        	// and namespace (for this GVR).
        	Get func(namespace string, name string) (Bindable, error)
        
        	// WithContext is a callback that infuses the context supplied to
        	// Do/Undo with additional context to enable them to complete their
        	// respective tasks.
        	WithContext BindableContext
        
        	// DynamicClient is used to patch subjects and apply mutations to
        	// Bindable resources (determined by GVR) to reflect status updates.
        	DynamicClient dynamic.Interface
        
        	// Factory is used for producing listers for the object references we
        	// encounter.
        	Factory duck.InformerFactory
        
        	// The tracker builds an index of what resources are watching other
        	// resources so that we can immediately react to changes to changes in
        	// tracked resources.
        	Tracker tracker.Interface
        
        	// Recorder is an event recorder for recording Event resources to the
        	// Kubernetes API.
        	Recorder record.EventRecorder
        
        	// Namespace Lister
        	NamespaceLister corev1listers.NamespaceLister
        
        	// Sub-resources reconciler. Used to reconcile Binding related resources
        	SubResourcesReconciler SubResourcesReconcilerInterface
        }

          BaseReconciler helps implement controller.Reconciler for Binding resources.

          func (*BaseReconciler) EnsureFinalizer

          func (r *BaseReconciler) EnsureFinalizer(ctx context.Context, fb kmeta.Accessor) error

            EnsureFinalizer makes sure that the provided resource has a finalizer in the form of this BaseReconciler's GVR's stringified GroupResource.

            func (*BaseReconciler) IsFinalizing

            func (r *BaseReconciler) IsFinalizing(ctx context.Context, fb kmeta.Accessor) bool

              IsFinalizing determines whether it is our reconciler's turn to finalize a resource in the process of being deleted. This means that our finalizer is at the head of the metadata.finalizers list.

              func (*BaseReconciler) Reconcile

              func (r *BaseReconciler) Reconcile(ctx context.Context, key string) error

                Reconcile implements controller.Reconciler

                func (*BaseReconciler) ReconcileDeletion

                func (r *BaseReconciler) ReconcileDeletion(ctx context.Context, fb Bindable) error

                  ReconcileDeletion handles reconcile a resource that is being deleted, which amounts to properly finalizing the resource.

                  func (*BaseReconciler) ReconcileSubject

                  func (r *BaseReconciler) ReconcileSubject(ctx context.Context, fb Bindable, mutation Mutation) error

                    ReconcileSubject handles applying the provided Binding "mutation" (Do or Undo) to the Binding's subject(s).

                    func (*BaseReconciler) RemoveFinalizer

                    func (r *BaseReconciler) RemoveFinalizer(ctx context.Context, fb kmeta.Accessor) error

                      RemoveFinalizer is the dual of EnsureFinalizer, it removes our finalizer from the Binding resource

                      func (*BaseReconciler) UpdateStatus

                      func (r *BaseReconciler) UpdateStatus(ctx context.Context, desired Bindable) error

                        UpdateStatus updates the status of the resource. Caller is responsible for checking for semantic differences before calling.

                        type Bindable

                        type Bindable interface {
                        	duck.Bindable
                        
                        	// Do performs this binding's mutation with the specified context on the
                        	// provided PodSpecable.  The provided context may be decorated by
                        	// passing a BindableContext to both NewAdmissionController and
                        	// BaseReconciler.
                        	Do(context.Context, *duckv1.WithPod)
                        
                        	// Undo is the dual of Do, it undoes the binding.
                        	Undo(context.Context, *duckv1.WithPod)
                        }

                          Bindable is implemented by Binding resources whose subjects are PodSpecable and that want to leverage this shared logic to simplify binding authorship.

                          type BindableContext

                          type BindableContext func(context.Context, Bindable) (context.Context, error)

                            BindableContext is the type of context decorator methods that may be supplied to NewAdmissionController and BaseReconciler.

                            type GetListAll

                              GetListAll is a factory method for the ListAll method, which may also be supplied with a ResourceEventHandler to register a callback with the Informer that sits behind the returned ListAll so that the handler can queue work whenever the result of ListAll changes.

                              type ListAll

                              type ListAll func() ([]Bindable, error)

                                ListAll is the type of methods for enumerating all of the Bindables on the cluster in order to index the covered types to program the admission webhook.

                                type Mutation

                                type Mutation func(context.Context, *duckv1.WithPod)

                                  Mutation is the type of the Do/Undo methods.

                                  type Reconciler

                                  type Reconciler struct {
                                  	pkgreconciler.LeaderAwareFuncs
                                  
                                  	Name        string
                                  	HandlerPath string
                                  	SecretName  string
                                  
                                  	Client       kubernetes.Interface
                                  	MWHLister    admissionlisters.MutatingWebhookConfigurationLister
                                  	SecretLister corelisters.SecretLister
                                  	ListAll      ListAll
                                  
                                  	// WithContext is a callback that infuses the context supplied to
                                  	// Do/Undo with additional context to enable them to complete their
                                  	// respective tasks.
                                  	WithContext BindableContext
                                  	// contains filtered or unexported fields
                                  }

                                    Reconciler implements an AdmissionController for altering PodSpecable resources that are the subject of a particular type of Binding. The two key methods are:

                                    1. reconcileMutatingWebhook: which enumerates all of the Bindings and
                                       compiles a list of resource types that should be intercepted by our
                                       webhook.  It also builds an index that can be used to efficiently
                                       handle Admit requests.
                                    2. Admit: which leverages the index built by the Reconciler to apply
                                       mutations to resources.
                                    

                                    func NewReconciler

                                    func NewReconciler(
                                    	name, path, secretName string,
                                    	client kubernetes.Interface,
                                    	mwhLister admissionlisters.MutatingWebhookConfigurationLister,
                                    	secretLister corelisters.SecretLister,
                                    	withContext BindableContext,
                                    	options ...ReconcilerOption,
                                    ) *Reconciler

                                    func (*Reconciler) Admit

                                      Admit implements AdmissionController

                                      func (*Reconciler) Path

                                      func (ac *Reconciler) Path() string

                                        Path implements AdmissionController

                                        func (*Reconciler) Reconcile

                                        func (ac *Reconciler) Reconcile(ctx context.Context, key string) error

                                          Reconcile implements controller.Reconciler

                                          type ReconcilerOption

                                          type ReconcilerOption func(*Reconciler)

                                            ReconcilerOptions is a function to modify the Reconciler.

                                            func WithSelector

                                            func WithSelector(s metav1.LabelSelector) ReconcilerOption

                                              WithSelector specifies the selector for the webhook.

                                              type SubResourcesReconcilerInterface

                                              type SubResourcesReconcilerInterface interface {
                                              	Reconcile(ctx context.Context, fb Bindable) error
                                              	ReconcileDeletion(ctx context.Context, fb Bindable) error
                                              }

                                                SubResourcesReconcilerInterface is used to reconcile binding related sub-resources. Reconcile is executed after Binding's ReconcileSubject and ReconcileDeletion will be executed before Binding's ReconcileDeletion