<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Shuhei Kitagawa</title>
    <description>The latest articles on DEV Community by Shuhei Kitagawa (@shuheiktgw).</description>
    <link>https://dev.to/shuheiktgw</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F105682%2F0d83ef70-ad5c-4a19-9512-994ae90ac77e.jpeg</url>
      <title>DEV Community: Shuhei Kitagawa</title>
      <link>https://dev.to/shuheiktgw</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/shuheiktgw"/>
    <language>en</language>
    <item>
      <title>Understanding the controller-runtime Cache Seriously</title>
      <dc:creator>Shuhei Kitagawa</dc:creator>
      <pubDate>Sat, 11 May 2024 00:37:06 +0000</pubDate>
      <link>https://dev.to/shuheiktgw/understanding-the-controller-runtime-cache-seriously-3c2k</link>
      <guid>https://dev.to/shuheiktgw/understanding-the-controller-runtime-cache-seriously-3c2k</guid>
      <description>&lt;p&gt;The &lt;a href="https://github.com/kubernetes-sigs/controller-runtime/tree/main" rel="noopener noreferrer"&gt;controller-runtime&lt;/a&gt; package provides a &lt;a href="https://github.com/kubernetes-sigs/controller-runtime/tree/24cc51fde1b37ece947b6ecc4f92ceaa4fe942b5/pkg/cache" rel="noopener noreferrer"&gt;Cache&lt;/a&gt; mechanism to users so that they don need to be aware of Informer's presence. By default, when you Get/List Kubernetes Objects, the Cache will automatically start Informers in the background and cache all Objects with the same GVK (GroupVersionKind).&lt;/p&gt;

&lt;p&gt;This (somewhat naïve) behavior of the Cache usually doesn't pose an issue in small Kubernetes Clusters. However, as the cluster scales up, this can lead Controllers to unexpectedly consume a significant amount of memory. To address this, the Cache comes equipped with &lt;a href="https://github.com/kubernetes-sigs/controller-runtime/blob/24cc51fde1b37ece947b6ecc4f92ceaa4fe942b5/pkg/cache/cache.go#L135" rel="noopener noreferrer"&gt;Options&lt;/a&gt; to finely control the behavior of Informers.&lt;/p&gt;

&lt;p&gt;However, there is a lack of documentation regarding these Options, making it challenging to understand the intricate behaviors. Therefore, we will first delve into the Cache code and then consider the behavior of the Options. When we refer to "Cache" below, it encompasses the &lt;a href="https://github.com/kubernetes-sigs/controller-runtime/blob/24cc51fde1b37ece947b6ecc4f92ceaa4fe942b5/pkg/cache/cache.go#L65" rel="noopener noreferrer"&gt;Cache interface&lt;/a&gt;, as well as its implementations, namely &lt;a href="https://github.com/kubernetes-sigs/controller-runtime/blob/bc35946fac1b56071def3ebf299996b281c77442/pkg/cache/informer_cache.go#L67" rel="noopener noreferrer"&gt;informerCache&lt;/a&gt;, &lt;a href="https://github.com/kubernetes-sigs/controller-runtime/blob/bc35946fac1b56071def3ebf299996b281c77442/pkg/cache/multi_namespace_cache.go#L68" rel="noopener noreferrer"&gt;multiNamespaceCache&lt;/a&gt;, and &lt;a href="https://github.com/kubernetes-sigs/controller-runtime/blob/bc35946fac1b56071def3ebf299996b281c77442/pkg/cache/delegating_by_gvk_cache.go#L33C6-L33C26" rel="noopener noreferrer"&gt;delegatingByGVKCache&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Get/List and Cache
&lt;/h2&gt;

&lt;p&gt;Before getting into the details of Cache, let's check the general flow of Get/List Kubernetes Objects using Cache. When using controller-runtime, you typically start by obtaining a Client to interact with the Kubernetes API using the &lt;a href="https://github.com/kubernetes-sigs/controller-runtime/blob/a6b9c0b672c77a79fff4d5bc03221af1e1fe21fa/pkg/cluster/cluster.go#L57" rel="noopener noreferrer"&gt;GetClient method&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The actual GetClient method is &lt;a href="https://github.com/kubernetes-sigs/controller-runtime/blob/77435913f5134b524dc0b83e8ed754a3f4174281/pkg/manager/internal.go#L224" rel="noopener noreferrer"&gt;controllerManager's GetClient method&lt;/a&gt;, which internally calls the &lt;a href="https://github.com/kubernetes-sigs/controller-runtime/blob/4fd4f6e7116453ea4e07a2cdd8ba0d38ad64680f/pkg/cluster/internal.go#L70" rel="noopener noreferrer"&gt;cluster's GetClient method&lt;/a&gt; This method simply returns the value of the cluster's client field, so we'll now look at what is set in this client field.&lt;/p&gt;

&lt;p&gt;What is set in the client field is &lt;a href="https://github.com/kubernetes-sigs/controller-runtime/blob/a6b9c0b672c77a79fff4d5bc03221af1e1fe21fa/pkg/cluster/cluster.go#L227" rel="noopener noreferrer"&gt;clientWriter&lt;/a&gt;. The &lt;code&gt;clientWriter&lt;/code&gt; is initialized using the options.NewClient function, with the &lt;a href="https://github.com/kubernetes-sigs/controller-runtime/blob/55c4331ea389d4d0332ac6d01b9b1c02969a62/pkg/client/client.go#L109" rel="noopener noreferrer"&gt;client package's New function&lt;/a&gt; set as the default. Here, Cache is also initialized using the options.NewCache function, with the &lt;a href="https://github.com/kubernetes-sigs/controller-runtime/blob/4cf9db06e3b06be9c57ced5d8b705339b816765d/pkg/cache/cache.go#L307" rel="noopener noreferrer"&gt;cache package's New function&lt;/a&gt; set as the default. The initialized Cache is passed as an argument to &lt;a href="https://github.com/kubernetes-sigs/controller-runtime/blob/a6b9c0b672c77a79fff4d5bc03221af1e1fe21fa/pkg/cluster/cluster.go#L224" rel="noopener noreferrer"&gt;options.NewClient&lt;/a&gt;&lt;sup id="fnref1"&gt;1&lt;/sup&gt;&lt;sup id="fnref2"&gt;2&lt;/sup&gt;.&lt;/p&gt;

&lt;p&gt;The cache package's New function then calls the &lt;a href="https://github.com/kubernetes-sigs/controller-runtime/blob/55c4331eaec389d4d0332ac6d01b9b1c02969a62/pkg/client/client.go#L117" rel="noopener noreferrer"&gt;newClient function&lt;/a&gt; to initialize the Client. It is clear that the Client's cache field is set with the Cache we initialized earlier &lt;a href="https://github.com/kubernetes-sigs/controller-runtime/blob/55c4331eaec389d4d0332ac6d01b9b1c02969a62/pkg/client/client.go#L203" rel="noopener noreferrer"&gt;(link)&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;All we need to do now is to look at the client's &lt;a href="https://github.com/kubernetes-sigs/controller-runtime/blob/55c4331eaec389d4d0332ac6d01b9bc02969a62/pkg/client/client.go#L352-L357" rel="noopener noreferrer"&gt;Get&lt;/a&gt; and &lt;a href="https://github.com/kubernetes-sigs/controller-runtime/blob/55c4331eaec389d4d0332ac6d01b9b1c02969a62/pkg/client/client.go#L374-L379" rel="noopener noreferrer"&gt;List&lt;/a&gt; methods. After calling the &lt;a href="https://github.com/kubernetes-sigs/controller-runtime/blob/55c4331eaec389d4d0332ac6d01b9b1c02969a62/pkg/client/client.go#L235" rel="noopener noreferrer"&gt;shouldBypassCache method&lt;/a&gt;, if the Kubernetes Object is allowed to be cached, the Get/List methods of Cache are called.&lt;/p&gt;

&lt;p&gt;Although this was rather extensive, we've now determined that calling Get/List using a client obtained from the GetClient method actually calls the Get/List methods of Cache.&lt;/p&gt;

