mirror of https://github.com/knative/pkg.git
276 lines
8.4 KiB
Markdown
276 lines
8.4 KiB
Markdown
# "Pod Spec"-able Bindings
|
|
|
|
The `psbinding` package provides facilities to make authoring
|
|
[Bindings](https://docs.google.com/document/d/1t5WVrj2KQZ2u5s0LvIUtfHnSonBv5Vcv8Gl2k5NXrCQ/edit)
|
|
whose subjects adhere to
|
|
[`duckv1.PodSpecable`](https://github.com/knative/pkg/blob/main/apis/duck/v1/podspec_types.go#L32)
|
|
easier. The Bindings doc mentions two key elements of the controller
|
|
architecture:
|
|
|
|
1. The standard controller,
|
|
1. 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:
|
|
|
|
```yaml
|
|
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](https://github.com/mattmoor/bindings#githubbinding) on which this is
|
|
based.
|
|
|
|
### `Do` and `Undo`
|
|
|
|
The `Undo` method itself is simply: remove the named secret volume and any
|
|
mounts of it:
|
|
|
|
```go
|
|
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.
|
|
|
|
```go
|
|
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.
|
|
|
|
```go
|
|
// 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 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:
|
|
|
|
```go
|
|
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:
|
|
|
|
```go
|
|
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:
|
|
|
|
```go
|
|
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:
|
|
|
|
```go
|
|
kclient := kubeclient.Get(ctx)
|
|
srr := FooBindingSubResourcesReconciler{
|
|
Client: kclient,
|
|
}
|
|
c := &psbinding.BaseReconciler{
|
|
...
|
|
SubresourcesReconciler: srr
|
|
}
|
|
```
|