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:
- The standard controller,
- 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.
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
.
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 customReconcile()
defined, which can still take advantage of the sharedFinalizer
handling,Status
manipulation orSubject
-reconciliation.
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
}
}
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,
)
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
}