&lt;h2&gt;
  
  
  Types of Cache
&lt;/h2&gt;

&lt;p&gt;The following types of Cache implement the &lt;a href="https://github.com/kubernetes-sigs/controller-runtime/blob/4cf9db06e3b06be9c57ced5d8b705339b816765d/pkg/cache/cache.go#L65" rel="noopener noreferrer"&gt;Cache interface&lt;/a&gt;:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;a href="https://github.com/kubernetes-sigs/controller-runtime/blob/bc35946fac1b56071def3ebf299996b281c77442/pkg/cache/informer_cache.go#L67" rel="noopener noreferrer"&gt;informerCache&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/kubernetes-sigs/controller-runtime/blob/bc35946fac1b56071def3ebf299996b281c77442/pkg/cache/multi_namespace_cache.go#L68C6-L68C26" rel="noopener noreferrer"&gt;multiNamespaceCache&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/kubernetes-sigs/controller-runtime/blob/bc35946fac1b56071def3ebf299996b281c77442/pkg/cache/delegating_by_gvk_cache.go#L33C6-L33C26" rel="noopener noreferrer"&gt;delegatingByGVKCache&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;informerCache is the most basic implementation of Cache, and it is directly used as Cache without specifying the Cache Options DefaultNamespaces or ByObject. multiNamespaceCache is used when setting the &lt;a href="https://github.com/kubernetes-sigs/controller-runtime/blob/4cf9db06e3b06be9c57ced5d8b705339b816765d/pkg/cache/cache.go#L192" rel="noopener noreferrer"&gt;DefaultNamespaces option&lt;/a&gt;, and internally has a map of &lt;a href="https://github.com/kubernetes-sigs/controller-runtime/blob/6dd03ed1f07cf03b34d088b9056082198039e328/pkg/cache/multi_namespace_cache.go#L72" rel="noopener noreferrer"&gt;namespace name to Cache (= informerCache)&lt;/a&gt;, allowing you to specify the behavior of Informers for particular namespaces.&lt;/p&gt;

&lt;p&gt;delegatingByGVKCache is used when setting the &lt;a href="https://github.com/kubernetes-sigs/controller-runtime/blob/4cf9db06e3b06be9c57ced5d8b705339b816765d/pkg/cache/cache.go#L227" rel="noopener noreferrer"&gt;ByObject option&lt;/a&gt;, and internally a map of &lt;a href="https://github.com/kubernetes-sigs/controller-runtime/blob/6dd03ed1f07cf03b34d088b9056082198039e328/pkg/cache/delegating_by_gvk_cache.go#L35" rel="noopener noreferrer"&gt;GVK to Cache&lt;/a&gt;. It allows you to specify the behavior of Informers for particular GVKs. If the &lt;a href="https://github.com/kubernetes-sigs/controller-runtime/blob/4cf9db06e3b06be9c57ced5d8b705339b816765d/pkg/cache/cache.go#L255" rel="noopener noreferrer"&gt;Namespaces option of ByObject&lt;/a&gt; is set&lt;sup id="fnref3"&gt;3&lt;/sup&gt;, then multiNamespaceCache is used as Cache for the corresponding GVK; otherwise, informerCache is used. Let's take a closer look at the behavior of each Cache.&lt;/p&gt;

&lt;h3&gt;
  
  
  informerCache
&lt;/h3&gt;

&lt;p&gt;Using the &lt;a href="https://github.com/kubernetes-sigs/controller-runtime/blob/bc35946fac1b56071def3ebf299996b281c77442/pkg/cache/informer_cache.go#L74" rel="noopener noreferrer"&gt;informerCache's Get method&lt;/a&gt; as an example, let's confirm the flow of dynamically created Informers. The Get method internally calls the &lt;a href="https://github.com/kubernetes-sigs/controller-runtime/blob/bc35946fac1b56071def3ebf299996b281c77442/pkg/cache/informer_cache.go#L80" rel="noopener noreferrer"&gt;getInformerForKind method of informerCache&lt;/a&gt;, which in &lt;a href="https://github.com/kubernetes-sigs/controller-runtime/blob/bc35946fac1b56071def3ebf299996b281c77442/pkg/cache/informer_cache.go#L190" rel="noopener noreferrer"&gt;calls the Get method of Informers&lt;/a&gt;. If the Informer does not exist, Get method of Informers invokes the &lt;a href="https://github.com/kubernetes-sigs/controller-runtime/blob/6dd03ed1f07cf03b34d088b9056082198039e328/pkg/cache/internal/informers.go#L342" rel="noopener noreferrer"&gt;addInformerToMap method&lt;/a&gt; to &lt;a href="https://github.com/kubernetes-sigs/controller-runtime/blob/6dd03ed1f07cf03b34d088b9056082198039e328/pkg/cache/internal/informers.go#L358-L370" rel="noopener noreferrer"&gt;create an Informer&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The created Informer is packed into a Cache entry (this Cache is not the Cache interface of the &lt;a href="https://github.com/kubernetes-sigs/controller-runtime/blob/6dd03ed1f07cf03b34d088b9056082198039e328/pkg/cache/cache.go#L65" rel="noopener noreferrer"&gt;cache package&lt;/a&gt;, but rather the &lt;a href="https://github.com/kubernetes-sigs/controller-runtime/blob/6dd03ed1f07cf03b34d088b9056082198039e328/pkg/cache/internal/informers.go#L87" rel="noopener noreferrer"&gt;Cache struct of the internal package&lt;/a&gt;), and is then &lt;a href="https://github.com/kubernetes-sigs/controller-runtime/blob/6dd03ed1f07cf03b34d088b9056082198039e328/pkg/cache/internal/informers.go#L138" rel="noopener noreferrer"&gt;saved to the tracker field of Informers&lt;/a&gt; via the &lt;a href="https://github.com/kubernetes-sigs/controller-runtime/blob/6dd03ed1f07cf03b34d088b9056082198039e328/pkg/cache/internal/informers.go#L330" rel="noopener noreferrer"&gt;informersByType method&lt;/a&gt;&lt;sup id="fnref4"&gt;4&lt;/sup&gt;.&lt;/p&gt;

&lt;p&gt;The Get method of informerCache eventually obtains the Object through the &lt;a href="https://github.com/kubernetes-sigs/controller-runtime/blob/bc35946fac1b56071def3ebf299996b281c77442/pkg/cache/informer_cache.go#L88" rel="noopener noreferrer"&gt;Get method of CacheReader set in the Reader field of this Cache entry&lt;/a&gt;. The &lt;a href="https://github.com/kubernetes-sigs/controller-runtime/blob/bbcaf6a9d9ff0ba256e18800f194d387b96dca33/pkg/cache/internal/cache_reader.go#L57" rel="noopener noreferrer"&gt;Get method of CacheReader&lt;/a&gt; retrieves the Object from the &lt;a href="https://github.com/kubernetes-sigs/controller-runtime/blob/bbcaf6a9d9ff0ba256e18800f194d387b96dca33/pkg/cache/internal/cache_reader.go#L64" rel="noopener noreferrer"&gt;Indexer&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  multiNamespaceCache
&lt;/h3&gt;

&lt;p&gt;For multiNamespaceCache as well, let's quickly look at its behavior. The multiNamespaceCache uses the &lt;a href="https://github.com/kubernetes-sigs/controller-runtime/blob/6dd03ed1f07cf03b34d088b9056082198039e328/pkg/cache/multi_namespace_cache.go#L38" rel="noopener noreferrer"&gt;newMultiNamespaceCache function&lt;/a&gt; to &lt;a href="https://github.com/kubernetes-sigs/controller-runtime/blob/6dd03ed1f07cf03b34d088b9056082198039e328/pkg/cache/multi_namespace_cache.go#L48" rel="noopener noreferrer"&gt;create an informerCache for each namespace&lt;/a&gt; and saves them in the namespaceToCache field. The &lt;a href="https://github.com/kubernetes-sigs/controller-runtime/blob/6dd03ed1f07cf03b34d088b9056082198039e328/pkg/cache/multi_namespace_cache.go#L226" rel="noopener noreferrer"&gt;Get method of multiNamespaceCache&lt;/a&gt; extracts the corresponding informerCache from the namespaceToCache field based on the namespace of the given Object, and &lt;a href="https://github.com/kubernetes-sigs/controller-runtime/blob/6dd03ed1f07cf03b34d088b9056082198039e328/pkg/cache/multi_namespace_cache.go#L244" rel="noopener noreferrer"&gt;calls the Get method of that informerCache&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  delegatingByGVKCache
&lt;/h3&gt;

