knative.dev/pkg@v0.0.0-20260602142205-ac97e43f6622/apis/duck/ABOUT.md (about) 1 # Knative Duck Typing 2 3  4 5 **Figure 1:** How to integrate with Knative. 6 7 ## Problem statement 8 9 In Knative, we want to support 10 [loose coupling](https://docs.google.com/presentation/d/1KxKAcIZyblkXbpdGVCgmzfDDIhqgUcwsa0zlABfvaXI/edit#slide=id.p) 11 of the building blocks we are releasing. We want users to be able to use these 12 building blocks together, but also support composing them with non-Knative 13 components as well. 14 15 Unlike Knative’s 16 [pluggability story](https://docs.google.com/presentation/d/10KWynvAJYuOEWy69VBa6bHJVCqIsz1TNdEKosNvcpPY/edit#slide=id.p) 17 (for replacing subsystems within a building block), we do not want to require 18 that the systems with which we compose have **identical** APIs (distinct 19 implementations). However, we do need a way of accessing (reading / writing) 20 certain **_pieces_** of information in a structured way. 21 22 **Enter [duck typing](https://en.wikipedia.org/wiki/Duck_typing)**. We will 23 define a partial schema, to which resource authors will adhere if they want to 24 participate within certain contexts of Knative. 25 26 For instance, consider the partial schema: 27 28 ```yaml 29 foo: 30 bar: <string> 31 ``` 32 33 Both of these resources implement the above duck type: 34 35 ```yaml 36 baz: 1234 37 foo: 38 bar: asdf 39 blah: 40 blurp: true 41 ``` 42 43 ```yaml 44 field: running out of ideas 45 foo: 46 bar: a different string 47 another: you get the point 48 ``` 49 50 ### Reading duck-typed data 51 52 At a high-level, reading duck-typed data is very straightforward: using the 53 partial object schema deserialize the resource ignoring unknown fields. The 54 fields we care about can then be accessed through the structured object that 55 represents the duck type. 56 57 ### Writing duck-typed data 58 59 How to write duck-typed data is less straightforward because we do not want to 60 clobber every field we do not know about. To accomplish this, we will lean on 61 Kubernetes’ well established patching model. 62 63 First, we read the resource we intend to modify as our duck type. Keeping a copy 64 of the original, we then modify the fields of this duck typed resource to 65 reflect the change we want. Lastly, we synthesize a JSON Patch of the changes 66 between the original and the final version and issue a Patch to the Kubernetes 67 API with the delta. 68 69 Since the duck type inherently contains a subset of the fields in the resource, 70 the resulting JSON Patch can only contain fields relevant to the resource. 71 72 ## Example: Reading Knative-style Conditions 73 74 In Knative, we follow the Kubernetes API principles of using `conditions` as a 75 key part of our resources’ status, but we go a step further in 76 [defining particular conventions](https://github.com/knative/serving/blob/main/docs/spec/errors.md#error-conditions-and-reporting) 77 on how these are used. 78 79 To support this, we define: 80 81 ```golang 82 type KResource struct { 83 metav1.TypeMeta `json:",inline"` 84 metav1.ObjectMeta `json:"metadata,omitempty"` 85 86 Status KResourceStatus `json:"status"` 87 } 88 89 type KResourceStatus struct { 90 Conditions Conditions `json:"conditions,omitempty"` 91 } 92 93 type Conditions []Condition 94 95 type Condition struct { 96 // structure adhering to K8s API principles 97 ... 98 } 99 ``` 100 101 We can now deserialize and reason about the status of any Knative-compatible 102 resource using this partial schema. 103 104 ## Example: Mutating Knative CRD Generations 105 106 In Knative, all of our resources define a `.spec.generation` field, which we use 107 in place of `.metadata.generation` because the latter was not properly managed 108 by Kubernetes (prior to 1.11 with `/status` subresource). We manage bumping this 109 generation field in our webhook if and only if the `.spec` changed. 110 111 To support this, we define: 112 113 ```golang 114 type Generational struct { 115 metav1.TypeMeta `json:",inline"` 116 metav1.ObjectMeta `json:"metadata,omitempty"` 117 118 Spec GenerationalSpec `json:"spec"` 119 } 120 121 type GenerationalSpec struct { 122 Generation Generation `json:"generation,omitempty"` 123 } 124 125 type Generation int64 126 ``` 127 128 Using this our webhook can read the current resource’s generation, increment it, 129 and generate a patch to apply it. 130 131 ## Example: Mutating Core Kubernetes Resources 132 133 Kubernetes already uses duck typing, in a way. Consider that `Deployment`, 134 `ReplicaSet`, `DaemonSet`, `StatefulSet`, and `Job` all embed a 135 `corev1.PodTemplateSpec` at the exact path: `.spec.template`. 136 137 Consider the example duck type: 138 139 ```yaml 140 type PodSpecable corev1.PodTemplateSpec 141 142 type WithPod struct { metav1.TypeMeta `json:",inline"` metav1.ObjectMeta 143 `json:"metadata,omitempty"` 144 145 Spec WithPodSpec `json:"spec,omitempty"` } 146 147 type WithPodSpec struct { Template PodSpecable `json:"template,omitempty"` } 148 ``` 149 150 Using this, we can access the PodSpec of arbitrary higher-level Kubernetes 151 resources in a very structured way and generate patches to mutate them. 152 [See examples](https://github.com/knative/pkg/blob/07104dad53e803457a95306e5b1322024bd69af3/apis/duck/podspec_test.go#L49-L53). 153 154 _You can also see a sample controller that reconciles duck-typed resources 155 [here](https://github.com/mattmoor/cachier)._ 156 157 ## Conventions 158 159 Each of our duck types will consist of a single structured field that must be 160 enclosed within the containing resource in a particular way. 161 162 1. This structured field will be named <code>Foo<strong>able</strong></code>, 163 2. <code>Fooable</code> will be directly included via a field named 164 <code>fooable</code>, 165 3. Additional skeletal layers around <code>Fooable</code> will be defined to 166 fully define <code>Fooable</code>’s position within complete resources. 167 168 _You can see parts of these in the examples above, however, those special cases 169 have been exempted from the first condition for legacy compatibility reasons._ 170 171 For example: 172 173 1. `type Conditions []Condition` 174 2. <code>Conditions Conditions 175 `json:"<strong>conditions</strong>,omitempty"`</code> 176 3. <code>KResource -> KResourceStatus -> Conditions</code> 177 178 ## Supporting Mechanics 179 180 We will provide a number of tools to enable working with duck types without 181 blowing off feet. 182 183 ### Verification 184 185 To verify that a particular resource implements a particular duck type, resource 186 authors are strongly encouraged to add the following as test code adjacent to 187 resource definitions. 188 189 `myresource_types.go`: 190 191 ```golang 192 package v1alpha1 193 194 type MyResource struct { 195 ... 196 } 197 ``` 198 199 `myresource_types_test.go`: 200 201 ```golang 202 package v1alpha1 203 204 import ( 205 "testing" 206 207 // This is where supporting tools for duck-typing will live. 208 "github.com/knative/pkg/apis/duck" 209 210 // This is where Knative-provided duck types will live. 211 duckv1alpha1 "github.com/knative/pkg/apis/duck/v1alpha1" 212 ) 213 214 // This verifies that MyResource contains all the necessary fields for the 215 // given implementable duck type. 216 func TestType(t *testing.T) { 217 err := duck.VerifyType(&MyResource{}, &duckv1alpha1.Conditions{}) 218 if err != nil { 219 t.Errorf("VerifyType() = %v", err) 220 } 221 } 222 ``` 223 224 \_This call will create a fully populated instance of the skeletal resource 225 containing the Conditions and ensure that the fields can 100% roundtrip through 226 <code>MyResource</code>.</em> 227 228 ### Patching 229 230 To produce a patch of a particular resource modification suitable for use with 231 <code>k8s.io/client-[go/dynamic](https://goto.google.com/dynamic)</code>, 232 developers can write: 233 234 ```golang 235 before := … 236 after := before.DeepCopy() 237 // modify "after" 238 239 patch, err := duck.CreatePatch(before, after) 240 // check err 241 242 bytes, err := patch.MarshalJSON() 243 // check err 244 245 dynamicClient.Patch(bytes) 246 ``` 247 248 ### Informers / Listers 249 250 To be able to efficiently access / monitor arbitrary duck-typed resources, we 251 want to be able to produce an Informer / Lister for interpreting particular 252 resource groups as a particular duck type. 253 254 To facilitate this, we provide several composable implementations of 255 `duck.InformerFactory`. 256 257 ```golang 258 type InformerFactory interface { 259 // Get an informer/lister pair for the given resource group. 260 Get(GroupVersionResource) (SharedIndexInformer, GenericLister, error) 261 } 262 263 264 // This produces informer/lister pairs that interpret objects in the resource group 265 // as the provided duck "Type" 266 dif := &duck.TypedInformerFactory{ 267 Client: dynaClient, 268 Type: &duckv1alpha1.Foo{}, 269 ResyncPeriod: 30 * time.Second, 270 StopChannel: stopCh, 271 } 272 273 // This registers the provided EventHandler with the informer each time an 274 // informer/lister pair is produced. 275 eif := &duck.EnqueueInformerFactory{ 276 Delegate: dif, 277 EventHandler: cache.ResourceEventHandlerFuncs{ 278 AddFunc: impl.EnqueueControllerOf, 279 UpdateFunc: controller.PassNew(impl.EnqueueControllerOf), 280 }, 281 } 282 283 // This caches informer/lister pairs so that we only produce one for each GVR. 284 cif := &duck.CachedInformerFactory{ 285 Delegate: eif, 286 } 287 ``` 288 289 ### Trackers 290 291 Informers are great when you have something like an `OwnerReference` to key off 292 of for the association (e.g. `impl.EnqueueControllerOf`), however, when the 293 association is looser e.g. `corev1.ObjectReference`, then we need a way of 294 configuring a reconciliation trigger for the cross-reference. 295 296 For this (generally) we have the `knative/pkg/tracker` package. Here is how it 297 is used with duck types: 298 299 ```golang 300 c := &Reconciler{ 301 Base: reconciler.NewBase(opt, controllerAgentName), 302 ... 303 } 304 logger := c.Logger.Named("Revisions") 305 impl := controller.NewContext( 306 ctx, 307 c, 308 controller.ControllerOptions{WorkQueueName: "Revisions", Logger: logger}, 309 ) 310 311 // Calls to Track create a 30 minute lease before they must be renewed. 312 // Coordinate this value with controller resync periods. 313 t := tracker.New(impl.EnqueueKey, 30*time.Minute) 314 cif := &duck.CachedInformerFactory{ 315 Delegate: &duck.EnqueueInformerFactory{ 316 Delegate: buildInformerFactory, 317 EventHandler: cache.ResourceEventHandlerFuncs{ 318 AddFunc: t.OnChanged, 319 UpdateFunc: controller.PassNew(t.OnChanged), 320 }, 321 }, 322 } 323 324 // Now use: c.buildInformerFactory.Get() to access ObjectReferences. 325 c.buildInformerFactory = buildInformerFactory 326 327 // Now use: c.tracker.Track(rev.Spec.BuildRef, rev) to queue rev 328 // each time rev.Spec.BuildRef changes. 329 c.tracker = t 330 ```