Produced via: `dep ensure -update knative.dev/test-infra knative.dev/pkg` /assign n3wscott |
||
---|---|---|
.. | ||
README.md | ||
controller.go | ||
doc.go | ||
index.go | ||
psbinding.go | ||
reconciler.go |
README.md
"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:
- 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.
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}),
}
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 customReconcile()
defined, which can still take advantage of the sharedFinalizer
handling,Status
manipulation orSubject
-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,
)