&lt;p&gt;The delegatingByGVKCache &lt;a href="https://github.com/kubernetes-sigs/controller-runtime/blob/4cf9db06e3b06be9c57ced5d8b705339b816765d/pkg/cache/cache.go#L333-L345" rel="noopener noreferrer"&gt;generates a Cache for each GVK based on the ByObject option&lt;/a&gt; and &lt;a href="https://github.com/kubernetes-sigs/controller-runtime/blob/24cc51fde1b37ece947b6ecc4f92ceaa4fe942b5/pkg/cache/delegating_by_gvk_cache.go#L35" rel="noopener noreferrer"&gt;saves them in the caches field of delegatingByGVKCache&lt;/a&gt;. As mentioned before, the Cache created here will be either an informerCache or a multiNamespaceCache depending on the option. The &lt;a href="https://github.com/kubernetes-sigs/controller-runtime/blob/bbcaf6a9d9ff0ba256e18800f194d387b96dca33/pkg/cache/delegating_by_gvk_cache.go#L39" rel="noopener noreferrer"&gt;Get method of delegatingByGVKCache&lt;/a&gt; extracts the Cache corresponding to the given Object's GVK and &lt;a href="https://github.com/kubernetes-sigs/controller-runtime/blob/bbcaf6a9d9ff0ba256e18800f194d387b96dca33/pkg/cache/delegating_by_gvk_cache.go#L44" rel="noopener noreferrer"&gt;calls the Get method of that Cache&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Builder's For, Watches, and Owns, and Cache
&lt;/h2&gt;

&lt;p&gt;In addition to calling Client's Get/List, you can also register Informers through Builder's &lt;a href="https://github.com/kubernetes-sigs/controller-runtime/blob/2add01e784f9f75a8f1be38fc0a21eee5eaee84f/pkg/builder/controller.go#L84" rel="noopener noreferrer"&gt;For&lt;/a&gt;, &lt;a href="https://github.com/kubernetes-sigs/controller-runtime/blob/2add01e784f9f75a8f1be38fc0a21eee5eaee84f/pkg/builder/controller.go#L137" rel="noopener noreferrer"&gt;Watches&lt;/a&gt;, and &lt;a href="https://github.com/kubernetes-sigs/controller-runtime/blob/2add01e784f9f75a8f1be38fc0a21eee5eaee84f/pkg/builder/controller.go#L114" rel="noopener noreferrer"&gt;Owns&lt;/a&gt; methods. Let's check the behavior of Cache in these cases as well. If we look at the &lt;a href="https://github.com/kubernetes-sigs/controller-runtime/blob/2add01e784f9f75a8f1be38fc0a21eee5eaee84f/pkg/builder/controller.go#L275" rel="noopener noreferrer"&gt;Builder's doWatch method&lt;/a&gt;, whether you use For, Watches, or Owns, you ultimately call the &lt;a href="https://github.com/kubernetes-sigs/controller-runtime/blob/5d388e9c8380816fa655fd3b6999b8b04ba5cd4c/pkg/internal/controller/controller.go#L118" rel="noopener noreferrer"&gt;Watch method of the Controller&lt;/a&gt;, which then &lt;a href="https://github.com/kubernetes-sigs/controller-runtime/blob/2add01e784f9f75a8f1be38fc0a21eee5eaee84f/pkg/internal/source/kind.go#L41" rel="noopener noreferrer"&gt;uses the Start method of Kind&lt;/a&gt; to &lt;a href="https://github.com/kubernetes-sigs/controller-runtime/blob/2add01e784f9f75a8f1be38fc0a21eee5eaee84f/pkg/internal/source/kind.go#L66" rel="noopener noreferrer"&gt;register the Informer using the GetInformer method of Cache&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cache Options
&lt;/h2&gt;

&lt;p&gt;Let's take a closer look at the behaviors of &lt;a href="https://github.com/kubernetes-sigs/controller-runtime/blob/4cf9db06e3b06be9c57ced5d8b705339b816765d/pkg/cache/cache.go#L135" rel="noopener noreferrer"&gt;Cache Options&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  ReaderFailOnMissingInformer
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://github.com/kubernetes-sigs/controller-runtime/blob/4cf9db06e3b06be9c57ced5d8b705339b816765d/pkg/cache/cache.go#L179" rel="noopener noreferrer"&gt;ReaderFailOnMissingInformer&lt;/a&gt; is an option that prevents the dynamic start-up of Informers through Get/List actions. By looking at the &lt;a href="https://github.com/kubernetes-sigs/controller-runtime/blob/bc35946fac1b56071def3ebf299996b281c77442/pkg/cache/informer_cache.go#L181" rel="noopener noreferrer"&gt;getInformerForKind method of informerCache&lt;/a&gt;, we can see that it will return an error if there is no corresponding Informer for an Object's GVK. You can preemptively start Informers either by calling Cache's GetInformer method through Builder's For, Watches, and Owns methods, or by directly invoking Cache's GetInformer method.&lt;/p&gt;

&lt;h3&gt;
  
  
  DefaultNamespaces
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://github.com/kubernetes-sigs/controller-runtime/blob/4cf9db06e3b06be9c57ced5d8b705339b816765d/pkg/cache/cache.go#L192" rel="noopener noreferrer"&gt;DefaultNamespaces&lt;/a&gt; is an option for using multiNamespaceCache to specify Informer behavior per namespace. To see what options can be specified with DefaultNamespaces, check out the &lt;a href="https://github.com/kubernetes-sigs/controller-runtime/blob/4cf9db06e3b06be9c57ced5d8b705339b816765d/pkg/cache/cache.go#L278" rel="noopener noreferrer"&gt;Config&lt;/a&gt;. Let's consider a few edge cases when DefaultNamespaces is specified.&lt;/p&gt;

&lt;h4&gt;
  
  
  Get/List for Cluster-scoped Object
&lt;/h4&gt;

&lt;p&gt;Upon reviewing the &lt;a href="https://github.com/kubernetes-sigs/controller-runtime/blob/6dd03ed1f07cf03b34d088b9056082198039e328/pkg/cache/multi_namespace_cache.go#L226" rel="noopener noreferrer"&gt;Get method of multiNamespaceCache&lt;/a&gt;, it is clear that if an Object is cluster-scoped, then it calls the Get of &lt;code&gt;clusterCache&lt;/code&gt;. The &lt;code&gt;clusterCache&lt;/code&gt; is an &lt;a href="https://github.com/kubernetes-sigs/controller-runtime/blob/6dd03ed1f07cf03b34d088b9056082198039e328/pkg/cache/multi_namespace_cache.go#L54" rel="noopener noreferrer"&gt;informerCache without namespace restriction&lt;/a&gt;&lt;sup id="fnref5"&gt;5&lt;/sup&gt;. This means that even if DefaultNamespaces is specified, Informers for cluster-scoped Objects can be started using &lt;code&gt;clusterCache&lt;/code&gt;, and Get/List operations can be performed.&lt;/p&gt;

&lt;h4&gt;
  
  
  Get/List for Objects Outside the Specified Namespace
&lt;/h4&gt;

