Skip to content

Latest commit

 

History

History
275 lines (224 loc) · 8.51 KB

File metadata and controls

275 lines (224 loc) · 8.51 KB

"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
	}