* drop duplicate workflows these are now part of the style workflow from knative/actions * gofmt with go1.19 |
||
|---|---|---|
| .. | ||
| README.md | ||
| controller.go | ||
| doc.go | ||
| index.go | ||
| index_test.go | ||
| psbinding.go | ||
| reconciler.go | ||
| table_test.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}),
}
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.BaseReconcilermay be embedded and a customReconcile()defined, which can still take advantage of the sharedFinalizerhandling,Statusmanipulation 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,
)
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
}