&lt;p&gt;When we revisit the &lt;a href="https://github.com/kubernetes-sigs/controller-runtime/blob/6dd03ed1f07cf03b34d088b9056082198039e328/pkg/cache/multi_namespace_cache.go#L226" rel="noopener noreferrer"&gt;Get method of multiNamespaceCache&lt;/a&gt;, the first step is to search &lt;code&gt;namespaceToCache&lt;/code&gt; using the namespace of the provided Object. If a Cache is not found, it searches &lt;code&gt;namespaceToCache&lt;/code&gt; again using &lt;code&gt;metav1.NamespaceAll&lt;/code&gt; (=""). This indicates that for Get/List of Objects outside the specified Namespace, if the empty string namespace is specified in DefaultNamespaces, informerCache is utilized; otherwise, an error is returned.&lt;/p&gt;

&lt;h3&gt;
  
  
  DefaultTransform
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://github.com/kubernetes-sigs/controller-runtime/blob/4cf9db06e3b06be9c57ced5d8b705339b816765d/pkg/cache/cache.go#L207" rel="noopener noreferrer"&gt;DefaultTransform&lt;/a&gt; specifies the default Transform function to pass to Informers. The Transform function allows modifications to be made to an Object before it is processed by the Informer. A common use case includes deleting fields from an Object (such as Metadata) to reduce memory consumption&lt;sup id="fnref6"&gt;6&lt;/sup&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  DefaultUnsafeDisableDeepCopy
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://github.com/kubernetes-sigs/controller-runtime/blob/4cf9db06e3b06be9c57ced5d8b705339b816765d/pkg/cache/cache.go#L223" rel="noopener noreferrer"&gt;DefaultUnsafeDisableDeepCopy&lt;/a&gt; is an option to conserve memory usage when calling Get/List by allowing the CacheReader to return Kubernetes Objects cached by the Informer without performing a DeepCopy. As stated in the &lt;a href="https://github.com/kubernetes-sigs/controller-runtime/blob/4cf9db06e3b06be9c57ced5d8b705339b816765d/pkg/cache/cache.go#L218-L219" rel="noopener noreferrer"&gt;comment&lt;/a&gt;, enabling this option requires you to perform a DeepCopy when making changes to the Object. This option is unlikely to be enabled unless there are special circumstances, such as listing a large number of Objects in a single reconcile.&lt;/p&gt;

&lt;h3&gt;
  
  
  ByObject
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://github.com/kubernetes-sigs/controller-runtime/blob/4cf9db06e3b06be9c57ced5d8b705339b816765d/pkg/cache/cache.go#L227" rel="noopener noreferrer"&gt;ByObject&lt;/a&gt; is an option to use delegatingByGVKCache to specify the behavior of Informers for each GVK. The available options that can be specified with ByObject can be found in the &lt;a href="https://github.com/kubernetes-sigs/controller-runtime/blob/4cf9db06e3b06be9c57ced5d8b705339b816765d/pkg/cache/cache.go#L234" rel="noopener noreferrer"&gt;ByObject struct&lt;/a&gt;. This option is similar to DefaultNamespaces, but the behavior is different when an Object with a GVK not specified by ByObject is passed to Get/List.&lt;/p&gt;

&lt;p&gt;Unlike DefaultNamespaces, which returns an error for most unspecified Namespace Get/List operations, the &lt;a href="https://github.com/kubernetes-sigs/controller-runtime/blob/f4dbd14c2db27a4f5a700c1c0e96dc0a0b18447f/pkg/cache/delegating_by_gvk_cache.go#L129" rel="noopener noreferrer"&gt;delegatingByGVKCache's cacheForGVK method&lt;/a&gt; returns &lt;code&gt;dbt.defaultCache&lt;/code&gt; when the provided GVK is not in &lt;code&gt;dbt.caches&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;defaultCache&lt;/code&gt; will be either a multiNamespaceCache or an informerCache depending on &lt;a href="https://github.com/kubernetes-sigs/controller-runtime/blob/4cf9db06e3b06be9c57ced5d8b705339b816765d/pkg/cache/cache.go#L316-L321" rel="noopener noreferrer"&gt;other options&lt;/a&gt;. Thus, with ByObject, if an Object with an unspecified GVK is provided, an error typically does not return, and an Informer may be dynamically started.&lt;/p&gt;

&lt;p&gt;In addition to the above, there are other Cache Options such as &lt;a href="https://github.com/kubernetes-sigs/controller-runtime/blob/4cf9db06e3b06be9c57ced5d8b705339b816765d/pkg/cache/cache.go#L170C2-L170C12" rel="noopener noreferrer"&gt;SyncPeriod&lt;/a&gt;, &lt;a href="https://github.com/kubernetes-sigs/controller-runtime/blob/4cf9db06e3b06be9c57ced5d8b705339b816765d/pkg/cache/cache.go#L196" rel="noopener noreferrer"&gt;DefaultLabelSelector&lt;/a&gt;, &lt;a href="https://github.com/kubernetes-sigs/controller-runtime/blob/4cf9db06e3b06be9c57ced5d8b705339b816765d/pkg/cache/cache.go#L200C2-L200C22" rel="noopener noreferrer"&gt;DefaultFieldSelector&lt;/a&gt;, as well as the &lt;a href="https://github.com/kubernetes-sigs/controller-runtime/blob/4cf9db06e3b06be9c57ced5d8b705339b816765d/pkg/cache/cache.go#L278" rel="noopener noreferrer"&gt;Config from DefaultNamespaces&lt;/a&gt; and the &lt;a href="https://github.com/kubernetes-sigs/controller-runtime/blob/4cf9db06e3b06be9c57ced5d8b705339b816765d/pkg/cache/cache.go#L234" rel="noopener noreferrer"&gt;ByObject option&lt;/a&gt;. However, we won't delve into these here as their behaviors can be easily understood from the discussion so far.&lt;/p&gt;




&lt;ol&gt;

&lt;li id="fn1"&gt;
&lt;p&gt;The initialized Cache is &lt;a href="https://github.com/kubernetes-sigs/controller-runtime/blob/a6b9c0b672c77a79fff4d5bc03221af1e1fe21fa/pkg/cluster/cluster.go#L254" rel="noopener noreferrer"&gt;set to the cluster's cache field&lt;/a&gt;, from where it is &lt;a href="https://github.com/kubernetes-sigs/controller-runtime/blob/4fd4f6e7116453ea4e07a2cdd8ba0d38ad64680f/pkg/cluster/internal.go#L104" rel="noopener noreferrer"&gt;started through the cluster's Start method&lt;/a&gt;. The cluster itself is &lt;a href="https://github.com/kubernetes-sigs/controller-runtime/blob/58719057609722d090ac4ee3f1866e5958d16e6f/pkg/manager/internal.go#L356" rel="noopener noreferrer"&gt;added as a Runnable to the controllerManager's runnables&lt;/a&gt; and started. The concrete implementation of the Start method held by the Cache interface is the &lt;a href="https://github.com/kubernetes-sigs/controller-runtime/blob/6dd03ed1f07cf03b34d088b9056082198039e328/pkg/cache/internal/informers.go#L189" rel="noopener noreferrer"&gt;Informers' Start method&lt;/a&gt; for the informerCache, whereas &lt;a href="https://github.com/kubernetes-sigs/controller-runtime/blob/6dd03ed1f07cf03b34d088b9056082198039e328/pkg/cache/multi_namespace_cache.go#L165" rel="noopener noreferrer"&gt;multiNamespaceCache&lt;/a&gt; and &lt;a href="https://github.com/kubernetes-sigs/controller-runtime/blob/f4dbd14c2db27a4f5a700c1c0e96dc0a0b18447f/pkg/cache/delegating_by_gvk_cache.go#L75" rel="noopener noreferrer"&gt;delegatingByGVKCache&lt;/a&gt; each have their own distinct Start methods. ↩&lt;/p&gt;
&lt;/li&gt;

