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 ```