knative.dev/pkg@v0.0.0-20260602142205-ac97e43f6622/webhook/psbinding/README.md (about)

     1  # "Pod Spec"-able Bindings
     2  
     3  The `psbinding` package provides facilities to make authoring
     4  [Bindings](https://docs.google.com/document/d/1t5WVrj2KQZ2u5s0LvIUtfHnSonBv5Vcv8Gl2k5NXrCQ/edit)
     5  whose subjects adhere to
     6  [`duckv1.PodSpecable`](https://github.com/knative/pkg/blob/main/apis/duck/v1/podspec_types.go#L32)
     7  easier. The Bindings doc mentions two key elements of the controller
     8  architecture:
     9  
    10  1. The standard controller,
    11  1. The mutating webhook (or "admission controller")
    12  
    13  This package provides facilities for bootstrapping both of these elements. To
    14  leverage the `psbinding` package, folks should adjust their Binding types to
    15  implement `psbinding.Bindable`, which contains a variety of methods that will
    16  look familiar to Knative controller authors with two new key methods: `Do` and
    17  `Undo` (aka the "mutation" methods).
    18  
    19  The mutation methods on the Binding take in
    20  `(context.Context, *duckv1.WithPod)`, and are expected to alter the
    21  `*duckv1.WithPod` appropriately to achieve the semantics of the Binding. So for
    22  example, if the Binding's runtime contract is the inclusion of a new environment
    23  variable `FOO` with some value extracted from the Binding's `spec` then in
    24  `Do()` the `duckv1.WithPod` would be altered so that each of the `containers:`
    25  contains:
    26  
    27  ```yaml
    28  env:
    29    - name: "FOO"
    30      value: "<from Binding spec>"
    31  ```
    32  
    33  ... and `Undo()` would remove these variables. `Do` is invoked for active
    34  Bindings, and `Undo` is invoked when they are being deleted, but their subjects
    35  remain.
    36  
    37  We will walk through a simple example Binding whose runtime contract is to mount
    38  secrets for talking to Github under `/var/bindings/github`.
    39  [See also](https://github.com/mattmoor/bindings#githubbinding) on which this is
    40  based.
    41  
    42  ### `Do` and `Undo`
    43  
    44  The `Undo` method itself is simply: remove the named secret volume and any
    45  mounts of it:
    46  
    47  ```go
    48  func (fb *GithubBinding) Undo(ctx context.Context, ps *duckv1.WithPod) {
    49  	spec := ps.Spec.Template.Spec
    50  
    51  	// Make sure the PodSpec does NOT have the github volume.
    52  	for i, v := range spec.Volumes {
    53  		if v.Name == github.VolumeName {
    54  			ps.Spec.Template.Spec.Volumes = append(spec.Volumes[:i], spec.Volumes[i+1:]...)
    55  			break
    56  		}
    57  	}
    58  
    59  	// Make sure that none of the [init]containers have the github volume mount
    60  	for i, c := range spec.InitContainers {
    61  		for j, vm := range c.VolumeMounts {
    62  			if vm.Name == github.VolumeName {
    63  				spec.InitContainers[i].VolumeMounts = append(vm[:j], vm[j+1:]...)
    64  				break
    65  			}
    66  		}
    67  	}
    68  	for i, c := range spec.Containers {
    69  		for j, vm := range c.VolumeMounts {
    70  			if vm.Name == github.VolumeName {
    71  				spec.Containers[i].VolumeMounts = append(vm[:j], vm[j+1:]...)
    72  				break
    73  			}
    74  		}
    75  	}
    76  }
    77  ```
    78  
    79  The `Do` method is the dual of this: ensure that the volume exists, and all
    80  containers have it mounted.
    81  
    82  ```go
    83  func (fb *GithubBinding) Do(ctx context.Context, ps *duckv1.WithPod) {
    84  
    85  	// First undo so that we can just unconditionally append below.
    86  	fb.Undo(ctx, ps)
    87  
    88  	// Make sure the PodSpec has a Volume like this:
    89  	volume := corev1.Volume{
    90  		Name: github.VolumeName,
    91  		VolumeSource: corev1.VolumeSource{
    92  			Secret: &corev1.SecretVolumeSource{
    93  				SecretName: fb.Spec.Secret.Name,
    94  			},
    95  		},
    96  	}
    97  	ps.Spec.Template.Spec.Volumes = append(ps.Spec.Template.Spec.Volumes, volume)
    98  
    99  	// Make sure that each [init]container in the PodSpec has a VolumeMount like this:
   100  	volumeMount := corev1.VolumeMount{
   101  		Name:      github.VolumeName,
   102  		ReadOnly:  true,
   103  		MountPath: github.MountPath,
   104  	}
   105  	spec := ps.Spec.Template.Spec
   106  	for i := range spec.InitContainers {
   107  		spec.InitContainers[i].VolumeMounts = append(spec.InitContainers[i].VolumeMounts, volumeMount)
   108  	}
   109  	for i := range spec.Containers {
   110  		spec.Containers[i].VolumeMounts = append(spec.Containers[i].VolumeMounts, volumeMount)
   111  	}
   112  }
   113  ```
   114  
   115  > Note: if additional context is needed to perform the mutation, then it may be
   116  > attached-to / extracted-from the supplied `context.Context`.
   117  
   118  ### The standard controller
   119  
   120  For simple Bindings (such as our `GithubBinding`), we should be able to
   121  implement our `*controller.Impl` by directly leveraging
   122  `*psbinding.BaseReconciler` to fully implement reconciliation.
   123  
   124  ```go
   125  // NewController returns a new GithubBinding reconciler.
   126  func NewController(
   127  	ctx context.Context,
   128  	cmw configmap.Watcher,
   129  ) *controller.Impl {
   130  	logger := logging.FromContext(ctx)
   131  
   132  	ghInformer := ghinformer.Get(ctx)
   133  	dc := dynamicclient.Get(ctx)
   134  	psInformerFactory := podspecable.Get(ctx)
   135  
   136  	c := &psbinding.BaseReconciler{
   137  		GVR: v1alpha1.SchemeGroupVersion.WithResource("githubbindings"),
   138  		Get: func(namespace string, name string) (psbinding.Bindable, error) {
   139  			return ghInformer.Lister().GithubBindings(namespace).Get(name)
   140  		},
   141  		DynamicClient: dc,
   142  		Recorder: record.NewBroadcaster().NewRecorder(
   143  			scheme.Scheme, corev1.EventSource{Component: controllerAgentName}),
   144  	}
   145  	logger = logger.Named("GithubBindings")
   146  	impl := controller.NewContext(ctx, wh, controller.ControllerOptions{WorkQueueName: "GithubBinding", Logger: logger})
   147  
   148  	logger.Info("Setting up event handlers")
   149  
   150  	ghInformer.Informer().AddEventHandler(controller.HandleAll(impl.Enqueue))
   151  
   152  	c.Tracker = tracker.New(impl.EnqueueKey, controller.GetTrackerLease(ctx))
   153  	c.Factory = &duck.CachedInformerFactory{
   154  		Delegate: &duck.EnqueueInformerFactory{
   155  			Delegate:     psInformerFactory,
   156  			EventHandler: controller.HandleAll(c.Tracker.OnChanged),
   157  		},
   158  	}
   159  
   160  	// If our `Do` / `Undo` methods need additional context, then we can
   161  	// setup a callback to infuse the `context.Context` here:
   162  	//    c.WithContext = ...
   163  	// Note that this can also set up additional informer watch events to
   164  	// trigger reconciliation when the infused context changes.
   165  
   166  	return impl
   167  }
   168  ```
   169  
   170  > Note: if customized reconciliation logic is needed (e.g. synthesizing
   171  > additional resources), then the `psbinding.BaseReconciler` may be embedded and
   172  > a custom `Reconcile()` defined, which can still take advantage of the shared
   173  > `Finalizer` handling, `Status` manipulation or `Subject`-reconciliation.
   174  
   175  ### The mutating webhook
   176  
   177  Setting up the mutating webhook is even simpler:
   178  
   179  ```go
   180  func NewWebhook(ctx context.Context, cmw configmap.Watcher) *controller.Impl {
   181  		return psbinding.NewAdmissionController(ctx,
   182  			// Name of the resource webhook.
   183  			"githubbindings.webhook.bindings.mattmoor.dev",
   184  
   185  			// The path on which to serve the webhook.
   186  			"/githubbindings",
   187  
   188  			// How to get all the Bindables for configuring the mutating webhook.
   189  			ListAll,
   190  
   191  			// How to setup the context prior to invoking Do/Undo.
   192  			func(ctx context.Context, b psbinding.Bindable) (context.Context, error) {
   193  				return ctx, nil
   194  			},
   195  		)
   196  	}
   197  }
   198  
   199  // ListAll enumerates all of the GithubBindings as Bindables so that the webhook
   200  // can reprogram itself as-needed.
   201  func ListAll(ctx context.Context, handler cache.ResourceEventHandler) psbinding.ListAll {
   202  	ghInformer := ghinformer.Get(ctx)
   203  
   204  	// Whenever a GithubBinding changes our webhook programming might change.
   205  	ghInformer.Informer().AddEventHandler(handler)
   206  
   207  	return func() ([]psbinding.Bindable, error) {
   208  		l, err := ghInformer.Lister().List(labels.Everything())
   209  		if err != nil {
   210  			return nil, err
   211  		}
   212  		bl := make([]psbinding.Bindable, 0, len(l))
   213  		for _, elt := range l {
   214  			bl = append(bl, elt)
   215  		}
   216  		return bl, nil
   217  	}
   218  }
   219  ```
   220  
   221  ### Putting it together
   222  
   223  With the above defined, then in our webhook's `main.go` we invoke
   224  `sharedmain.MainWithContext` passing the additional controller constructors:
   225  
   226  ```go
   227  	sharedmain.MainWithContext(ctx, "webhook",
   228  		// Our other controllers.
   229  		// ...
   230  
   231  		// For each binding we have our controller and binding webhook.
   232  		githubbinding.NewController, githubbinding.NewWebhook,
   233  	)
   234  ```
   235  
   236  ### Subresource reconciler
   237  
   238  Sometimes we might find the need for controlling not only `psbinding.Bindable`
   239  and `duckv1.WithPod`, but also other resources. We can achieve this by
   240  implementing `psbinding.SubResourcesReconcilerInterface` and injecting it in the
   241  `psbinding.BaseReconciler`.
   242  
   243  For example we can implement a SubResourcesReconciler to create/delete k8s
   244  resources:
   245  
   246  ```go
   247  type FooBindingSubResourcesReconciler struct {
   248      Client kubernetes.Interface
   249  }
   250  
   251  func (fr *FooBindingSubresourcesReconciler) Reconcile(ctx context.Context, fb psbinding.Bindable) error {
   252      // Logic to create k8s resources here
   253      return err
   254  }
   255  
   256  func (fr *FooBindingSubresourcesReconciler) ReconcileDeletion(ctx context.Context, fb psbinding.Bindable) error {
   257      // Logic to delete k8s resources related to our Bindable
   258      return err
   259  }
   260  
   261  ```
   262  
   263  The SubResourcesReconciler can be then injected in the
   264  `psbinding.BaseReconciler` as follows:
   265  
   266  ```go
   267  kclient := kubeclient.Get(ctx)
   268  srr := FooBindingSubResourcesReconciler{
   269      Client: kclient,
   270  }
   271  c := &psbinding.BaseReconciler{
   272  		...
   273          SubresourcesReconciler: srr
   274  	}
   275  ```