&lt;li id="fn2"&gt;
&lt;p&gt;The &lt;a href="https://github.com/kubernetes-sigs/controller-runtime/blob/4cf9db06e3b06be9c57ced5d8b705339b816765d/pkg/cache/cache.go#L65" rel="noopener noreferrer"&gt;Cache&lt;/a&gt; itself also implements the Start method through the &lt;a href="https://github.com/kubernetes-sigs/controller-runtime/blob/4cf9db06e3b06be9c57ced5d8b705339b816765d/pkg/cache/cache.go#L76" rel="noopener noreferrer"&gt;Informers interface&lt;/a&gt;, so it can be &lt;a href="https://github.com/kubernetes-sigs/controller-runtime/blob/58719057609722d090ac4ee3f1866e5958d16e6f/pkg/manager/internal.go#L172" rel="noopener noreferrer"&gt;directly added to the controllerManager as a Runnable&lt;/a&gt;. By creating a Client using this Cache, you can utilize a Client with Cache Options that are completely different from those of the controllerManager. ↩&lt;/p&gt;
&lt;/li&gt;

&lt;li id="fn3"&gt;
&lt;p&gt;The multiNamespaceCache is also employed when the &lt;a href="https://github.com/kubernetes-sigs/controller-runtime/blob/4cf9db06e3b06be9c57ced5d8b705339b816765d/pkg/cache/cache.go#L460" rel="noopener noreferrer"&gt;DefaultNamespaces option is specified&lt;/a&gt;. As noted in the &lt;a href="https://github.com/kubernetes-sigs/controller-runtime/blob/4cf9db06e3b06be9c57ced5d8b705339b816765d/pkg/cache/cache.go#L247" rel="noopener noreferrer"&gt;comment&lt;/a&gt;, to avoid using the DefaultNamespaces option, one must specify an empty list in the Namespaces option of ByObject. ↩&lt;/p&gt;
&lt;/li&gt;

&lt;li id="fn4"&gt;
&lt;p&gt;For more details about client-go's Informer and Indexer, refer to other documents like &lt;a href="https://github.com/kubernetes/sample-controller/blob/master/docs/controller-client-go.md" rel="noopener noreferrer"&gt;client-go under the hood&lt;/a&gt;. ↩&lt;/p&gt;
&lt;/li&gt;

&lt;li id="fn5"&gt;
&lt;p&gt;The globalConfig, when the multiNamespaceCache is generated from DefaultNamespaces, is produced using the &lt;a href="https://github.com/kubernetes-sigs/controller-runtime/blob/4cf9db06e3b06be9c57ced5d8b705339b816765d/pkg/cache/cache.go#L364" rel="noopener noreferrer"&gt;optionDefaultsToConfig function&lt;/a&gt;, but when the multiNamespaceCache is created through the delegatingByGVKCache originating from ByObject, it results in &lt;a href="https://github.com/kubernetes-sigs/controller-runtime/blob/4cf9db06e3b06be9c57ced5d8b705339b816765d/pkg/cache/cache.go#L340" rel="noopener noreferrer"&gt;nil&lt;/a&gt;. ↩&lt;/p&gt;
&lt;/li&gt;

&lt;li id="fn6"&gt;
&lt;p&gt;Another method to limit memory usage is by using &lt;a href="https://github.com/kubernetes/apimachinery/blob/d5c9711b77ee5a0dde0fef41c9ca86a67f5ddb4e/pkg/apis/meta/v1/types.go#L1496" rel="noopener noreferrer"&gt;PartialObjectMetadata&lt;/a&gt;. By utilizing PartialObjectMetadata, only the metadata of an Object is cached, which can help to reduce the amount of memory used. ↩&lt;/p&gt;
&lt;/li&gt;

&lt;/ol&gt;

</description>
      <category>kubernetes</category>
      <category>kubebuilder</category>
      <category>operator</category>
    </item>
    <item>
      <title>Kubernetes Event Aggregation and Spam Filtering in client-go</title>
      <dc:creator>Shuhei Kitagawa</dc:creator>
      <pubDate>Mon, 17 Apr 2023 23:09:56 +0000</pubDate>
      <link>https://dev.to/shuheiktgw/kubernetes-event-aggregation-and-spam-filtering-in-client-go-25mn</link>
      <guid>https://dev.to/shuheiktgw/kubernetes-event-aggregation-and-spam-filtering-in-client-go-25mn</guid>
      <description>&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://github.com/kubernetes/client-go" rel="noopener noreferrer"&gt;clint-go&lt;/a&gt; has features for event aggregation, which groups similar events into one, and spam filtering, which applies rate limits to events.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/kubernetes/client-go/blob/2a6c116e406126324eee341e874612a5093bdbb0/tools/record/events_cache.go#L242" rel="noopener noreferrer"&gt;Event aggregation&lt;/a&gt; uses an aggregation key generated by the &lt;a href="https://github.com/kubernetes/client-go/blob/2a6c116e406126324eee341e874612a5093bdbb0/tools/record/events_cache.go#L165" rel="noopener noreferrer"&gt;EventAggregatorByReasonFunc&lt;/a&gt; as a key, and if the same event is published 10 or more times within 10 minutes, instead of posting a new event, update the existing to save etcd capacity.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/kubernetes/client-go/blob/2a6c116e406126324eee341e874612a5093bdbb0/tools/record/events_cache.go#L129" rel="noopener noreferrer"&gt;Spam filtering&lt;/a&gt; performs using the value generated by the &lt;a href="https://github.com/kubernetes/client-go/blob/2a6c116e406126324eee341e874612a5093bdbb0/tools/record/events_cache.go#L71" rel="noopener noreferrer"&gt;getSpamKey function&lt;/a&gt; as a key. The spam filter uses a rate limiter based on the token bucket algorithm, with an initial 25 tokens and a refill rate of 1 token per 5 minutes. If an event is published beyond the limit, the event is discarded.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Background
&lt;/h2&gt;

&lt;p&gt;When I wanted to create a CustomController that detects the completion of a child Job of a CronJob and performs an action, I noticed that CronJobController (v2) published an Event called &lt;a href="https://github.com/kubernetes/kubernetes/blob/1b825c179baad213bb570787f719d74211be3bcf/pkg/controller/cronjob/cronjob_controllerv2.go#L452" rel="noopener noreferrer"&gt;SawCompletedJob&lt;/a&gt; when the child Job completed. I thought, "Oh, I can simply use the Event as a cue."&lt;/p&gt;

&lt;p&gt;However, when I created a CronJob that ran once a minute in a local cluster (kind), I discovered that a SawCompletedJob event became unobservable from the middle of the run (about the 10th run). Specifically, the Events looked like the ones below.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ kubectl alpha events --for cronjob/hello
...
8m29s (x2 over 8m29s)   Normal   SawCompletedJob    CronJob/hello   Saw completed job: hello-28023907, status: Complete
8m29s                   Normal   SuccessfulDelete   CronJob/hello   Deleted job hello-28023904
7m35s                   Normal   SuccessfulCreate   CronJob/hello   Created job hello-28023908
7m28s (x2 over 7m28s)   Normal   SawCompletedJob    CronJob/hello   Saw completed job: hello-28023908, status: Complete
7m28s                   Normal   SuccessfulDelete   CronJob/hello   Deleted job hello-28023905
6m35s                   Normal   SuccessfulCreate   CronJob/hello   Created job hello-28023909
6m28s                   Normal   SawCompletedJob    CronJob/hello   Saw completed job: hello-28023909, status: Complete
2m35s (x3 over 4m35s)   Normal   SuccessfulCreate   CronJob/hello   (combined from similar events): Created job hello-28023913
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;SawCompletedJob events were published until 6m28s, but they became unobservable after that. SuccessfulCreate events were published in an aggregated form, but we cannot see all of them. By the way, all events from child Jobs and child Pods are observable.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ kubectl alpha events

