knative.dev/pkg@v0.0.0-20260602142205-ac97e43f6622/apis/duck/ABOUT.md (about)

     1  # Knative Duck Typing
     2  
     3  ![A Trojan Duck](images/Knative-Duck0.png)
     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  ```