psbinding

package
v0.0.3 Latest Latest
Warning

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

Go to latest
Published: Apr 15, 2022 License: Apache-2.0 Imports: 41 Imported by: 0

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.NewContext(ctx, wh, controller.ControllerOptions{WorkQueueName: "GithubBinding", Logger: logger})

	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
	}

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)

ReconcilerOption 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

Jump to

Keyboard shortcuts

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