4m18s                 Normal    Scheduled                 Pod/hello-28023914-frb94   Successfully assigned default/hello-28023914-frb94 to kind-control-plane
4m11s                 Normal    Completed                 Job/hello-28023914         Job completed
3m18s                 Normal    Started                   Pod/hello-28023915-5fsh5   Started container hello
3m18s                 Normal    SuccessfulCreate          Job/hello-28023915         Created pod: hello-28023915-5fsh5
3m18s                 Normal    Created                   Pod/hello-28023915-5fsh5   Created container hello
3m18s                 Normal    Pulled                    Pod/hello-28023915-5fsh5   Container image "busybox:1.28" already present on machine
3m18s                 Normal    Scheduled                 Pod/hello-28023915-5fsh5   Successfully assigned default/hello-28023915-5fsh5 to kind-control-plane
3m11s                 Normal    Completed                 Job/hello-28023915         Job completed
2m18s                 Normal    Started                   Pod/hello-28023916-qbqqk   Started container hello
2m18s                 Normal    Pulled                    Pod/hello-28023916-qbqqk   Container image "busybox:1.28" already present on machine
2m18s                 Normal    Created                   Pod/hello-28023916-qbqqk   Created container hello
2m18s                 Normal    SuccessfulCreate          Job/hello-28023916         Created pod: hello-28023916-qbqqk
2m18s                 Normal    Scheduled                 Pod/hello-28023916-qbqqk   Successfully assigned default/hello-28023916-qbqqk to kind-control-plane
2m11s                 Normal    Completed                 Job/hello-28023916         Job completed
78s                   Normal    SuccessfulCreate          Job/hello-28023917         Created pod: hello-28023917-kpxvn
78s                   Normal    Created                   Pod/hello-28023917-kpxvn   Created container hello
78s                   Normal    Pulled                    Pod/hello-28023917-kpxvn   Container image "busybox:1.28" already present on machine
78s                   Normal    Started                   Pod/hello-28023917-kpxvn   Started container hello
78s                   Normal    Scheduled                 Pod/hello-28023917-kpxvn   Successfully assigned default/hello-28023917-kpxvn to kind-control-plane
71s                   Normal    Completed                 Job/hello-28023917         Job completed
18s (x8 over 7m18s)   Normal    SuccessfulCreate          CronJob/hello              (combined from similar events): Created job hello-28023918
18s                   Normal    Started                   Pod/hello-28023918-grvbz   Started container hello
18s                   Normal    Created                   Pod/hello-28023918-grvbz   Created container hello
18s                   Normal    Pulled                    Pod/hello-28023918-grvbz   Container image "busybox:1.28" already present on machine
18s                   Normal    SuccessfulCreate          Job/hello-28023918         Created pod: hello-28023918-grvbz
18s                   Normal    Scheduled                 Pod/hello-28023918-grvbz   Successfully assigned default/hello-28023918-grvbz to kind-control-plane
11s                   Normal    Completed                 Job/hello-28023918         Job completed
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;As I couldn't find any specific description of this behavior in the Kubernetes official documentation, I investigated it by reading the source code to figure out what was happening.&lt;/p&gt;

&lt;h2&gt;
  
  
  Kubernetes Event Publication Flow
&lt;/h2&gt;

&lt;p&gt;client-go sends Kubernetes Events to kube-apiserver through and etcd stores them. client-go is responsible for event aggregation and spam filtering, but the flow of client-go sending Events to kube-apiserver is quite complex, so we will explain it first. Note that we will use the SawCompleteJob Event of CronJobController as an example, but please note that the details may vary on each controller.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fsacw777gmi2ghdt354f8.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fsacw777gmi2ghdt354f8.png" alt="client-go Kubernetes Event publication flow"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;CronJobController publishes an Event via the Recorder's &lt;a href="https://github.com/kubernetes/kubernetes/blob/1b825c179baad213bb570787f719d74211be3bcf/pkg/controller/cronjob/cronjob_controllerv2.go#L452" rel="noopener noreferrer"&gt;Eventf method&lt;/a&gt;. The method internally calls the Broadcaster's &lt;a href="https://github.com/kubernetes/apimachinery/blob/40ea93bb2f462f29e5af2018b7380683788a20c2/pkg/watch/mux.go#L231" rel="noopener noreferrer"&gt;ActionOrDrop method&lt;/a&gt; and sends the Event to the incoming channel.&lt;/li&gt;
&lt;li&gt;The Event in the incoming channel is retrieved by &lt;a href="https://github.com/kubernetes/apimachinery/blob/40ea93bb2f462f29e5af2018b7380683788a20c2/pkg/watch/mux.go#L264-L277" rel="noopener noreferrer"&gt;the loop goroutine&lt;/a&gt; and &lt;a href="https://github.com/kubernetes/apimachinery/blob/40ea93bb2f462f29e5af2018b7380683788a20c2/pkg/watch/mux.go#L281-L288" rel="noopener noreferrer"&gt;forwarded to each Watcher's result channel&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;The Event Watcher goroutine calls &lt;a href="https://github.com/kubernetes/client-go/blob/f6a5a1f1391b9b6ceaaa499bd7cebf508d5e02a6/tools/record/event.go#L327" rel="noopener noreferrer"&gt;the eventHandler&lt;/a&gt; for the Event received from the result channel. The eventHandler calls &lt;a href="https://github.com/kubernetes/client-go/blob/f6a5a1f1391b9b6ceaaa499bd7cebf508d5e02a6/tools/record/event.go#L199-L201" rel="noopener noreferrer"&gt;recordToSink in the eventBroadcasterImpl&lt;/a&gt;, where EventCorrelator performs event aggregation and spam filtering and then calls &lt;a href="https://github.com/kubernetes/client-go/blob/f6a5a1f1391b9b6ceaaa499bd7cebf508d5e02a6/tools/record/event.go#L251" rel="noopener noreferrer"&gt;recordEvent&lt;/a&gt; to post an Event (or update the Event if it has been aggregated).&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Note: Starting loop goroutine
&lt;/h3&gt;

&lt;p&gt;The loop goroutine starts through &lt;a href="https://github.com/kubernetes/apimachinery/blob/40ea93bb2f462f29e5af2018b7380683788a20c2/pkg/watch/mux.go#L84-L95" rel="noopener noreferrer"&gt;the NewLongQueueBroadcaster function&lt;/a&gt;, which is called through &lt;a href="https://github.com/kubernetes/client-go/blob/f6a5a1f1391b9b6ceaaa499bd7cebf508d5e02a6/tools/record/event.go#L161-L164" rel="noopener noreferrer"&gt;the NewBroadcaster function&lt;/a&gt;. The NewBroadcaster function is called in &lt;a href="https://github.com/kubernetes/kubernetes/blob/1b825c179baad213bb570787f719d74211be3bcf/pkg/controller/cronjob/cronjob_controllerv2.go#L85" rel="noopener noreferrer"&gt;the NewControllerV2 function&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Note: Starting Event Watcher
&lt;/h3&gt;

&lt;p&gt;The CronJobController calls &lt;a href="https://github.com/kubernetes/kubernetes/blob/1b825c179baad213bb570787f719d74211be3bcf/pkg/controller/cronjob/cronjob_controllerv2.go#L133" rel="noopener noreferrer"&gt;the StartRecordingToSink method&lt;/a&gt; of eventBroadcasterImpl, which starts the Event Watcher from &lt;a href="https://github.com/kubernetes/client-go/blob/f6a5a1f1391b9b6ceaaa499bd7cebf508d5e02a6/tools/record/event.go#L198-L201" rel="noopener noreferrer"&gt;the StartEventWatcher method&lt;/a&gt;. The StartEventWatcher method initializes and registers the Watcher through &lt;a href="https://github.com/kubernetes/apimachinery/blob/40ea93bb2f462f29e5af2018b7380683788a20c2/pkg/watch/mux.go#L141-L158" rel="noopener noreferrer"&gt;the Watch method&lt;/a&gt; of the Broadcaster. What I found interesting is that &lt;a href="https://github.com/kubernetes/apimachinery/blob/40ea93bb2f462f29e5af2018b7380683788a20c2/pkg/watch/mux.go#L144-L152" rel="noopener noreferrer"&gt;the registration process of the Watcher itself&lt;/a&gt; is sent to the incoming channel, and &lt;a href="https://github.com/kubernetes/apimachinery/blob/40ea93bb2f462f29e5af2018b7380683788a20c2/pkg/watch/mux.go#L269-L272" rel="noopener noreferrer"&gt;the loop goroutine executes it&lt;/a&gt;, making the events published before the start of the Watcher invisible to it (&lt;a href="https://github.com/kubernetes/apimachinery/blob/40ea93bb2f462f29e5af2018b7380683788a20c2/pkg/watch/mux.go#L113-L116" rel="noopener noreferrer"&gt;the comment&lt;/a&gt; calls it as a "terrible hack" though).&lt;/p&gt;

