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:
- 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.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,
)
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
- The AdmissionController, which lives in psbinding.go (controller.go) sets it up.
- 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 ¶
- Variables
- func NewAdmissionController(ctx context.Context, name, path string, gla GetListAll, ...) *controller.Impl
- type BaseReconciler
- func (r *BaseReconciler) EnsureFinalizer(ctx context.Context, fb kmeta.Accessor) error
- func (r *BaseReconciler) IsFinalizing(ctx context.Context, fb kmeta.Accessor) bool
- func (r *BaseReconciler) Reconcile(ctx context.Context, key string) error
- func (r *BaseReconciler) ReconcileDeletion(ctx context.Context, fb Bindable) error
- func (r *BaseReconciler) ReconcileSubject(ctx context.Context, fb Bindable, mutation Mutation) error
- func (r *BaseReconciler) RemoveFinalizer(ctx context.Context, fb kmeta.Accessor) error
- func (r *BaseReconciler) UpdateStatus(ctx context.Context, desired Bindable) error
- type Bindable
- type BindableContext
- type GetListAll
- type ListAll
- type Mutation
- type Reconciler
- type ReconcilerOption
- type SubResourcesReconcilerInterface
Constants ¶
This section is empty.
Variables ¶
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 ¶
EnsureFinalizer makes sure that the provided resource has a finalizer in the form of this BaseReconciler's GVR's stringified GroupResource.
func (*BaseReconciler) IsFinalizing ¶
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 ¶
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 ¶
BindableContext is the type of context decorator methods that may be supplied to NewAdmissionController and BaseReconciler.
type GetListAll ¶
type GetListAll func(context.Context, cache.ResourceEventHandler) ListAll
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 ¶
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 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:
- 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.
- 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 ¶
func (ac *Reconciler) Admit(ctx context.Context, request *admissionv1.AdmissionRequest) *admissionv1.AdmissionResponse
Admit implements AdmissionController
type ReconcilerOption ¶
type ReconcilerOption func(*Reconciler)
ReconcilerOptions 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