&lt;h2&gt;
  
  
  EventCorrelator
&lt;/h2&gt;

&lt;p&gt;EventCorrelator implements the core logic for Kubernetes Event aggregation and spam filtering. EventCorrelator is initialized through the NewEventCorrelatorWithOptions function called in &lt;a href="https://github.com/kubernetes/client-go/blob/f6a5a1f1391b9b6ceaaa499bd7cebf508d5e02a6/tools/record/event.go#L197" rel="noopener noreferrer"&gt;the StartRecordingToSink method&lt;/a&gt; of eventBroadcasterImpl. Note that e.options is empty, so the Controller uses &lt;a href="https://github.com/kubernetes/client-go/blob/2a6c116e406126324eee341e874612a5093bdbb0/tools/record/events_cache.go#L457" rel="noopener noreferrer"&gt;the default values&lt;/a&gt;. eventBroadcasterImpl's &lt;a href="https://github.com/kubernetes/client-go/blob/f6a5a1f1391b9b6ceaaa499bd7cebf508d5e02a6/tools/record/event.go#L209" rel="noopener noreferrer"&gt;recordToSink method&lt;/a&gt; calls &lt;a href="https://github.com/kubernetes/client-go/blob/2a6c116e406126324eee341e874612a5093bdbb0/tools/record/events_cache.go#L510" rel="noopener noreferrer"&gt;EventCorrelate method&lt;/a&gt;, which aggregates Events and applies tge spam filter.&lt;/p&gt;

&lt;h3&gt;
  
  
  Aggregation
&lt;/h3&gt;

&lt;p&gt;The EventCorrelator's &lt;a href="https://github.com/kubernetes/client-go/blob/2a6c116e406126324eee341e874612a5093bdbb0/tools/record/events_cache.go#L242" rel="noopener noreferrer"&gt;EventAggregate method&lt;/a&gt; and &lt;a href="https://github.com/kubernetes/client-go/blob/2a6c116e406126324eee341e874612a5093bdbb0/tools/record/events_cache.go#L327" rel="noopener noreferrer"&gt;the eventObserve method&lt;/a&gt; of eventLogger are used for Event aggregation. The source code has detailed comments, so it's recommended to refer to it directly for more information, but here's a brief overview of the process:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Calculate aggregationKey and localKey using &lt;a href="https://github.com/kubernetes/client-go/blob/2a6c116e406126324eee341e874612a5093bdbb0/tools/record/events_cache.go#L165" rel="noopener noreferrer"&gt;EventAggregatorByReasonFunc&lt;/a&gt;. aggregationKey consists of  event.Source, event.InvolvedObject, event.Type, event.Reason, event.ReportingController, and event.ReportingInstance, while localKey is event.Message.&lt;/li&gt;
&lt;li&gt;Search the cache of EventAggregator using the the aggregationKey. The cache value is &lt;a href="https://github.com/kubernetes/client-go/blob/2a6c116e406126324eee341e874612a5093bdbb0/tools/record/events_cache.go#L227" rel="noopener noreferrer"&gt;aggregateRecord&lt;/a&gt;. If the number of the localKeys within the maxIntervalInSeconds (&lt;a href="https://github.com/kubernetes/client-go/blob/2a6c116e406126324eee341e874612a5093bdbb0/tools/record/events_cache.go#L42" rel="noopener noreferrer"&gt;default 600 seconds&lt;/a&gt;) is greater than or equal to the maxEvents (&lt;a href="https://github.com/kubernetes/client-go/blob/2a6c116e406126324eee341e874612a5093bdbb0/tools/record/events_cache.go#L41" rel="noopener noreferrer"&gt;default 10&lt;/a&gt;), return aggregationKey as the key, otherwise return &lt;a href="https://github.com/kubernetes/client-go/blob/2a6c116e406126324eee341e874612a5093bdbb0/tools/record/events_cache.go#L246" rel="noopener noreferrer"&gt;the eventKey&lt;/a&gt; as the key. Note that the eventKey should be unique to each Event.&lt;/li&gt;
&lt;li&gt;Call &lt;a href="https://github.com/kubernetes/client-go/blob/2a6c116e406126324eee341e874612a5093bdbb0/tools/record/events_cache.go#L327" rel="noopener noreferrer"&gt;the eventObserve method&lt;/a&gt; of eventLogger with the key returned from the EventCorrelate method, and search the cache of eventLogger. The cache value is &lt;a href="https://github.com/kubernetes/client-go/blob/2a6c116e406126324eee341e874612a5093bdbb0/tools/record/events_cache.go#L300" rel="noopener noreferrer"&gt;eventLog&lt;/a&gt;. If it hits the cache (i.e., the key is aggregationKey), compute the patch to update the Event.&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Spam Filtering
&lt;/h3&gt;

&lt;p&gt;For spam filtering, &lt;a href="https://github.com/kubernetes/client-go/blob/2a6c116e406126324eee341e874612a5093bdbb0/tools/record/events_cache.go#L516" rel="noopener noreferrer"&gt;the filterFunc of EventCorrelator is called&lt;/a&gt; to apply it. The actual implementation of filterFunc is &lt;a href="https://github.com/kubernetes/client-go/blob/2a6c116e406126324eee341e874612a5093bdbb0/tools/record/events_cache.go#L129" rel="noopener noreferrer"&gt;the Filter method&lt;/a&gt; of EventSourceObjectSpamFilter. Again, it's recommended to refer to the source code for details, but here's a brief overview of the process:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Calculate the eventKey from the Event using &lt;a href="https://github.com/kubernetes/client-go/blob/2a6c116e406126324eee341e874612a5093bdbb0/tools/record/events_cache.go#L71" rel="noopener noreferrer"&gt;the getSpamKey function&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;Search the cache of EventSourceObjectSpamFilter using the eventKey. The cache value is &lt;a href="https://github.com/kubernetes/client-go/blob/2a6c116e406126324eee341e874612a5093bdbb0/tools/record/events_cache.go#L123" rel="noopener noreferrer"&gt;spamRecord&lt;/a&gt;, which contains &lt;a href="https://github.com/kubernetes/client-go/blob/2a6c116e406126324eee341e874612a5093bdbb0/tools/record/events_cache.go#L145" rel="noopener noreferrer"&gt;the rate limiter&lt;/a&gt;. &lt;a href="https://github.com/kubernetes/client-go/blob/2a6c116e406126324eee341e874612a5093bdbb0/tools/record/events_cache.go#L441" rel="noopener noreferrer"&gt;The default values for qps and burst of the rate limiter&lt;/a&gt; are &lt;a href="https://github.com/kubernetes/client-go/blob/2a6c116e406126324eee341e874612a5093bdbb0/tools/record/events_cache.go#L49" rel="noopener noreferrer"&gt;1/300&lt;/a&gt; and &lt;a href="https://github.com/kubernetes/client-go/blob/2a6c116e406126324eee341e874612a5093bdbb0/tools/record/events_cache.go#L48" rel="noopener noreferrer"&gt;25&lt;/a&gt;, respectively. According to &lt;a href="https://github.com/kubernetes/client-go/blob/b9fa896d5ded5a18c5c84b3f6e80959c303dd05c/util/flowcontrol/throttle.go#L61-L62" rel="noopener noreferrer"&gt;the comment&lt;/a&gt;, the rate limiter uses the token bucket algorithm, so there are initially 25 tokens, and then one token is refilled every 5 minutes.&lt;/li&gt;
&lt;li&gt;Call &lt;a href="https://github.com/kubernetes/client-go/blob/b9fa896d5ded5a18c5c84b3f6e80959c303dd05c/util/flowcontrol/throttle.go#L120" rel="noopener noreferrer"&gt;the TryAccept method&lt;/a&gt; of tokenBucketPassiveRateLimiter to check the rate limit. If it exceeds it, &lt;a href="https://github.com/kubernetes/client-go/blob/f6a5a1f1391b9b6ceaaa499bd7cebf508d5e02a6/tools/record/event.go#L218-L220" rel="noopener noreferrer"&gt;discard the Event&lt;/a&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Why did SawCompletedJob Event become unobservable?
&lt;/h2&gt;

&lt;p&gt;Taking the above into consideration, let's think about why the SawCompleteJob Event became unobservable. In short, it is likely due to be caused by the spam filter.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;CronJobController issues three Events, SuccessfulCreate, SawCompletedJob, and SuccessfulDelete, per child Job every minute (Strictly speaking, it publishes SuccessfulDelete only when it reaches the HistoryLimit).&lt;/li&gt;
&lt;li&gt;The Controller uses a spam filter whose the key is solely based on the Source and InvolvedObject (See &lt;a href="https://github.com/kubernetes/client-go/blob/2a6c116e406126324eee341e874612a5093bdbb0/tools/record/events_cache.go#L71" rel="noopener noreferrer"&gt;getSpamKey function&lt;/a&gt;). Therefore, these thee types of Events are identified as the same Event.&lt;/li&gt;
&lt;li&gt;The Controller consumed the first 25 tokens at a rate of three tokens per minute. One token is refilled every five minutes, but around nine minutes, the tokens started running out. After that, A toke was refilled every five minutes, but it was consumed by a (aggregated) SuccessfulCreate Event, so SawCompletedJob and SuccessfulDelete were never published thereafter.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Note: Event Types
&lt;/h3&gt;

&lt;p&gt;I'll list &lt;code&gt;SuccessfulCreate&lt;/code&gt;、 &lt;code&gt;SawCompletedJob&lt;/code&gt;、&lt;code&gt;SuccessfulDelete&lt;/code&gt; events' involvedObject and source below.&lt;/p&gt;

&lt;h4&gt;
  
  
  SuccessfulCreate Event
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;apiVersion: v1
kind: Event
involvedObject:
  apiVersion: batch/v1
  kind: CronJob
  name: hello
  namespace: default
  resourceVersion: "289520"
  uid: 5f3cfeca-8a83-452a-beb9-7a5f9c1eff63
source:
  component: cronjob-controller
...
reason: SuccessfulCreate
message: Created job hello-28025408
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  SawCompletedJob Event
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;apiVersion: v1
kind: Event
involvedObject:
  apiVersion: batch/v1
  kind: CronJob
  name: hello
  namespace: default
  resourceVersion: "289020"
  uid: 5f3cfeca-8a83-452a-beb9-7a5f9c1eff63
source:
  component: cronjob-controller
...
reason: SawCompletedJob
message: 'Saw completed job: hello-28025408, status: Complete'
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  SuccessfulDelete Event
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;apiVersion: v1
kind: Event
involvedObject:
  apiVersion: batch/v1
  kind: CronJob
  name: hello
  namespace: default
  resourceVersion: "289520"
  uid: 5f3cfeca-8a83-452a-beb9-7a5f9c1eff63
source:
  component: cronjob-controller
...
reason: SuccessfulDelete
message: Deleted job hello-28025408
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



</description>
      <category>kubernetes</category>
      <category>cloudnative</category>
      <category>go</category>
    </item>
    <item>
      <title>Released new Travis CI API Go client, shuheiktgw/go-travis</title>
      <dc:creator>Shuhei Kitagawa</dc:creator>
      <pubDate>Mon, 05 Nov 2018 01:35:46 +0000</pubDate>
      <link>https://dev.to/shuheiktgw/released-new-travis-ci-api-go-client-shuheiktgwgo-travis-a11</link>
      <guid>https://dev.to/shuheiktgw/released-new-travis-ci-api-go-client-shuheiktgwgo-travis-a11</guid>
      <description>&lt;p&gt;I just released new Travis CI API client in Go, which is called &lt;a href="https://github.com/shuheiktgw/go-travis" rel="noopener noreferrer"&gt;shuheiktgw/go-travis&lt;/a&gt;, so let me introduce why I built it and how to use it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why I started developing shuheiktgw/go-travis
&lt;/h2&gt;

&lt;p&gt;Originally, I was just looking for a Travis CI API's client library in Go to kick a build from a CI environment, and found &lt;a href="https://github.com/Ableton/go-travis" rel="noopener noreferrer"&gt;Ableton/go-travis&lt;/a&gt;. Unfortunately, there are a few problems with the library.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;It does not seem to be maintained anymore.&lt;/li&gt;
&lt;li&gt;It uses API v2, whose latest version is v3.&lt;/li&gt;
&lt;li&gt;It only supports limited number of endpoints.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In short, I found a vacuum in the field of Travis CI API client in Go, and I was simply thrilled that I might be the first one to fill it. &lt;/p&gt;

&lt;p&gt;Also, I used Travis CI for my private projects a lot, so I thought building a Golang client library for them may be a good way to return a favor.&lt;/p&gt;

&lt;p&gt;Thant's how I forked &lt;a href="https://github.com/Ableton/go-travis" rel="noopener noreferrer"&gt;Ableton/go-travis&lt;/a&gt; and started developing &lt;a href="https://github.com/shuheiktgw/go-travis" rel="noopener noreferrer"&gt;shuheiktgw/go-travis&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  How it works
&lt;/h2&gt;

&lt;p&gt;The way shuheiktgw/go-travis works is deadly simple. Its implementation is heavily inspired by &lt;a href="https://github.com/google/go-github" rel="noopener noreferrer"&gt;google/go-github&lt;/a&gt; (as Ableton/go-travis does), so if you have used go-github, you can instantly understand how it works.&lt;/p&gt;

&lt;h3&gt;
  
  
  Install
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;go get github.com/shuheiktgw/go-travis
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Usage
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="n"&gt;client&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;travis&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NewClient&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;travis&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ApiComUrl&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"TravisApiToken"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c"&gt;// List all the builds which belongs to the current user&lt;/span&gt;
&lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Builds&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;List&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Background&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;travis.NewClient&lt;/code&gt; takes two arguments, &lt;code&gt;baseUrl&lt;/code&gt; and &lt;code&gt;travisToken&lt;/code&gt;. For &lt;code&gt;baseUrl&lt;/code&gt;, you need to choose either &lt;code&gt;travis.ApiComUrl&lt;/code&gt; or &lt;code&gt;travis.ApiOrgUrl&lt;/code&gt;, so please read &lt;a href="https://github.com/shuheiktgw/go-travis#url" rel="noopener noreferrer"&gt;URL section&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;travisToken&lt;/code&gt; can be empty, but then you probably need to authenticate via GitHub token. So please check out &lt;a href="https://github.com/shuheiktgw/go-travis#authentication" rel="noopener noreferrer"&gt;Authentication section&lt;/a&gt; too for more detailed information on authentication.&lt;/p&gt;

&lt;h2&gt;
  
  
  Features
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Supported features
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Supports all the endpoints of &lt;a href="https://developer.travis-ci.com/" rel="noopener noreferrer"&gt;Travis CI API v3&lt;/a&gt;! 🎉&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Unsupported features
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Does not support &lt;a href="https://developer.travis-ci.com/eager-loading#eager%20loading" rel="noopener noreferrer"&gt;Eager loading&lt;/a&gt; so far..&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That's all, and thank you for reading the post! If you have chance to use &lt;a href="https://github.com/shuheiktgw/go-travis" rel="noopener noreferrer"&gt;shuheiktgw/go-travis&lt;/a&gt;, I'd love to hear your feedbacks. Also, contributions to the library are, of course, always welcomed!&lt;/p&gt;

</description>
      <category>showdev</category>
      <category>go</category>
      <category>devops</category>
    </item>
  </channel>
</rss>
