<?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: Moep90</title>
    <description>The latest articles on DEV Community by Moep90 (@moep90).</description>
    <link>https://dev.to/moep90</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.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4190888%2Fe7f3930f-32f8-4bbe-8c61-0aef288e7cb5.png</url>
      <title>DEV Community: Moep90</title>
      <link>https://dev.to/moep90</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/moep90"/>
    <language>en</language>
    <item>
      <title>Kapitan generator releases, 19 to 25 October 2026</title>
      <dc:creator>Moep90</dc:creator>
      <pubDate>Sat, 07 Nov 2026 21:05:40 +0000</pubDate>
      <link>https://dev.to/kadet-hub/kapitan-generator-releases-19-to-25-october-2026-2imk</link>
      <guid>https://dev.to/kadet-hub/kapitan-generator-releases-19-to-25-october-2026-2imk</guid>
      <description>&lt;p&gt;Versions published on kadet-hub from 19 to 25 October 2026, each through the gate and signed. The generator page shows each version's output diff against the version before it, its SBOM and the command that verifies it.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Generator&lt;/th&gt;
&lt;th&gt;Versions&lt;/th&gt;
&lt;th&gt;Changes&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://kadet-hub.org/generators/cloudnative-pg/" rel="noopener noreferrer"&gt;cloudnative-pg&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;1.1.0, 1.1.1&lt;/td&gt;
&lt;td&gt;&lt;a href="https://github.com/kadet-hub/generators/blob/5e4335d164d811f9f896063c948cae3e5712ab5c/cloudnative-pg/CHANGELOG.md" rel="noopener noreferrer"&gt;CHANGELOG&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://kadet-hub.org/generators/external-secrets/" rel="noopener noreferrer"&gt;external-secrets&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;1.0.5&lt;/td&gt;
&lt;td&gt;&lt;a href="https://github.com/kadet-hub/generators/blob/ed27d4ba26d2a87d65a1631d8c62957315132b0e/external-secrets/CHANGELOG.md" rel="noopener noreferrer"&gt;CHANGELOG&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://kadet-hub.org/generators/keycloak/" rel="noopener noreferrer"&gt;keycloak&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;1.1.0&lt;/td&gt;
&lt;td&gt;&lt;a href="https://github.com/kadet-hub/generators/blob/74b89a708f4d2d5e20c963b301977f21d3d5b537/keycloak/CHANGELOG.md" rel="noopener noreferrer"&gt;CHANGELOG&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://kadet-hub.org/generators/kubernetes/" rel="noopener noreferrer"&gt;kubernetes&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;1.4.0, 1.5.0, 1.6.0, 1.7.0, 1.8.0, 1.9.0, 1.9.1&lt;/td&gt;
&lt;td&gt;&lt;a href="https://github.com/kadet-hub/generators/blob/5e4335d164d811f9f896063c948cae3e5712ab5c/kubernetes/CHANGELOG.md" rel="noopener noreferrer"&gt;CHANGELOG&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://kadet-hub.org/generators/kubevirt/" rel="noopener noreferrer"&gt;kubevirt&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;1.1.2, 1.2.0, 1.3.0, 1.3.1, 1.3.2&lt;/td&gt;
&lt;td&gt;&lt;a href="https://github.com/kadet-hub/generators/blob/5e4335d164d811f9f896063c948cae3e5712ab5c/kubevirt/CHANGELOG.md" rel="noopener noreferrer"&gt;CHANGELOG&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;a href="https://kadet-hub.org/generators/mutate/" rel="noopener noreferrer"&gt;mutate&lt;/a&gt; (new)&lt;/td&gt;
&lt;td&gt;0.1.0&lt;/td&gt;
&lt;td&gt;&lt;a href="https://github.com/kadet-hub/generators/blob/925525b2fabb1c2fb33c00f1b00b0d2495edce06/mutate/CHANGELOG.md" rel="noopener noreferrer"&gt;CHANGELOG&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://kadet-hub.org/generators/rabbitmq/" rel="noopener noreferrer"&gt;rabbitmq&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;1.1.0&lt;/td&gt;
&lt;td&gt;&lt;a href="https://github.com/kadet-hub/generators/blob/0011e4cdc40b7386730065784204bf22768b4b27/rabbitmq/CHANGELOG.md" rel="noopener noreferrer"&gt;CHANGELOG&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://kadet-hub.org/generators/strimzi/" rel="noopener noreferrer"&gt;strimzi&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;1.1.0&lt;/td&gt;
&lt;td&gt;&lt;a href="https://github.com/kadet-hub/generators/blob/2442bb7cd9ba0dc445a7a1b726bba527a0446d00/strimzi/CHANGELOG.md" rel="noopener noreferrer"&gt;CHANGELOG&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  cloudnative-pg
&lt;/h2&gt;

&lt;h3&gt;
  
  
  cloudnative-pg 1.1.1
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;__version__&lt;/code&gt; now reports the release version, which still read 1.0.1. Rendered output is unchanged.&lt;/p&gt;

&lt;h3&gt;
  
  
  cloudnative-pg 1.1.0
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;name&lt;/code&gt; on any entry names its resource and its output file, which before always took the entry's key. A recipe that may write only keys of its own name can then keep a resource name, such as the Cluster &lt;code&gt;app-db&lt;/code&gt; whose claims and Secrets carry its name. Two entries of a section with the same name in one namespace now fail the compile; before, the later one replaced the earlier output file. Output is unchanged for inventories without &lt;code&gt;name&lt;/code&gt; and without such a collision.&lt;/p&gt;

&lt;h2&gt;
  
  
  external-secrets
&lt;/h2&gt;

&lt;h3&gt;
  
  
  external-secrets 1.0.5
&lt;/h3&gt;

&lt;p&gt;Built for External Secrets Operator 2.13.0. The upstream CRDs in &lt;code&gt;config/crds/bases&lt;/code&gt; are unchanged against 2.12.0, so the shipped schemas, CRDs and rendered output are unchanged.&lt;/p&gt;

&lt;h2&gt;
  
  
  keycloak
&lt;/h2&gt;

&lt;h3&gt;
  
  
  keycloak 1.1.0
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;name&lt;/code&gt; on an entry of &lt;code&gt;instances&lt;/code&gt;, &lt;code&gt;realm_imports&lt;/code&gt;, &lt;code&gt;oidc_clients&lt;/code&gt; or &lt;code&gt;saml_clients&lt;/code&gt; names its resource and its output file, which before always took the entry's key. The defaults &lt;code&gt;realm.realm&lt;/code&gt; and &lt;code&gt;keycloakCRName&lt;/code&gt; follow it. A recipe that may write only keys of its own name can then keep a Keycloak name, such as &lt;code&gt;sso&lt;/code&gt;, whose StatefulSet, Services and Secrets carry it. Two entries of a section with the same name in one namespace now fail the compile. Output is unchanged for inventories without &lt;code&gt;name&lt;/code&gt; and without such a collision.&lt;/p&gt;

&lt;h2&gt;
  
  
  kubernetes
&lt;/h2&gt;

&lt;h3&gt;
  
  
  kubernetes 1.9.1
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;__version__&lt;/code&gt; now reports the release version, which still read 1.1.1. Rendered output is unchanged.&lt;/p&gt;

&lt;h3&gt;
  
  
  kubernetes 1.9.0
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;webhook_configuration_name&lt;/code&gt; and &lt;code&gt;validating_webhook_configuration_name&lt;/code&gt; on a component name its MutatingWebhookConfiguration and ValidatingWebhookConfiguration. Without them both stay &lt;code&gt;&amp;lt;namespace&amp;gt;-&amp;lt;component&amp;gt;&lt;/code&gt;. The CloudNativePG operator writes its CA only into configurations of fixed names.&lt;/li&gt;
&lt;li&gt;A &lt;code&gt;volume_mounts&lt;/code&gt; entry takes &lt;code&gt;volume&lt;/code&gt;, the volume it mounts. Its key then names only the mount, so one volume can be mounted at several paths with their own &lt;code&gt;sub_path&lt;/code&gt;, as Nextcloud's chart mounts its claim six times. Without &lt;code&gt;volume&lt;/code&gt; the key names the volume, as before.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Output is unchanged for inventories without the two name keys and without &lt;code&gt;volume&lt;/code&gt; in &lt;code&gt;volume_mounts&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  kubernetes 1.8.0
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;generators.kubernetes.routes.&amp;lt;name&amp;gt;&lt;/code&gt; renders one route attached to Gateways outside the inventory, named in &lt;code&gt;parent_refs&lt;/code&gt; by &lt;code&gt;name&lt;/code&gt; and optionally &lt;code&gt;namespace&lt;/code&gt;, &lt;code&gt;section_name&lt;/code&gt; and &lt;code&gt;port&lt;/code&gt;. It takes the route keys of &lt;code&gt;gateways.&amp;lt;gw&amp;gt;.routes&lt;/code&gt; and renders no Gateway. Before, a route was rendered only together with its Gateway, so a route to the cluster's shared Gateway needed a &lt;code&gt;manifests&lt;/code&gt; entry. The file is &lt;code&gt;&amp;lt;namespace&amp;gt;/&amp;lt;name&amp;gt;-route&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;A redirect rule takes &lt;code&gt;replace_full_path&lt;/code&gt; or &lt;code&gt;replace_prefix_match&lt;/code&gt;, which render the redirect's &lt;code&gt;path&lt;/code&gt; as &lt;code&gt;ReplaceFullPath&lt;/code&gt; or &lt;code&gt;ReplacePrefixMatch&lt;/code&gt;, on the routes of &lt;code&gt;gateways&lt;/code&gt; and of &lt;code&gt;routes&lt;/code&gt;. Both together fail the compile.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;webhook_ca_injected: true&lt;/code&gt; on a component renders its webhooks with &lt;code&gt;failurePolicy: Fail&lt;/code&gt; and no &lt;code&gt;caBundle&lt;/code&gt;, for a server that writes the CA into its webhook configurations itself, as the CloudNativePG operator does. Without it such a webhook still fails the compile.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Output is unchanged for inventories without &lt;code&gt;routes&lt;/code&gt;, without the two redirect keys and without &lt;code&gt;webhook_ca_injected&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  kubernetes 1.7.0
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;args&lt;/code&gt; takes a mapping next to the list. Its values render in the mapping's order: a string as one argument, a list as several, &lt;code&gt;null&lt;/code&gt; as none. A target can override or drop one argument a class sets by its key, which a list did not allow because the backends append or merge lists. A mapping value fails the compile.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;service: {}&lt;/code&gt; renders the component's Service with the API defaults. Before, an empty mapping rendered no Service and &lt;code&gt;type: ClusterIP&lt;/code&gt; had to be written. &lt;code&gt;api_service&lt;/code&gt; accepts it as well. A StatefulSet with &lt;code&gt;service: null&lt;/code&gt; no longer fails the compile.&lt;/li&gt;
&lt;li&gt;README: component &lt;code&gt;labels&lt;/code&gt; go on the workload, its Services, HPA and VPA, not on the pod template. Service, PodDisruptionBudget and NetworkPolicy selectors take all pod template labels. The README also states the label-value rule for names, the sorted container ports and &lt;code&gt;namespaceSelector: {}&lt;/code&gt; on a webhook.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Output is unchanged for inventories without an empty &lt;code&gt;service&lt;/code&gt; mapping and without an &lt;code&gt;args&lt;/code&gt; mapping.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/kadet-hub/generators/blob/5e4335d164d811f9f896063c948cae3e5712ab5c/kubernetes/CHANGELOG.md" rel="noopener noreferrer"&gt;Full changelog&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  kubevirt
&lt;/h2&gt;

&lt;h3&gt;
  
  
  kubevirt 1.3.2
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;__version__&lt;/code&gt; now reports the release version, which still read 1.1.1. Rendered output is unchanged.&lt;/p&gt;

&lt;h3&gt;
  
  
  kubevirt 1.3.1
&lt;/h3&gt;

&lt;p&gt;The KubeVirt CR renders &lt;code&gt;spec: {}&lt;/code&gt; when the inventory sets no &lt;code&gt;spec&lt;/code&gt; or &lt;code&gt;spec: null&lt;/code&gt;. Before, it rendered without &lt;code&gt;spec&lt;/code&gt;, which the CRD requires, so the API server rejected it with &lt;code&gt;spec: Required value&lt;/code&gt;. Targets that set &lt;code&gt;spec&lt;/code&gt; render the same output.&lt;/p&gt;

&lt;h3&gt;
  
  
  kubevirt 1.3.0
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;csi.image&lt;/code&gt; is optional and defaults to &lt;code&gt;ghcr.io/kadet-hub/mirror/kubevirt-csi-driver:3a1a099330045754011803e3d53cd8276ff21723@sha256:44c98353e6137455d6a393ad13e04f60e10ee49fdcd00cf078f7f7084714f409&lt;/code&gt;, kadet-hub's unmodified copy of the quay.io &lt;code&gt;latest&lt;/code&gt; build of 3a1a099. Targets that set &lt;code&gt;image&lt;/code&gt; render the same output. The README's Driver image section links the image's upstream sources.&lt;/p&gt;

&lt;h3&gt;
  
  
  kubevirt 1.2.0
&lt;/h3&gt;

&lt;p&gt;Adds &lt;code&gt;csi.snapshot_controller&lt;/code&gt; for the tenant role. &lt;code&gt;true&lt;/code&gt; renders the snapshot-controller of external-snapshotter v8.7.0 with its RBAC and the VolumeSnapshotClass, VolumeSnapshotContent and VolumeSnapshot CRDs, so a tenant VolumeSnapshotClass works without a separate install. The controller runs in the CSI namespace with a restricted security context. The README's CSI defaults list where it differs from upstream's manifest. On the infra role the key fails the compile. Output without the key stays the same.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;snapshot.storage.k8s.io&lt;/code&gt; schema group now names external-snapshotter 8.7.0 (&lt;code&gt;client/config/crd&lt;/code&gt;) as its source instead of the KubeVirt CSI driver's copy, and adds the VolumeSnapshotContent and VolumeSnapshot CRDs. All three files are upstream's byte for byte, and the output renders them from there.&lt;/p&gt;

&lt;h3&gt;
  
  
  kubevirt 1.1.2
&lt;/h3&gt;

&lt;p&gt;Built for the KubeVirt CSI driver at &lt;a href="mailto:main@e13603a"&gt;main@e13603a&lt;/a&gt;. Upstream now deploys what &lt;code&gt;csi&lt;/code&gt; already rendered. Since kubevirt/csi-driver#392 its controller runs with the restricted security context, with UID and GID 1001 from the &lt;code&gt;plain-kubernetes&lt;/code&gt; component, and the node plugin's &lt;code&gt;csi-driver&lt;/code&gt; container as UID 0. Since kubevirt/csi-driver#396 it pins csi-snapshotter v8.7.0. The rendered output stays the same, and the README's CSI defaults keep only the remaining differences. The VolumeSnapshotClass CRD and schema move to e13603a, whose file is external-snapshotter's v8.7.0 CRD and differs from the shipped one only in descriptions.&lt;/p&gt;

&lt;p&gt;The driver image is still the &lt;code&gt;latest&lt;/code&gt; build of 3a1a099. Upstream removed its image workflow (kubevirt/csi-driver#393), so the controller fix of kubevirt/csi-driver#397 is in no published image.&lt;/p&gt;

&lt;h2&gt;
  
  
  mutate
&lt;/h2&gt;

&lt;h3&gt;
  
  
  mutate 0.1.0
&lt;/h3&gt;

&lt;p&gt;First release. Patches the YAML files an earlier compile entry of the same &lt;code&gt;output_path&lt;/code&gt; wrote, such as Kapitan's &lt;code&gt;helm&lt;/code&gt; input, in place: mutations keyed by id, applied in id order with deletes last, match objects exactly on &lt;code&gt;apiVersion&lt;/code&gt;, &lt;code&gt;kind&lt;/code&gt;, &lt;code&gt;name&lt;/code&gt;, &lt;code&gt;namespace&lt;/code&gt; and &lt;code&gt;labels&lt;/code&gt;, and &lt;code&gt;merge&lt;/code&gt; (JSON Merge Patch, with &lt;code&gt;merge_lists&lt;/code&gt; for keyed list merge), &lt;code&gt;json6902&lt;/code&gt;, &lt;code&gt;delete&lt;/code&gt; or &lt;code&gt;images&lt;/code&gt;. &lt;code&gt;expects&lt;/code&gt; fails the compile when a mutation matches no object, or not the given count. Only changed files are written; a file whose objects are all deleted becomes a comment line. The compile entry needs &lt;code&gt;output_type: plain&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  rabbitmq
&lt;/h2&gt;

&lt;h3&gt;
  
  
  rabbitmq 1.1.0
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;name&lt;/code&gt; on an entry of a custom resource section names its resource, its output file and, for the kinds with &lt;code&gt;spec.name&lt;/code&gt;, the default of &lt;code&gt;spec.name&lt;/code&gt;, which before always took the entry's key. A recipe that may write only keys of its own name can then keep a resource name, such as the RabbitmqCluster &lt;code&gt;production&lt;/code&gt; whose StatefulSet, claims and Secrets carry its name. Two entries of a section with the same name in one namespace now fail the compile; before, the later one replaced the earlier output file. Output is unchanged for inventories without &lt;code&gt;name&lt;/code&gt; on those sections and without such a collision.&lt;/p&gt;

&lt;h2&gt;
  
  
  strimzi
&lt;/h2&gt;

&lt;h3&gt;
  
  
  strimzi 1.1.0
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;name&lt;/code&gt; on a Kafka, node pool, topic or user entry names its resource and its output key, which before always came from the entry's key. A recipe that may write only keys of its own name can then keep a cluster's name, such as the Kafka &lt;code&gt;events&lt;/code&gt; with the node pool &lt;code&gt;dual&lt;/code&gt; whose pods, claims, Services and Secrets carry both names. &lt;code&gt;cluster&lt;/code&gt; and &lt;code&gt;strimzi.io/cluster&lt;/code&gt; name the Kafka's resource. A topic or user whose &lt;code&gt;cluster&lt;/code&gt; names Kafkas in two namespaces fails the compile unless it sets &lt;code&gt;namespace&lt;/code&gt;. Output is unchanged for inventories without &lt;code&gt;name&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://kadet-hub.org/blog/releases-2026-10-19/" rel="noopener noreferrer"&gt;kadet-hub.org&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>kubernetes</category>
      <category>devops</category>
      <category>kapitan</category>
      <category>gitops</category>
    </item>
    <item>
      <title>Docker tag and digest: which one Kubernetes runs</title>
      <dc:creator>Moep90</dc:creator>
      <pubDate>Sat, 07 Nov 2026 13:43:39 +0000</pubDate>
      <link>https://dev.to/kadet-hub/docker-tag-and-digest-which-one-kubernetes-runs-2646</link>
      <guid>https://dev.to/kadet-hub/docker-tag-and-digest-which-one-kubernetes-runs-2646</guid>
      <description>&lt;p&gt;An image reference can carry a tag and a &lt;a href="https://kadet-hub.org/glossary/#digest" rel="noopener noreferrer"&gt;digest&lt;/a&gt; at&lt;br&gt;
once, as in &lt;code&gt;busybox:1.37.0@sha256:73aaf090…&lt;/code&gt;. Kubernetes runs the image&lt;br&gt;
the digest names. The tag is removed before the pull and never compared&lt;br&gt;
with the digest. If the two disagree, the pod runs the digest's image while&lt;br&gt;
the spec shows the tag's version.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://kadet-hub.org/recipes/" rel="noopener noreferrer"&gt;recipes&lt;/a&gt; in kadet-hub/recipes could produce exactly that&lt;br&gt;
mismatch. They kept the tag in one file and the digest in another, until&lt;br&gt;
&lt;a href="https://github.com/kadet-hub/recipes/pull/50" rel="noopener noreferrer"&gt;kadet-hub/recipes#50&lt;/a&gt; put&lt;br&gt;
both into one literal line.&lt;/p&gt;

&lt;p&gt;Everything below ran on 2026-11-02 with kind v0.33.0 and its default node&lt;br&gt;
image &lt;code&gt;kindest/node:v1.37.0@sha256:a1ed56cf…&lt;/code&gt; (Kubernetes v1.37.0,&lt;br&gt;
containerd v2.3.4), kubectl v1.37.2 and oras 1.3.6.&lt;/p&gt;

&lt;h2&gt;
  
  
  A tag that disagrees with its digest
&lt;/h2&gt;

&lt;p&gt;The two busybox tags resolved to these digests:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ oras manifest fetch --descriptor docker.io/library/busybox:1.36.1
{"mediaType":"application/vnd.oci.image.index.v1+json","digest":"sha256:73aaf090f3d85aa34ee199857f03fa3a95c8ede2ffd4cc2cdb5b94e566b11662","size":9535}
$ oras manifest fetch --descriptor docker.io/library/busybox:1.37.0
{"mediaType":"application/vnd.oci.image.index.v1+json","digest":"sha256:a7dbc319b1438115a207e11930dc4bb3e643dcb3ec0bdd694bb36cd8fad289ec","size":9535}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;Each is the digest of an OCI image index, the list of per-platform images&lt;br&gt;
behind the tag. Tags can move, so a later run may resolve other digests.&lt;/p&gt;

&lt;p&gt;The pod takes the tag 1.37.0 and the digest of 1.36.1. That is the state a&lt;br&gt;
version bump leaves behind when the digest stays:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;apiVersion: v1
kind: Pod
metadata:
  name: tag-and-digest
spec:
  restartPolicy: Never
  containers:
  - name: busybox
    image: busybox:1.37.0@sha256:73aaf090f3d85aa34ee199857f03fa3a95c8ede2ffd4cc2cdb5b94e566b11662
    command: [sh, -c, "busybox | head -1"]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;The API server accepts it, and the kubelet pulls and starts it:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ kubectl get events --field-selector involvedObject.name=tag-and-digest -o jsonpath='{range .items[*]}{.reason}{": "}{.message}{"\n"}{end}'
Scheduled: Successfully assigned default/tag-and-digest to post-digest-24c921-control-plane
Pulling: Pulling image "busybox:1.37.0@sha256:73aaf090f3d85aa34ee199857f03fa3a95c8ede2ffd4cc2cdb5b94e566b11662"
Pulled: Successfully pulled image "busybox:1.37.0@sha256:73aaf090f3d85aa34ee199857f03fa3a95c8ede2ffd4cc2cdb5b94e566b11662" in 2.15s (2.15s including waiting). Image size: 2217006 bytes.
Created: Container created
Started: Container started
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;The container prints the version of the binary it runs:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ kubectl logs tag-and-digest
BusyBox v1.36.1 (2023-05-18 22:34:17 UTC) multi-call binary.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;The pod's image fields:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ kubectl get pod tag-and-digest -o jsonpath='{.spec.containers[0].image}{"\n"}{.spec.containers[0].imagePullPolicy}{"\n"}{.status.containerStatuses[0].image}{"\n"}{.status.containerStatuses[0].imageID}{"\n"}'
busybox:1.37.0@sha256:73aaf090f3d85aa34ee199857f03fa3a95c8ede2ffd4cc2cdb5b94e566b11662
IfNotPresent
sha256:b116e155074440ffd9e449559433feb4cd2341eb3554b1da1c638c976e56451d
docker.io/library/busybox@sha256:73aaf090f3d85aa34ee199857f03fa3a95c8ede2ffd4cc2cdb5b94e566b11662
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;&lt;code&gt;.spec.containers[].image&lt;/code&gt; keeps the string as written, tag included.&lt;br&gt;
&lt;code&gt;.status.containerStatuses[].imageID&lt;/code&gt; holds the repository and the digest&lt;br&gt;
without a tag. It is the field that tells you which image runs. Its digest&lt;br&gt;
is the one oras resolved for 1.36.1.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;.status.containerStatuses[].image&lt;/code&gt; carries no tag either. containerd&lt;br&gt;
reports the image's first tag there, and the node-local image ID when the&lt;br&gt;
image has none&lt;br&gt;
(&lt;a href="https://github.com/containerd/containerd/blob/v2.3.4/internal/cri/server/container_status.go#L40-L66" rel="noopener noreferrer"&gt;container_status.go&lt;/a&gt;).&lt;br&gt;
That ID, &lt;code&gt;sha256:b116e155…&lt;/code&gt;, is the digest of the image's config. The node&lt;br&gt;
stored no tag for the image it pulled by digest:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ docker exec post-digest-24c921-control-plane crictl images --digests | grep -E 'IMAGE|busybox'
IMAGE                                           TAG                  DIGEST              IMAGE ID            SIZE
docker.io/library/busybox                       &amp;lt;none&amp;gt;               73aaf090f3d85       b116e15507444       2.22MB
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;A second pod with &lt;code&gt;busybox:1.37.0&lt;/code&gt; and no digest prints &lt;code&gt;BusyBox v1.37.0&lt;/code&gt;.&lt;br&gt;
Its &lt;code&gt;imageID&lt;/code&gt; ends in &lt;code&gt;a7dbc319…&lt;/code&gt;, the digest of 1.37.0. Its&lt;br&gt;
&lt;code&gt;.status.containerStatuses[].image&lt;/code&gt; is &lt;code&gt;docker.io/library/busybox:1.37.0&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The tag is never looked up
&lt;/h2&gt;

&lt;p&gt;The tag &lt;code&gt;no-such-tag&lt;/code&gt; does not exist:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ oras manifest fetch --descriptor docker.io/library/busybox:no-such-tag
Error response from registry: failed to find "docker.io/library/busybox:no-such-tag": docker.io/library/busybox:no-such-tag: not found
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;A third pod uses it in front of the 1.36.1 digest, with&lt;br&gt;
&lt;code&gt;imagePullPolicy: Always&lt;/code&gt; so that the kubelet asks for a pull:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;apiVersion: v1
kind: Pod
metadata:
  name: no-such-tag
spec:
  restartPolicy: Never
  containers:
  - name: busybox
    image: busybox:no-such-tag@sha256:73aaf090f3d85aa34ee199857f03fa3a95c8ede2ffd4cc2cdb5b94e566b11662
    imagePullPolicy: Always
    command: [sh, -c, "busybox | head -1"]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;Its events report&lt;br&gt;
&lt;code&gt;Successfully pulled image "busybox:no-such-tag@sha256:73aaf090…"&lt;/code&gt;, and it&lt;br&gt;
runs 1.36.1:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ kubectl logs no-such-tag
BusyBox v1.36.1 (2023-05-18 22:34:17 UTC) multi-call binary.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;The kubelet hands the reference to the container runtime as written. Its&lt;br&gt;
&lt;code&gt;applyDefaultImageTag&lt;/code&gt; appends &lt;code&gt;:latest&lt;/code&gt; only when a reference has neither&lt;br&gt;
a tag nor a digest&lt;br&gt;
(&lt;a href="https://github.com/kubernetes/kubernetes/blob/v1.37.0/pkg/kubelet/images/image_manager.go#L422-L439" rel="noopener noreferrer"&gt;image_manager.go&lt;/a&gt;,&lt;br&gt;
Kubernetes v1.37.0).&lt;/p&gt;

&lt;p&gt;containerd's CRI plugin normalizes the reference with &lt;code&gt;ParseDockerRef&lt;/code&gt;&lt;br&gt;
before it pulls&lt;br&gt;
(&lt;a href="https://github.com/containerd/containerd/blob/v2.3.4/internal/cri/server/images/image_pull.go#L153" rel="noopener noreferrer"&gt;image_pull.go&lt;/a&gt;).&lt;br&gt;
The function comes from distribution/reference v0.6.0, the version&lt;br&gt;
containerd v2.3.4 depends on. Its documentation says what happens to the&lt;br&gt;
tag:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;For references containing both a tag and a digest, it returns a digested reference.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Its example turns &lt;code&gt;docker.io/library/busybox:latest@sha256:7cc4b5ae…&lt;/code&gt;&lt;br&gt;
into &lt;code&gt;docker.io/library/busybox@sha256:7cc4b5ae…&lt;/code&gt;&lt;br&gt;
(&lt;a href="https://github.com/distribution/reference/blob/v0.6.0/normalize.go#L88-L121" rel="noopener noreferrer"&gt;normalize.go&lt;/a&gt;).&lt;br&gt;
containerd also resolves a reference against the images already on the&lt;br&gt;
node through &lt;code&gt;ParseDockerRef&lt;/code&gt;&lt;br&gt;
(&lt;a href="https://github.com/containerd/containerd/blob/v2.3.4/internal/cri/server/images/service.go#L160-L186" rel="noopener noreferrer"&gt;service.go&lt;/a&gt;).&lt;br&gt;
The presence check behind &lt;code&gt;IfNotPresent&lt;/code&gt; therefore ignores the tag as well.&lt;/p&gt;

&lt;p&gt;The Kubernetes documentation states the outcome in its list of image name&lt;br&gt;
examples. It describes &lt;code&gt;registry.k8s.io/pause:3.5@sha256:1ff6c18f…&lt;/code&gt; as&lt;br&gt;
"Image name with tag and digest. Only the digest will be used for&lt;br&gt;
pulling."&lt;br&gt;
(&lt;a href="https://kubernetes.io/docs/concepts/containers/images/#image-names" rel="noopener noreferrer"&gt;Images&lt;/a&gt;).&lt;/p&gt;

&lt;h2&gt;
  
  
  The tag still sets the pull policy
&lt;/h2&gt;

&lt;p&gt;When &lt;code&gt;imagePullPolicy&lt;/code&gt; is omitted, the API server defaults it from the tag.&lt;br&gt;
&lt;code&gt;SetDefaults_Container&lt;/code&gt; sets &lt;code&gt;Always&lt;/code&gt; for the tag &lt;code&gt;latest&lt;/code&gt; and&lt;br&gt;
&lt;code&gt;IfNotPresent&lt;/code&gt; for any other&lt;br&gt;
(&lt;a href="https://github.com/kubernetes/kubernetes/blob/v1.37.0/pkg/apis/core/v1/defaults.go#L83-L93" rel="noopener noreferrer"&gt;defaults.go&lt;/a&gt;).&lt;br&gt;
The tag comes from &lt;code&gt;ParseImageName&lt;/code&gt;, which returns it even when a digest is&lt;br&gt;
present&lt;br&gt;
(&lt;a href="https://github.com/kubernetes/kubernetes/blob/v1.37.0/pkg/util/parsers/parsers.go#L32-L55" rel="noopener noreferrer"&gt;parsers.go&lt;/a&gt;).&lt;br&gt;
A server-side dry run shows both cases:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ kubectl run latest-and-digest --restart=Never --image=busybox:latest@sha256:73aaf090f3d85aa34ee199857f03fa3a95c8ede2ffd4cc2cdb5b94e566b11662 --dry-run=server -o jsonpath='{.spec.containers[0].imagePullPolicy}{"\n"}'
Always
$ kubectl run digest-only --restart=Never --image=busybox@sha256:73aaf090f3d85aa34ee199857f03fa3a95c8ede2ffd4cc2cdb5b94e566b11662 --dry-run=server -o jsonpath='{.spec.containers[0].imagePullPolicy}{"\n"}'
IfNotPresent
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;The documentation's list of defaults says: "if you omit the&lt;br&gt;
&lt;code&gt;imagePullPolicy&lt;/code&gt; field, and you specify the digest for the container&lt;br&gt;
image, the &lt;code&gt;imagePullPolicy&lt;/code&gt; is automatically set to &lt;code&gt;IfNotPresent&lt;/code&gt;"&lt;br&gt;
(&lt;a href="https://kubernetes.io/docs/concepts/containers/images/#imagepullpolicy-defaulting" rel="noopener noreferrer"&gt;default image pull policy&lt;/a&gt;).&lt;br&gt;
With &lt;code&gt;:latest&lt;/code&gt; in front of the digest, the tag decides and the result is&lt;br&gt;
&lt;code&gt;Always&lt;/code&gt;. Under &lt;code&gt;Always&lt;/code&gt; the runtime contacts the registry at every&lt;br&gt;
container start, although the digest already fixes the image&lt;br&gt;
(&lt;a href="https://kubernetes.io/docs/concepts/containers/images/#image-pull-policy" rel="noopener noreferrer"&gt;image pull policy&lt;/a&gt;).&lt;/p&gt;

&lt;h2&gt;
  
  
  Two files, one image
&lt;/h2&gt;

&lt;p&gt;The recipes are Kapitan &lt;a href="https://kadet-hub.org/glossary/#inventory" rel="noopener noreferrer"&gt;inventory&lt;/a&gt; classes. Until&lt;br&gt;
kadet-hub/recipes#50, each kept an image's tag in&lt;br&gt;
&lt;code&gt;applications.&amp;lt;name&amp;gt;.version&lt;/code&gt; in its &lt;code&gt;init.yml&lt;/code&gt;, and the digest in the&lt;br&gt;
image line of another class file. In the miniflux recipe, &lt;code&gt;init.yml&lt;/code&gt; set&lt;br&gt;
&lt;code&gt;version: 2.3.4&lt;/code&gt; and &lt;code&gt;miniflux.yml&lt;/code&gt; had this line:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;image: miniflux/miniflux:${applications.miniflux.version}@sha256:72e49f513a63104506aab98ba8cfc3a409d4fde23550a583dec07ec941f1e94e
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;The &lt;code&gt;${...}&lt;/code&gt; is an &lt;a href="https://kadet-hub.org/glossary/#interpolation" rel="noopener noreferrer"&gt;interpolation&lt;/a&gt; that Kapitan&lt;br&gt;
fills in from the parameter. A target that raised the version without the&lt;br&gt;
digest compiled to manifests with the new tag and the old digest. That is&lt;br&gt;
the first pod above. The spec and every diff of the compiled manifests show&lt;br&gt;
the new version, and the kubelet runs the old image.&lt;/p&gt;

&lt;p&gt;The fix writes each image as one literal,&lt;br&gt;
&lt;code&gt;&amp;lt;registry&amp;gt;/&amp;lt;repository&amp;gt;:&amp;lt;tag&amp;gt;@sha256:&amp;lt;digest&amp;gt;&lt;/code&gt;:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;image: miniflux/miniflux:2.3.4@sha256:72e49f513a63104506aab98ba8cfc3a409d4fde23550a583dec07ec941f1e94e
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;&lt;code&gt;applications.&amp;lt;name&amp;gt;.version&lt;/code&gt; is gone wherever it only fed image tags.&lt;br&gt;
&lt;code&gt;check lint&lt;/code&gt;, the linter the recipes run in CI, now fails on a line of a&lt;br&gt;
class file that holds both &lt;code&gt;@sha256:&lt;/code&gt; and &lt;code&gt;${&lt;/code&gt;:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;for n, line in enumerate(text.splitlines(), 1):
    if "@sha256:" in line and "${" in line:
        errors.append(f"{f}:{n}: image reference takes its tag from an interpolation (RCP-66)")
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;A reference without a digest does not match this check, and the recipes'&lt;br&gt;
specification leaves those to review&lt;br&gt;
(&lt;a href="https://github.com/kadet-hub/recipes/blob/main/docs/specs/recipes.md" rel="noopener noreferrer"&gt;RCP-66&lt;/a&gt;).&lt;/p&gt;

&lt;h2&gt;
  
  
  Renovate reads the tag from the same line
&lt;/h2&gt;

&lt;p&gt;Renovate writes references in this form. Its Docker documentation says&lt;br&gt;
that "When pinning a digest, Renovate retains the Docker tag in the &lt;code&gt;FROM&lt;/code&gt;&lt;br&gt;
line for readability", with &lt;code&gt;FROM node:14.15.1@sha256:d938c176…&lt;/code&gt; as the&lt;br&gt;
example&lt;br&gt;
(&lt;a href="https://docs.renovatebot.com/docker/#digest-pinning" rel="noopener noreferrer"&gt;Digest Pinning&lt;/a&gt;).&lt;br&gt;
The &lt;code&gt;pinDigests&lt;/code&gt; option pins images "by means of their SHA256 digest and&lt;br&gt;
not only by tag"&lt;br&gt;
(&lt;a href="https://docs.renovatebot.com/configuration-options/#pindigests" rel="noopener noreferrer"&gt;pinDigests&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;Renovate's updates start from the tag it finds in the file&lt;br&gt;
(&lt;a href="https://docs.renovatebot.com/docker/#how-it-works" rel="noopener noreferrer"&gt;How It Works&lt;/a&gt;):&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;If the image tag in use "looks" like a version (e.g. &lt;code&gt;myimage:1&lt;/code&gt;, &lt;code&gt;myimage:1.1&lt;/code&gt;, &lt;code&gt;myimage:1.1.0&lt;/code&gt;, &lt;code&gt;myimage:1-onbuild&lt;/code&gt;) then Renovate checks the Docker registry for upgrades&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The same page describes pull requests for a new digest behind an unchanged&lt;br&gt;
tag&lt;br&gt;
(&lt;a href="https://docs.renovatebot.com/docker/#digest-updating" rel="noopener noreferrer"&gt;Digest Updating&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;In kadet-hub/recipes, Renovate's regex managers capture the tag as&lt;br&gt;
&lt;code&gt;currentValue&lt;/code&gt; and the digest as &lt;code&gt;currentDigest&lt;/code&gt; from the same image line.&lt;br&gt;
Those are the capture groups the&lt;br&gt;
&lt;a href="https://docs.renovatebot.com/modules/manager/regex/#required-capture-groups" rel="noopener noreferrer"&gt;regex manager documentation&lt;/a&gt;&lt;br&gt;
asks for. The old miniflux line had &lt;code&gt;${applications.miniflux.version}&lt;/code&gt;&lt;br&gt;
where that version belongs.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://kadet-hub.org/blog/docker-tag-and-digest/" rel="noopener noreferrer"&gt;kadet-hub.org&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>kubernetes</category>
      <category>devops</category>
      <category>kapitan</category>
      <category>gitops</category>
    </item>
    <item>
      <title>Renovate automerge with required reviews on GitHub</title>
      <dc:creator>Moep90</dc:creator>
      <pubDate>Sat, 07 Nov 2026 13:43:30 +0000</pubDate>
      <link>https://dev.to/kadet-hub/renovate-automerge-with-required-reviews-on-github-23ma</link>
      <guid>https://dev.to/kadet-hub/renovate-automerge-with-required-reviews-on-github-23ma</guid>
      <description>&lt;p&gt;Renovate's automerge stops at a branch that requires an approving review.&lt;br&gt;
Its documentation says so: "If you have mandatory Pull Request reviews then&lt;br&gt;
it means Renovate can't automerge its own PR until such a review has&lt;br&gt;
happened"&lt;br&gt;
(&lt;a href="https://docs.renovatebot.com/key-concepts/automerge/#required-pull-request-reviews" rel="noopener noreferrer"&gt;Renovate docs&lt;/a&gt;).&lt;br&gt;
The &lt;code&gt;main&lt;/code&gt; ruleset of kadet-hub/registry requires one approving review and&lt;br&gt;
a code-owner review. Since 2 November 2026 Renovate merges its own minor,&lt;br&gt;
patch and digest updates there without either. The Renovate GitHub App has&lt;br&gt;
a ruleset bypass that works on pull requests only. GitHub's auto-merge&lt;br&gt;
stays off, so Renovate merges once every check on the pull request has&lt;br&gt;
passed, including the checks the ruleset does not require.&lt;/p&gt;

&lt;p&gt;The change is&lt;br&gt;
&lt;a href="https://github.com/kadet-hub/registry/pull/441" rel="noopener noreferrer"&gt;kadet-hub/registry#441&lt;/a&gt;.&lt;br&gt;
The first pull request merged this way was&lt;br&gt;
&lt;a href="https://github.com/kadet-hub/registry/pull/439" rel="noopener noreferrer"&gt;#439&lt;/a&gt;, a digest update of&lt;br&gt;
the distroless &lt;code&gt;debug&lt;/code&gt; image that the &lt;a href="https://kadet-hub.org/glossary/#gate" rel="noopener noreferrer"&gt;gate&lt;/a&gt;'s sandbox&lt;br&gt;
image copies its busybox from. The GitHub docs quoted below were read on&lt;br&gt;
2 November 2026, the Renovate docs and source at Renovate 44.201.0.&lt;/p&gt;
&lt;h2&gt;
  
  
  Why an approval bot does not help
&lt;/h2&gt;

&lt;p&gt;Renovate's docs name two ways past a required review on GitHub. One is a&lt;br&gt;
bypass of the review requirement. The other is the helper apps&lt;br&gt;
renovate-approve and renovate-approve-2, which "will mark all automerging&lt;br&gt;
Pull Requests by Renovate as approved"&lt;br&gt;
(&lt;a href="https://docs.renovatebot.com/key-concepts/automerge/#required-pull-request-reviews" rel="noopener noreferrer"&gt;Renovate docs&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;An approval from an app does not satisfy a code-owner review. With&lt;br&gt;
code-owner review required, "any pull request that modifies content with a&lt;br&gt;
code owner must be approved by that code owner"&lt;br&gt;
(&lt;a href="https://docs.github.com/en/repositories/configuring-branches-and-merges-in-your-repository/managing-rulesets/available-rules-for-rulesets#require-a-pull-request-before-merging" rel="noopener noreferrer"&gt;GitHub docs&lt;/a&gt;).&lt;br&gt;
A CODEOWNERS file lists owners as "GitHub usernames or team names"&lt;br&gt;
(&lt;a href="https://docs.github.com/en/repositories/managing-your-repositorys-settings-and-features/customizing-your-repository/about-code-owners#codeowners-syntax" rel="noopener noreferrer"&gt;GitHub docs&lt;/a&gt;).&lt;br&gt;
A workflow that approves with its &lt;code&gt;GITHUB_TOKEN&lt;/code&gt; approves as an app too,&lt;br&gt;
because "The &lt;code&gt;GITHUB_TOKEN&lt;/code&gt; secret is a GitHub App installation access&lt;br&gt;
token"&lt;br&gt;
(&lt;a href="https://docs.github.com/en/actions/concepts/security/github_token" rel="noopener noreferrer"&gt;GitHub docs&lt;/a&gt;).&lt;br&gt;
Renovate's docs put it this way: "having a &lt;code&gt;CODEOWNERS&lt;/code&gt; file could block&lt;br&gt;
automerging, because it means a code owner must review the PR"&lt;br&gt;
(&lt;a href="https://docs.renovatebot.com/key-concepts/automerge/#codeowners" rel="noopener noreferrer"&gt;Renovate docs&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;kadet-hub/registry has a code owner for &lt;code&gt;.github/&lt;/code&gt;, &lt;code&gt;gate/&lt;/code&gt;, &lt;code&gt;policy/&lt;/code&gt;,&lt;br&gt;
&lt;code&gt;scan/&lt;/code&gt;, &lt;code&gt;sandbox/&lt;/code&gt; and &lt;code&gt;.clusterfuzzlite/&lt;/code&gt;. Every file Renovate updates in&lt;br&gt;
that repository sits under one of them, apart from the registry entries.&lt;/p&gt;
&lt;h1&gt;
  
  
  439 changed &lt;code&gt;sandbox/Dockerfile&lt;/code&gt;.
&lt;/h1&gt;

&lt;p&gt;GitHub's auto-merge is the other half of that setup, and Renovate uses it&lt;br&gt;
by default. "Auto-merge merges a pull request automatically after all&lt;br&gt;
required reviews and status checks pass"&lt;br&gt;
(&lt;a href="https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/incorporating-changes-from-a-pull-request/automatically-merging-a-pull-request#about-auto-merge" rel="noopener noreferrer"&gt;GitHub docs&lt;/a&gt;).&lt;br&gt;
It does not wait for a check the branch does not require. The registry's&lt;br&gt;
ruleset requires two checks, &lt;code&gt;gate&lt;/code&gt; and &lt;code&gt;renders&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Several checks that exercise a dependency update in this repository come&lt;br&gt;
from path-filtered workflows. &lt;code&gt;selftest&lt;/code&gt; runs the gate's self tests when&lt;br&gt;
&lt;code&gt;sandbox/&lt;/code&gt;, &lt;code&gt;gate/&lt;/code&gt;, &lt;code&gt;scan/&lt;/code&gt;, &lt;code&gt;policy/&lt;/code&gt; or a few other paths change.&lt;br&gt;
&lt;code&gt;sandbox-advisories&lt;/code&gt;&lt;br&gt;
fails on an advisory that has a fix for the sandbox image. GitHub's&lt;br&gt;
troubleshooting page says that the checks of a workflow skipped by path&lt;br&gt;
filtering "stay in a "Pending" state and block merging", and advises:&lt;br&gt;
"Avoid requiring workflows that can be skipped"&lt;br&gt;
(&lt;a href="https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/collaborating-on-repositories-with-code-quality-features/troubleshooting-required-status-checks#handling-skipped-but-required-checks" rel="noopener noreferrer"&gt;GitHub docs&lt;/a&gt;).&lt;br&gt;
So these checks are not required, and auto-merge would not wait for them.&lt;/p&gt;
&lt;h2&gt;
  
  
  A bypass for pull requests only
&lt;/h2&gt;
&lt;h1&gt;
  
  
  441 added one entry to the bypass list of the &lt;code&gt;main&lt;/code&gt; ruleset. The
&lt;/h1&gt;

&lt;p&gt;repository keeps a copy of the ruleset in &lt;code&gt;.github/ruleset.json&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight diff"&gt;&lt;code&gt;&lt;span class="gd"&gt;-  "bypass_actors": [{"actor_id": 5, "actor_type": "RepositoryRole", "bypass_mode": "pull_request"}],
&lt;/span&gt;&lt;span class="gi"&gt;+  "bypass_actors": [
+    {"actor_id": 5, "actor_type": "RepositoryRole", "bypass_mode": "pull_request"},
+    {"actor_id": 2740, "actor_type": "Integration", "bypass_mode": "pull_request"}
+  ],
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;Integration&lt;/code&gt; is the actor type of a GitHub App. &lt;code&gt;2740&lt;/code&gt; is the ID that&lt;br&gt;
&lt;code&gt;gh api /apps/renovate&lt;/code&gt; returns for the Renovate app. The live ruleset&lt;br&gt;
carries the same entry:&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ gh api repos/kadet-hub/registry/rulesets/24720369 --jq '.bypass_actors[] | select(.actor_type == "Integration")'
{"actor_id":2740,"actor_type":"Integration","bypass_mode":"pull_request"}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;p&gt;The REST API accepts &lt;code&gt;always&lt;/code&gt;, &lt;code&gt;pull_request&lt;/code&gt; and &lt;code&gt;exempt&lt;/code&gt; for&lt;br&gt;
&lt;code&gt;bypass_mode&lt;/code&gt;, and its reference says: "pull_request means that an actor&lt;br&gt;
can only bypass rules on pull requests"&lt;br&gt;
(&lt;a href="https://docs.github.com/en/rest/repos/rules#update-a-repository-ruleset" rel="noopener noreferrer"&gt;GitHub REST docs&lt;/a&gt;).&lt;br&gt;
The default is &lt;code&gt;always&lt;/code&gt;. An entry that leaves &lt;code&gt;bypass_mode&lt;/code&gt; out lets the&lt;br&gt;
app push to &lt;code&gt;main&lt;/code&gt; directly.&lt;/p&gt;

&lt;p&gt;In the ruleset editor, &lt;code&gt;pull_request&lt;/code&gt; is the option "For pull requests&lt;br&gt;
only", which GitHub offers "to grant bypass to an actor without allowing&lt;br&gt;
them to push directly to a repository". The same page goes on:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The selected actor is now required to open a pull request to make&lt;br&gt;
changes to a repository, creating a clear trail of their changes in the&lt;br&gt;
pull request and audit log. The actor can then choose to bypass any&lt;br&gt;
branch protections and merge that pull request.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;(&lt;a href="https://docs.github.com/en/repositories/configuring-branches-and-merges-in-your-repository/managing-rulesets/creating-rulesets-for-a-repository#granting-bypass-permissions-for-your-branch-or-tag-ruleset" rel="noopener noreferrer"&gt;GitHub docs&lt;/a&gt;)&lt;/p&gt;

&lt;p&gt;The bypass covers the rules of the whole ruleset, the required status&lt;br&gt;
checks among them. Once the app is on the list, GitHub no longer stops its&lt;br&gt;
merge on a failed check. That job moves to Renovate.&lt;/p&gt;
&lt;h2&gt;
  
  
  What Renovate waits for
&lt;/h2&gt;

&lt;p&gt;The top of &lt;code&gt;renovate.json&lt;/code&gt; leaves automerge off by default and switches&lt;br&gt;
GitHub's auto-merge off:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nl"&gt;"automerge"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="err"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"platformAutomerge"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="err"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"minimumReleaseAge"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"3 days"&lt;/span&gt;&lt;span class="err"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"minimumReleaseAgeBehaviour"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"timestamp-required"&lt;/span&gt;&lt;span class="err"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With &lt;code&gt;platformAutomerge&lt;/code&gt; at its default &lt;code&gt;true&lt;/code&gt;, Renovate hands the merge to&lt;br&gt;
the platform, which merges "after all (if any) branch policies have been&lt;br&gt;
met"&lt;br&gt;
(&lt;a href="https://docs.renovatebot.com/configuration-options/#platformautomerge" rel="noopener noreferrer"&gt;Renovate docs&lt;/a&gt;).&lt;br&gt;
With &lt;code&gt;false&lt;/code&gt;, Renovate merges with its own API call. For the default&lt;br&gt;
&lt;code&gt;automergeType&lt;/code&gt; of &lt;code&gt;pr&lt;/code&gt;, Renovate "automerges the PR on a subsequent run&lt;br&gt;
once it detects the PR's status checks are 'green'", and "Renovate only&lt;br&gt;
automerges branches which are up-to-date and green"&lt;br&gt;
(&lt;a href="https://docs.renovatebot.com/configuration-options/#automergetype" rel="noopener noreferrer"&gt;Renovate docs&lt;/a&gt;).&lt;br&gt;
The entry for &lt;code&gt;ignoreTests&lt;/code&gt; states the default: "Renovate's default&lt;br&gt;
behavior is to only automerge if every status check has succeeded"&lt;br&gt;
(&lt;a href="https://docs.renovatebot.com/configuration-options/#ignoretests" rel="noopener noreferrer"&gt;Renovate docs&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;What green means on GitHub is decided in &lt;code&gt;getBranchStatus&lt;/code&gt;&lt;br&gt;
(&lt;a href="https://github.com/renovatebot/renovate/blob/44.201.0/lib/modules/platform/github/index.ts#L1188-L1288" rel="noopener noreferrer"&gt;Renovate 44.201.0 source&lt;/a&gt;).&lt;br&gt;
It reads the commit statuses and the check runs of the branch's head&lt;br&gt;
commit. One check run with the conclusion &lt;code&gt;failure&lt;/code&gt; makes the branch red.&lt;br&gt;
The branch is green only when every check run has the conclusion&lt;br&gt;
&lt;code&gt;success&lt;/code&gt;, &lt;code&gt;neutral&lt;/code&gt; or &lt;code&gt;skipped&lt;/code&gt; and the remaining commit statuses are&lt;br&gt;
successful. A check run that is queued or still running has no conclusion&lt;br&gt;
yet, so the branch stays yellow and Renovate does not merge. Successful&lt;br&gt;
statuses that Renovate sets itself, such as &lt;code&gt;renovate/stability-days&lt;/code&gt;, are&lt;br&gt;
filtered out first, so they cannot turn a branch green on their own. GitHub&lt;br&gt;
counts the same three conclusions as a passing required check&lt;br&gt;
(&lt;a href="https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/collaborating-on-repositories-with-code-quality-features/troubleshooting-required-status-checks#required-check-needs-to-succeed-against-the-latest-commit-sha" rel="noopener noreferrer"&gt;GitHub docs&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;Renovate sees only the checks that exist on the commit when it runs. A&lt;br&gt;
workflow that has not created its check run yet cannot hold the merge.&lt;/p&gt;
&lt;h2&gt;
  
  
  Which updates merge
&lt;/h2&gt;

&lt;p&gt;The last three &lt;code&gt;packageRules&lt;/code&gt; of &lt;code&gt;renovate.json&lt;/code&gt; decide what Renovate may&lt;br&gt;
merge:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"description"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"REV-5: Renovate merges its own pull request once every check passed"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"matchUpdateTypes"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="s2"&gt;"minor"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="s2"&gt;"patch"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="s2"&gt;"digest"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="s2"&gt;"pin"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="s2"&gt;"pinDigest"&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"automerge"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="err"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"description"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"REV-5: an entry bump publishes a new version, which a maintainer approves (DEC-3)"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"matchFileNames"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="s2"&gt;"generators/*.yaml"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="s2"&gt;"recipes/*.yaml"&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"automerge"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="err"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"description"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"REV-5: a digest update of an action under the same version is a moved tag"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"matchManagers"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="s2"&gt;"github-actions"&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"matchUpdateTypes"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="s2"&gt;"digest"&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"automerge"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The descriptions cite kadet-hub's specs. REV-5 is the automerge requirement&lt;br&gt;
in &lt;a href="https://github.com/kadet-hub/registry/blob/main/docs/specs/gate.md" rel="noopener noreferrer"&gt;gate.md&lt;/a&gt;,&lt;br&gt;
and DEC-3 is the decision in&lt;br&gt;
&lt;a href="https://github.com/kadet-hub/registry/blob/main/docs/specs/marketplace.md" rel="noopener noreferrer"&gt;marketplace.md&lt;/a&gt;&lt;br&gt;
that every new version needs a maintainer's approval.&lt;/p&gt;

&lt;p&gt;Renovate applies &lt;code&gt;packageRules&lt;/code&gt; in order, and "later rules may override&lt;br&gt;
configuration options from earlier ones, if they both specify the same&lt;br&gt;
option"&lt;br&gt;
(&lt;a href="https://docs.renovatebot.com/configuration-options/#packagerules" rel="noopener noreferrer"&gt;Renovate docs&lt;/a&gt;).&lt;br&gt;
The first rule turns automerge on for five update types. A major update&lt;br&gt;
matches none of the three and keeps the top-level &lt;code&gt;"automerge": false&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The second rule covers the registry entries. Each file under &lt;code&gt;generators/&lt;/code&gt;&lt;br&gt;
and &lt;code&gt;recipes/&lt;/code&gt; pins a generator or a recipe to a tag and commit in its&lt;br&gt;
author's repository. Merging a bump of that pin publishes the new version&lt;br&gt;
in the catalog, so these bumps stay with a maintainer whatever their update&lt;br&gt;
type.&lt;/p&gt;

&lt;p&gt;The third rule is about GitHub Actions. kadet-hub pins every action to a&lt;br&gt;
commit SHA with the version as a comment, through the&lt;br&gt;
&lt;code&gt;helpers:pinGitHubActionDigests&lt;/code&gt; preset. A new release of an action changes&lt;br&gt;
the version in that comment. A &lt;code&gt;digest&lt;/code&gt; update under the same version&lt;br&gt;
means that the tag now points at another commit. That is how&lt;br&gt;
tj-actions/changed-files was compromised in March 2025: "Attackers&lt;br&gt;
retroactively modified multiple version tags to reference a malicious&lt;br&gt;
commit"&lt;br&gt;
(&lt;a href="https://github.com/advisories/GHSA-mrrh-fwg8-r2c3" rel="noopener noreferrer"&gt;GHSA-mrrh-fwg8-r2c3&lt;/a&gt;).&lt;br&gt;
The three days of &lt;code&gt;minimumReleaseAge&lt;/code&gt; do not hold such an update back: "If&lt;br&gt;
an existing tag is force-pushed to new commits, the digest update ages&lt;br&gt;
against the original release date, so it may pass Minimum Release Age&lt;br&gt;
immediately"&lt;br&gt;
(&lt;a href="https://docs.renovatebot.com/key-concepts/minimum-release-age/#digest-updates" rel="noopener noreferrer"&gt;Renovate docs&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;Everything else still waits three days after its release when the&lt;br&gt;
datasource reports a release timestamp. kadet-hub's config treats git tags&lt;br&gt;
and registries other than Docker Hub as reporting none and lets those&lt;br&gt;
updates through without one (&lt;code&gt;minimumReleaseAgeBehaviour:&lt;br&gt;
timestamp-optional&lt;/code&gt;). Such an update merges as soon as its checks pass.&lt;/p&gt;

&lt;p&gt;kadet-hub's threat model in marketplace.md records what this gives up. A&lt;br&gt;
malicious minor, patch or image digest release now reaches &lt;code&gt;main&lt;/code&gt; without&lt;br&gt;
a maintainer once the release age allows it and every check passes. Before&lt;/p&gt;

&lt;h1&gt;
  
  
  441 it needed one approval by the single maintainer.
&lt;/h1&gt;

&lt;p&gt;&lt;a href="https://github.com/kadet-hub/recipes/pull/56" rel="noopener noreferrer"&gt;kadet-hub/recipes#56&lt;/a&gt; made&lt;br&gt;
the same change in kadet-hub/recipes and keeps more updates manual. Lock&lt;br&gt;
file maintenance is not on its automerge list, because "the updated&lt;br&gt;
packages have no release timestamp for Renovate to check"&lt;br&gt;
(&lt;a href="https://docs.renovatebot.com/key-concepts/minimum-release-age/#lockfilemaintenance-updates" rel="noopener noreferrer"&gt;Renovate docs&lt;/a&gt;).&lt;br&gt;
The pin of the &lt;code&gt;next&lt;/code&gt; tag of kadet-hub's Kapitan image stays manual, since&lt;br&gt;
that image is rebuilt up to every 15 minutes and its digest updates have no&lt;br&gt;
release timestamp. A recipe's &lt;code&gt;Source&lt;/code&gt; line names the chart or image the&lt;br&gt;
recipe is built from, and updates to it wait for a reviewer. The &lt;code&gt;main&lt;/code&gt; ruleset&lt;br&gt;
there requires one approval without a code-owner review and has the same&lt;br&gt;
&lt;code&gt;pull_request&lt;/code&gt; bypass for the Renovate app.&lt;/p&gt;

&lt;h2&gt;
  
  
  The first merge
&lt;/h2&gt;

&lt;h1&gt;
  
  
  439 has no review, and GitHub still reports a review as required:
&lt;/h1&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ gh pr view 439 -R kadet-hub/registry --json mergedBy,mergedAt,reviewDecision,reviews
{"mergedAt":"2026-11-02T10:11:50Z","mergedBy":{"is_bot":true,"login":"app/renovate"},"reviewDecision":"REVIEW_REQUIRED","reviews":[]}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;The check runs on its last commit, ordered by completion time in UTC:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ gh pr view 439 -R kadet-hub/registry --json statusCheckRollup \
    --jq '.statusCheckRollup | map(select(.__typename == "CheckRun")) | sort_by(.completedAt)[] | [.completedAt, .conclusion, .name] | @tsv'
2026-11-02T09:25:20Z    SUCCESS workflow-lint
2026-11-02T09:25:24Z    SUCCESS checksum-pins
2026-11-02T09:25:31Z    SUCCESS sandbox-advisories
2026-11-02T09:25:49Z    SUCCESS gate
2026-11-02T09:25:49Z    SKIPPED renders
2026-11-02T09:25:59Z    SUCCESS comment
2026-11-02T09:26:06Z    SUCCESS consumer-selftest
2026-11-02T09:26:26Z    SUCCESS CodeQL
2026-11-02T09:26:31Z    SUCCESS analyze
2026-11-02T10:10:36Z    SUCCESS gate-selftest
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;The same rollup holds one commit status, &lt;code&gt;renovate/stability-days&lt;/code&gt;, with&lt;br&gt;
the state &lt;code&gt;SUCCESS&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;gate&lt;/code&gt; and &lt;code&gt;renders&lt;/code&gt; are the required checks. &lt;code&gt;renders&lt;/code&gt; checks a recipe on&lt;br&gt;
kind and runs only when the gate compiled one, so on #439 it was skipped.&lt;br&gt;
A skipped job "will not prevent a pull request from merging, even if it is&lt;br&gt;
a required check"&lt;br&gt;
(&lt;a href="https://docs.github.com/en/actions/how-tos/write-workflows/choose-when-workflows-run/control-jobs-with-conditions" rel="noopener noreferrer"&gt;GitHub docs&lt;/a&gt;).&lt;br&gt;
Both required checks were done at 09:25:49. &lt;code&gt;gate-selftest&lt;/code&gt; finished at&lt;br&gt;
10:10:36, and Renovate merged at 10:11:50.&lt;/p&gt;

&lt;p&gt;GitHub keeps the rule evaluations of the merge as a rule suite, which the&lt;br&gt;
REST API returns&lt;br&gt;
(&lt;a href="https://docs.github.com/en/rest/repos/rule-suites#get-a-repository-rule-suite" rel="noopener noreferrer"&gt;GitHub REST docs&lt;/a&gt;):&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ gh api repos/kadet-hub/registry/rulesets/rule-suites/4853993537 \
    --jq '{actor_name, ref, result, rules: [.rule_evaluations[] | {rule_type, result}]}'
{"actor_name":"renovate[bot]","ref":"refs/heads/main","result":"bypass","rules":[{"result":"fail","rule_type":"pull_request"},{"result":"pass","rule_type":"required_status_checks"},{"result":"pass","rule_type":"non_fast_forward"},{"result":"pass","rule_type":"deletion"}]}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;The &lt;code&gt;pull_request&lt;/code&gt; rule failed, and its details name the missing&lt;br&gt;
code-owner review. The required status checks passed on their own. The&lt;br&gt;
merge went through as &lt;code&gt;bypass&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://kadet-hub.org/blog/renovate-automerge-required-reviews/" rel="noopener noreferrer"&gt;kadet-hub.org&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>kubernetes</category>
      <category>devops</category>
      <category>kapitan</category>
      <category>gitops</category>
    </item>
    <item>
      <title>Actions Runner Controller dind image from your own registry</title>
      <dc:creator>Moep90</dc:creator>
      <pubDate>Tue, 27 Oct 2026 21:28:06 +0000</pubDate>
      <link>https://dev.to/kadet-hub/actions-runner-controller-dind-image-from-your-own-registry-4pom</link>
      <guid>https://dev.to/kadet-hub/actions-runner-controller-dind-image-from-your-own-registry-4pom</guid>
      <description>&lt;p&gt;With &lt;code&gt;containerMode.type: dind&lt;/code&gt;, the gha-runner-scale-set chart of&lt;br&gt;
actions-runner-controller (ARC) adds a Docker daemon to every runner pod.&lt;br&gt;
The chart fixes that container's image as &lt;code&gt;docker:dind&lt;/code&gt;, which the kubelet&lt;br&gt;
pulls from Docker Hub, and the current release, 0.15.0 from 1 October 2026,&lt;br&gt;
has no value to change it. This post replaces the image on the rendered&lt;br&gt;
manifests, first with Kustomize over &lt;code&gt;helm template&lt;/code&gt;, then with&lt;br&gt;
&lt;a href="https://kadet-hub.org/glossary/#kapitan" rel="noopener noreferrer"&gt;Kapitan&lt;/a&gt;'s &lt;a href="https://kadet-hub.org/generators/mutate/" rel="noopener noreferrer"&gt;mutate&lt;/a&gt;&lt;br&gt;
&lt;a href="https://kadet-hub.org/glossary/#generator" rel="noopener noreferrer"&gt;generator&lt;/a&gt;, for a pull-through mirror and for a&lt;br&gt;
registry inside an air-gapped network.&lt;/p&gt;

&lt;p&gt;Each patch was rendered from the 0.15.0 chart with Helm 4.3.1, Kustomize&lt;br&gt;
5.8.3, and Kapitan 0.36.3 with mutate 0.1.0, and applied to a kind cluster&lt;br&gt;
on Kubernetes 1.37.0. The guide &lt;a href="https://kadet-hub.org/guides/patch-helm-chart/" rel="noopener noreferrer"&gt;Patch a Helm chart&lt;/a&gt;&lt;br&gt;
compares Helm post-renderers, Kustomize, Flux and Argo CD as places for such&lt;br&gt;
a patch.&lt;/p&gt;
&lt;h2&gt;
  
  
  The dind image in gha-runner-scale-set {#arc-dind-image}
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://github.com/actions/actions-runner-controller/issues/3997" rel="noopener noreferrer"&gt;actions/actions-runner-controller#3997&lt;/a&gt;,&lt;br&gt;
opened in March 2025 and at 28 thumbs up, asks for a way to pull the dind&lt;br&gt;
image from your own registry instead of Docker Hub, whose pull limits for&lt;br&gt;
unauthenticated users it cites. Pull request #3851, which made the image&lt;br&gt;
configurable, was closed unmerged in August 2025. Closing the related #4219,&lt;br&gt;
the ARC maintainer nikola-jokic&lt;br&gt;
&lt;a href="https://github.com/actions/actions-runner-controller/pull/4219#issuecomment-3236359120" rel="noopener noreferrer"&gt;wrote&lt;/a&gt;&lt;br&gt;
that "we don't want to create a patch that will simply modify the dind&lt;br&gt;
image" and pointed to the way the chart's &lt;code&gt;values.yaml&lt;/code&gt; documents: leave&lt;br&gt;
&lt;code&gt;containerMode&lt;/code&gt; empty and write the whole runner pod spec into &lt;code&gt;template&lt;/code&gt;&lt;br&gt;
yourself. That spec is two init containers, the runner container and three&lt;br&gt;
volumes, which you then keep in step with the chart.&lt;/p&gt;

&lt;p&gt;In 0.15.0 the image is a literal in&lt;br&gt;
&lt;a href="https://github.com/actions/actions-runner-controller/blob/gha-runner-scale-set-0.15.0/charts/gha-runner-scale-set/templates/_helpers.tpl#L263-L264" rel="noopener noreferrer"&gt;&lt;code&gt;_helpers.tpl&lt;/code&gt;&lt;/a&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="pi"&gt;{{&lt;/span&gt;&lt;span class="nv"&gt;- define "gha-runner-scale-set.dind-container" -&lt;/span&gt;&lt;span class="pi"&gt;}}&lt;/span&gt;
&lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;docker:dind&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The chart &lt;code&gt;gha-runner-scale-set-experimental&lt;/code&gt;, published next to it as&lt;br&gt;
0.15.0, has the value &lt;code&gt;runner.dind.container.image&lt;/code&gt;, and renders a mirror&lt;br&gt;
image with it. Its release notes say experimental charts "are not supported&lt;br&gt;
for production workloads and may be modified or republished without&lt;br&gt;
notice."&lt;/p&gt;
&lt;h3&gt;
  
  
  Where the chart puts dind
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://github.com/actions/actions-runner-controller/blob/gha-runner-scale-set-0.15.0/charts/gha-runner-scale-set/templates/autoscalingrunnerset.yaml#L265" rel="noopener noreferrer"&gt;&lt;code&gt;autoscalingrunnerset.yaml&lt;/code&gt;&lt;/a&gt;&lt;br&gt;
tests &lt;code&gt;ge (.Capabilities.KubeVersion.Minor | int) 29&lt;/code&gt;. From Kubernetes 1.29,&lt;br&gt;
which turned on &lt;a href="https://kubernetes.io/docs/concepts/workloads/pods/sidecar-containers/" rel="noopener noreferrer"&gt;sidecar containers&lt;/a&gt;&lt;br&gt;
by default, dind is the second init container, with &lt;code&gt;restartPolicy: Always&lt;/code&gt;&lt;br&gt;
and a startup probe that runs &lt;code&gt;docker info&lt;/code&gt;, so the runner container starts&lt;br&gt;
once dockerd answers. Below 1.29 dind is the second entry in &lt;code&gt;containers&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;helm template&lt;/code&gt; has no cluster to read the version from and uses its&lt;br&gt;
built-in default, v1.37.0 for Helm 4.3.1. For an older cluster, pass&lt;br&gt;
&lt;code&gt;--kube-version&lt;/code&gt;. A patch has to look for dind in the list the chart&lt;br&gt;
rendered for that version.&lt;/p&gt;
&lt;h3&gt;
  
  
  Kustomize over helm template
&lt;/h3&gt;

&lt;p&gt;The values select dind mode and pin the runner image by tag and&lt;br&gt;
&lt;a href="https://kadet-hub.org/glossary/#digest" rel="noopener noreferrer"&gt;digest&lt;/a&gt;; the chart uses the image of the container named&lt;br&gt;
&lt;code&gt;runner&lt;/code&gt; for the init container that copies the runner's externals, too.&lt;br&gt;
&lt;code&gt;controllerServiceAccount&lt;/code&gt; names the ServiceAccount of a controller&lt;br&gt;
installed as release &lt;code&gt;arc&lt;/code&gt; in &lt;code&gt;arc-systems&lt;/code&gt;. Without it the chart looks up&lt;br&gt;
the controller Deployment in the cluster, and &lt;code&gt;helm template&lt;/code&gt;, which has&lt;br&gt;
none, fails with &lt;code&gt;No gha-rs-controller deployment found&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;githubConfigUrl&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;https://github.com/example-org&lt;/span&gt;
&lt;span class="na"&gt;githubConfigSecret&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;arc-github&lt;/span&gt;
&lt;span class="na"&gt;controllerServiceAccount&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;namespace&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;arc-systems&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;arc-gha-rs-controller&lt;/span&gt;
&lt;span class="na"&gt;containerMode&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;dind&lt;/span&gt;
&lt;span class="na"&gt;template&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;containers&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;runner&lt;/span&gt;
        &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ghcr.io/actions/actions-runner:2.338.0@sha256:4ffadc0002b2581327e06101fc8c06cd189232baf79fe561fac9caeb76f5e807&lt;/span&gt;
        &lt;span class="na"&gt;command&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/home/runner/run.sh"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Render the chart and build the patch over the output:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;helm template arc-runners oci://ghcr.io/actions/actions-runner-controller-charts/gha-runner-scale-set &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--version&lt;/span&gt; 0.15.0 &lt;span class="nt"&gt;--namespace&lt;/span&gt; arc-runners &lt;span class="nt"&gt;-f&lt;/span&gt; values.yaml &lt;span class="nt"&gt;--kube-version&lt;/span&gt; 1.37.0 &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; helm.yaml
kustomize build &lt;span class="nb"&gt;.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;with this &lt;code&gt;kustomization.yaml&lt;/code&gt; next to &lt;code&gt;helm.yaml&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;resources&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;helm.yaml&lt;/span&gt;
&lt;span class="na"&gt;images&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;docker&lt;/span&gt;
    &lt;span class="na"&gt;newName&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;mirror.gcr.io/library/docker&lt;/span&gt;
    &lt;span class="na"&gt;newTag&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;29.9.1-dind&lt;/span&gt;
    &lt;span class="na"&gt;digest&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;sha256:3e5756e2b6dad6a5f1f5deacf0a19f9eaa2581b76ae092b2d4a118162e1d4d4e&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Kustomize's &lt;code&gt;images&lt;/code&gt; transformer looks for &lt;code&gt;containers&lt;/code&gt; and&lt;br&gt;
&lt;code&gt;initContainers&lt;/code&gt; at any depth of any object, so it finds dind in the&lt;br&gt;
AutoscalingRunnerSet, a custom resource, in both layouts. The test&lt;br&gt;
rendered the chart for 1.37.0 and for 1.28.0, and in both outputs the&lt;br&gt;
dind image was the only field of the AutoscalingRunnerSet that changed. A strategic merge patch is no&lt;br&gt;
alternative here: Kustomize has no schema for the AutoscalingRunnerSet and&lt;br&gt;
replaces the whole &lt;code&gt;initContainers&lt;/code&gt; list, so a patch that names only dind&lt;br&gt;
and its image leaves a list with that one entry, without&lt;br&gt;
&lt;code&gt;init-dind-externals&lt;/code&gt; and without dind's arguments and &lt;code&gt;privileged&lt;/code&gt;.&lt;/p&gt;
&lt;h3&gt;
  
  
  Kapitan's mutate
&lt;/h3&gt;

&lt;p&gt;A &lt;a href="https://kadet-hub.org/glossary/#class" rel="noopener noreferrer"&gt;class&lt;/a&gt; pins the chart by version and by the digest of&lt;br&gt;
its OCI artifact, renders it with the same values file and passes the&lt;br&gt;
Kubernetes version to Helm:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;parameters&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;kube_version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;1.37.0&lt;/span&gt;
  &lt;span class="na"&gt;kapitan&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;dependencies&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;helm&lt;/span&gt;
        &lt;span class="na"&gt;source&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;oci://ghcr.io/actions/actions-runner-controller-charts/gha-runner-scale-set@sha256:bdfdad17cf8e45ae064c73ed6764f0c75d4bf71a6e2912e6363a43d6dfc787de&lt;/span&gt;
        &lt;span class="na"&gt;chart_name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;gha-runner-scale-set&lt;/span&gt;
        &lt;span class="na"&gt;version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;0.15.0&lt;/span&gt;
        &lt;span class="na"&gt;output_path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;charts/gha-runner-scale-set&lt;/span&gt;
    &lt;span class="na"&gt;compile&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;input_type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;helm&lt;/span&gt;
        &lt;span class="na"&gt;input_paths&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;charts/gha-runner-scale-set&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
        &lt;span class="na"&gt;output_path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;gha-runner-scale-set&lt;/span&gt;
        &lt;span class="na"&gt;helm_values_files&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;values.yaml&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
        &lt;span class="na"&gt;helm_params&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;arc-runners&lt;/span&gt;
          &lt;span class="na"&gt;namespace&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;arc-runners&lt;/span&gt;
          &lt;span class="na"&gt;kube_version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${kube_version}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;mutate's &lt;code&gt;images&lt;/code&gt; action covers Pods and the Pod templates of workload&lt;br&gt;
kinds, not custom resources, so the mutation is a JSON Patch:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;parameters&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;mutations&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;dind-image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;match&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;actions.github.com/v1alpha1&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;AutoscalingRunnerSet&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;arc-runners&lt;/span&gt;&lt;span class="pi"&gt;}&lt;/span&gt;
      &lt;span class="na"&gt;json6902&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;op&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;test&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;/spec/template/spec/initContainers/1/name&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;value&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;dind&lt;/span&gt;&lt;span class="pi"&gt;}&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;op&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;replace&lt;/span&gt;
          &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;/spec/template/spec/initContainers/1/image&lt;/span&gt;
          &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;mirror.gcr.io/library/docker:29.9.1-dind@sha256:3e5756e2b6dad6a5f1f5deacf0a19f9eaa2581b76ae092b2d4a118162e1d4d4e&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;test&lt;/code&gt; fails the compile unless the second init container is dind, and&lt;br&gt;
&lt;code&gt;replace&lt;/code&gt; sets its image. The &lt;a href="https://kadet-hub.org/glossary/#target" rel="noopener noreferrer"&gt;target&lt;/a&gt; adds mutate as a&lt;br&gt;
second compile entry on the chart's &lt;code&gt;output_path&lt;/code&gt;, as the guide shows. For a&lt;br&gt;
cluster below 1.29 the class below sets the Kubernetes version and patches&lt;br&gt;
&lt;code&gt;containers&lt;/code&gt; instead:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;parameters&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;kube_version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;1.28.0&lt;/span&gt;
  &lt;span class="na"&gt;mutations&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;dind-image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;match&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;actions.github.com/v1alpha1&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;AutoscalingRunnerSet&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;arc-runners&lt;/span&gt;&lt;span class="pi"&gt;}&lt;/span&gt;
      &lt;span class="na"&gt;json6902&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;op&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;test&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;/spec/template/spec/containers/1/name&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;value&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;dind&lt;/span&gt;&lt;span class="pi"&gt;}&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;op&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;replace&lt;/span&gt;
          &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;/spec/template/spec/containers/1/image&lt;/span&gt;
          &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;mirror.gcr.io/library/docker:29.9.1-dind@sha256:3e5756e2b6dad6a5f1f5deacf0a19f9eaa2581b76ae092b2d4a118162e1d4d4e&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Both mutated renders produced the same AutoscalingRunnerSet as Kustomize.&lt;br&gt;
The first mutation on a render for 1.28.0, where dind is not an init container,&lt;br&gt;
stops the compile:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;mutate: mutation dind-image: gha-runner-scale-set/templates/autoscalingrunnerset.yaml: json6902 path '/spec/template/spec/initContainers/1/name' does not exist
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Kapitan's helm compile entry also accepts a &lt;code&gt;kube_version&lt;/code&gt; key of its own,&lt;br&gt;
not the one under &lt;code&gt;helm_params&lt;/code&gt;. Kapitan 0.36.3&lt;br&gt;
&lt;a href="https://github.com/kapicorp/kapitan/blob/v0.36.3/kapitan/inputs/helm.py#L63-L64" rel="noopener noreferrer"&gt;passes it to Helm&lt;/a&gt;&lt;br&gt;
as &lt;code&gt;--api-versions&lt;/code&gt;, which leaves &lt;code&gt;.Capabilities.KubeVersion&lt;/code&gt; at Helm's&lt;br&gt;
default: with &lt;code&gt;kube_version: 1.28.0&lt;/code&gt; on the entry, dind was still rendered&lt;br&gt;
as an init container.&lt;/p&gt;
&lt;h3&gt;
  
  
  On kind
&lt;/h3&gt;

&lt;p&gt;The kind cluster got the four CRDs of the controller chart&lt;br&gt;
gha-runner-scale-set-controller 0.15.0 and the mutated output of the&lt;br&gt;
scale-set chart. The API server accepted both Kustomize outputs and the&lt;br&gt;
mutated 1.28.0 output in a server-side dry run, and stored the&lt;br&gt;
AutoscalingRunnerSet with these init containers (name, image,&lt;br&gt;
&lt;code&gt;restartPolicy&lt;/code&gt;):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;init-dind-externals ghcr.io/actions/actions-runner:2.338.0@sha256:4ffadc0002b2581327e06101fc8c06cd189232baf79fe561fac9caeb76f5e807
dind    mirror.gcr.io/library/docker:29.9.1-dind@sha256:3e5756e2b6dad6a5f1f5deacf0a19f9eaa2581b76ae092b2d4a118162e1d4d4e    Always
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The controller creates runner pods only for a scale set registered with&lt;br&gt;
GitHub, so the test built a Pod from the stored &lt;code&gt;spec.template&lt;/code&gt; and&lt;br&gt;
replaced the runner's command with &lt;code&gt;docker version&lt;/code&gt; followed by a sleep.&lt;br&gt;
The Pod became Ready. The kubelet reported the dind image as pulled from&lt;br&gt;
the mirror, with the digest of the patch:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;dind    mirror.gcr.io/library/docker@sha256:3e5756e2b6dad6a5f1f5deacf0a19f9eaa2581b76ae092b2d4a118162e1d4d4e    started=true
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;and the runner container, which started after dind's startup probe, printed&lt;br&gt;
the daemon's version:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;server 29.9.1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Air-gapped registry
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;mirror.gcr.io&lt;/code&gt; serves Google's cached copies of Docker Hub images, and for&lt;br&gt;
this tag it returned the same index digest as Docker Hub. In a network&lt;br&gt;
without that access, copy the image into your registry by digest:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;oras &lt;span class="nb"&gt;cp&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt; mirror.gcr.io/library/docker:29.9.1-dind@sha256:3e5756e2b6dad6a5f1f5deacf0a19f9eaa2581b76ae092b2d4a118162e1d4d4e &lt;span class="se"&gt;\&lt;/span&gt;
  registry.example.internal/library/docker:29.9.1-dind
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;oras cp&lt;/code&gt; copies the image index with every platform, so the copy has the&lt;br&gt;
same digest, &lt;code&gt;sha256:3e5756e2…&lt;/code&gt;, and the patch needs only the new registry&lt;br&gt;
in &lt;code&gt;newName&lt;/code&gt;, or in the mutation's &lt;code&gt;value&lt;/code&gt;. In the test, oras 1.3.5 copied&lt;br&gt;
the index to a local registry, which then served it with that digest and&lt;br&gt;
the platforms &lt;code&gt;linux/amd64&lt;/code&gt;, &lt;code&gt;linux/arm64v8&lt;/code&gt;, &lt;code&gt;linux/armv6&lt;/code&gt; and&lt;br&gt;
&lt;code&gt;linux/armv7&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://kadet-hub.org/blog/actions-runner-controller-dind/" rel="noopener noreferrer"&gt;kadet-hub.org&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>kubernetes</category>
      <category>devops</category>
      <category>kapitan</category>
      <category>gitops</category>
    </item>
    <item>
      <title>Bitnami PostgreSQL chart alternatives: CloudNativePG and Kapitan</title>
      <dc:creator>Moep90</dc:creator>
      <pubDate>Tue, 27 Oct 2026 10:52:38 +0000</pubDate>
      <link>https://dev.to/kadet-hub/bitnami-postgresql-chart-alternatives-cloudnativepg-and-kapitan-2nko</link>
      <guid>https://dev.to/kadet-hub/bitnami-postgresql-chart-alternatives-cloudnativepg-and-kapitan-2nko</guid>
      <description>&lt;p&gt;If you installed PostgreSQL, Redis or nginx from a Bitnami chart, the&lt;br&gt;
packaged chart you pinned and the images it pulls no longer receive&lt;br&gt;
updates, and the legacy images are meant only to buy time for a migration.&lt;br&gt;
For PostgreSQL the move with the least friction is an operator its own&lt;br&gt;
project maintains, CloudNativePG, together with images from that project.&lt;br&gt;
Its &lt;a href="https://kadet-hub.org/glossary/#crd" rel="noopener noreferrer"&gt;custom resources&lt;/a&gt; can come from the upstream &lt;code&gt;cluster&lt;/code&gt; chart or be&lt;br&gt;
rendered in your own repository, for example with the&lt;br&gt;
&lt;a href="https://kadet-hub.org/generators/cloudnative-pg/" rel="noopener noreferrer"&gt;cloudnative-pg&lt;/a&gt; &lt;a href="https://kadet-hub.org/glossary/#generator" rel="noopener noreferrer"&gt;generator&lt;/a&gt;. Both work. Neither&lt;br&gt;
replaces Bitnami one to one, because a chart or a generator only produces&lt;br&gt;
manifests; the images still need a source you trust. For the other&lt;br&gt;
Bitnami charts, &lt;a href="https://kadet-hub.org/guides/bitnami-chart-alternatives/" rel="noopener noreferrer"&gt;Bitnami chart alternatives&lt;/a&gt;&lt;br&gt;
lists the replacement chart by chart.&lt;/p&gt;
&lt;h2&gt;
  
  
  What Broadcom changed, and when
&lt;/h2&gt;

&lt;p&gt;Bitnami announced the change in&lt;br&gt;
&lt;a href="https://github.com/bitnami/containers/issues/83267" rel="noopener noreferrer"&gt;bitnami/containers#83267&lt;/a&gt;,&lt;br&gt;
opened on 16 July 2025 under the title "Upcoming changes to the Bitnami&lt;br&gt;
catalog (effective August 28th, 2025)". The issue states that "since August&lt;br&gt;
28th, we have not published new Bitnami container images or Helm charts to&lt;br&gt;
Docker Hub in OCI format". The deletion of the public catalog&lt;br&gt;
&lt;code&gt;docker.io/bitnami&lt;/code&gt; was postponed to 29 September 2025, with three 24-hour&lt;br&gt;
brownouts on 28 August, 2 September and 17 September.&lt;/p&gt;

&lt;p&gt;Images and charts are affected differently:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Versioned image tags moved to &lt;code&gt;docker.io/bitnamilegacy&lt;/code&gt;. That catalog
"will receive no further updates or support and should only be used for
temporary migration purposes". Tags based on debian-8, debian-9,
debian-10, centos-7 and ol-7 were not copied.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;docker.io/bitnami&lt;/code&gt; keeps a limited set of hardened images that "are
intended for development and are only available on the 'latest' tag".
Versioned, supported images are part of the paid Bitnami Secure Images
offering.&lt;/li&gt;
&lt;li&gt;Packaged charts "will remain available at &lt;code&gt;docker.io/bitnamicharts&lt;/code&gt; as OCI
artifacts, but they will no longer receive updates". The FAQ adds that
deploying them "will not work out-of-the-box unless you override the
bundled images with valid ones".&lt;/li&gt;
&lt;li&gt;The chart source at &lt;a href="https://github.com/bitnami/charts" rel="noopener noreferrer"&gt;bitnami/charts&lt;/a&gt;
stays available under Apache 2.0.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The documented stopgap is to keep the chart version and point every image&lt;br&gt;
repository at &lt;code&gt;bitnamilegacy&lt;/code&gt;, for PostgreSQL with&lt;br&gt;
&lt;code&gt;--set image.repository=bitnamilegacy/postgresql&lt;/code&gt; and the same for&lt;br&gt;
&lt;code&gt;os-shell&lt;/code&gt; and &lt;code&gt;postgres-exporter&lt;/code&gt;, plus&lt;br&gt;
&lt;code&gt;global.security.allowInsecureImages=true&lt;/code&gt;. That keeps a release running. It&lt;br&gt;
does not get you a patched PostgreSQL.&lt;/p&gt;

&lt;p&gt;On 24 September 2025 the CNCF&lt;br&gt;
&lt;a href="https://www.cncf.io/blog/2025/09/24/cncfs-helm-project-remains-fully-open-source-and-unaffected-by-recent-vendor-deprecations/" rel="noopener noreferrer"&gt;stated&lt;/a&gt;&lt;br&gt;
that "these changes do not impact the Helm project itself" and that&lt;br&gt;
"Bitnami Helm Charts are a specific vendor-maintained implementation". It&lt;br&gt;
advised users to "begin exploring migration or mirroring strategies". Helm&lt;br&gt;
itself stays a CNCF graduated project under Apache 2.0.&lt;/p&gt;
&lt;h2&gt;
  
  
  What the PostgreSQL chart looks like today
&lt;/h2&gt;

&lt;p&gt;On 17 October 2026, the source of &lt;code&gt;bitnami/postgresql&lt;/code&gt; in bitnami/charts is&lt;br&gt;
at chart version 17.1.2, last changed on 8 September 2026. Its default image&lt;br&gt;
is &lt;code&gt;docker.io/bitnami/postgresql:17.6.0-debian-12-r4&lt;/code&gt;, and that tag does not&lt;br&gt;
resolve. The same tag does resolve in &lt;code&gt;docker.io/bitnamilegacy&lt;/code&gt;. The OCI&lt;br&gt;
chart &lt;code&gt;docker.io/bitnamicharts/postgresql:17.1.2&lt;/code&gt; does not exist, while&lt;br&gt;
16.7.0, the version from the announcement's example, still does. The source&lt;br&gt;
keeps moving, but nothing published matches it, so consuming it means&lt;br&gt;
packaging the chart and choosing images yourself.&lt;/p&gt;
&lt;h2&gt;
  
  
  The options
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Option&lt;/th&gt;
&lt;th&gt;Who maintains the manifests&lt;/th&gt;
&lt;th&gt;Images&lt;/th&gt;
&lt;th&gt;Cost of staying&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Bitnami chart with &lt;code&gt;bitnamilegacy&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;nobody, frozen&lt;/td&gt;
&lt;td&gt;frozen, no fixes&lt;/td&gt;
&lt;td&gt;grows with every CVE&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Bitnami Secure Images&lt;/td&gt;
&lt;td&gt;Broadcom&lt;/td&gt;
&lt;td&gt;Broadcom&lt;/td&gt;
&lt;td&gt;subscription&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Upstream CloudNativePG charts&lt;/td&gt;
&lt;td&gt;CloudNativePG project&lt;/td&gt;
&lt;td&gt;&lt;code&gt;ghcr.io/cloudnative-pg/postgresql&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;chart values to track&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Operator plus rendered manifests&lt;/td&gt;
&lt;td&gt;you, from a generator&lt;/td&gt;
&lt;td&gt;&lt;code&gt;ghcr.io/cloudnative-pg/postgresql&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;an inventory and a compile step&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The last two share the operator. CloudNativePG 1.30.1 was released on 23&lt;br&gt;
September 2026 as &lt;code&gt;cnpg-1.30.1.yaml&lt;/code&gt; and as the chart &lt;code&gt;cloudnative-pg&lt;/code&gt;&lt;br&gt;
0.29.1. Moving to it is a platform change, not a chart swap. The Bitnami&lt;br&gt;
chart renders StatefulSets and Services directly; a CloudNativePG &lt;code&gt;Cluster&lt;/code&gt;&lt;br&gt;
is a resource the operator reconciles into instances, services and&lt;br&gt;
failover. Existing data&lt;br&gt;
moves with a logical import; the operator's&lt;br&gt;
&lt;a href="https://github.com/cloudnative-pg/cloudnative-pg/blob/v1.30.1/docs/src/database_import.md" rel="noopener noreferrer"&gt;import documentation&lt;/a&gt;&lt;br&gt;
describes it as &lt;code&gt;pg_dump&lt;/code&gt; over the network followed by &lt;code&gt;pg_restore&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;For the &lt;code&gt;Cluster&lt;/code&gt; itself the upstream project ships a &lt;code&gt;cluster&lt;/code&gt; chart.&lt;br&gt;
&lt;code&gt;helm template&lt;/code&gt; of &lt;code&gt;cluster&lt;/code&gt; 0.9.0 with &lt;code&gt;instances=3&lt;/code&gt; and&lt;br&gt;
&lt;code&gt;storage.size=10Gi&lt;/code&gt; produced the Cluster, a ConfigMap of monitoring queries and a &lt;code&gt;helm test&lt;/code&gt;&lt;br&gt;
Job. The image came out as &lt;code&gt;ghcr.io/cloudnative-pg/postgresql:16&lt;/code&gt;, because&lt;br&gt;
the chart's &lt;code&gt;version.postgresql&lt;/code&gt; defaults to &lt;code&gt;"16"&lt;/code&gt;. If you go this way,&lt;br&gt;
pin &lt;code&gt;cluster.imageName&lt;/code&gt;.&lt;/p&gt;
&lt;h2&gt;
  
  
  Rendering the Cluster with a generator
&lt;/h2&gt;

&lt;p&gt;The other way is to keep the manifests in your repository and render them&lt;br&gt;
from an &lt;a href="https://kadet-hub.org/glossary/#inventory" rel="noopener noreferrer"&gt;inventory&lt;/a&gt;. The &lt;a href="https://github.com/kadet-hub/examples/tree/main/bitnami" rel="noopener noreferrer"&gt;example&lt;/a&gt;&lt;br&gt;
does this with the cloudnative-pg generator 1.0.1, which is built for&lt;br&gt;
CloudNativePG 1.30.1 and the Barman Cloud Plugin 0.15.1 (see&lt;br&gt;
&lt;a href="https://kadet-hub.org/compatibility/" rel="noopener noreferrer"&gt;compatibility&lt;/a&gt;). The &lt;a href="https://kadet-hub.org/glossary/#target" rel="noopener noreferrer"&gt;target&lt;/a&gt; maps the Bitnami defaults of&lt;br&gt;
one database and one owner onto a three-instance cluster:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;parameters&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;kapitan&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;dependencies&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;oci&lt;/span&gt;
        &lt;span class="na"&gt;source&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ghcr.io/kadet-hub/cloudnative-pg:1.0.1@sha256:281417a421b0053070fbdc552f9032a3f8d564c0ca2047ec500842a0d060a8f7&lt;/span&gt;
        &lt;span class="na"&gt;output_path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;lib/cloudnative-pg&lt;/span&gt;
    &lt;span class="na"&gt;compile&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;output_path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;manifests/&lt;/span&gt;
        &lt;span class="na"&gt;input_type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;kadet&lt;/span&gt;
        &lt;span class="na"&gt;output_type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;yml&lt;/span&gt;
        &lt;span class="na"&gt;input_paths&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;lib/cloudnative-pg&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
  &lt;span class="na"&gt;namespace&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;databases&lt;/span&gt;
  &lt;span class="na"&gt;generators&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;cloudnative-pg&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;clusters&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;app-db&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;image_name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ghcr.io/cloudnative-pg/postgresql:18.6-system-trixie@sha256:5efb66993894055f0aa50bd94337c6c4ff2e2129f4d433c527151aebbf283fb0&lt;/span&gt;
          &lt;span class="na"&gt;storage&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;10Gi&lt;/span&gt;
          &lt;span class="na"&gt;bootstrap&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="na"&gt;initdb&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;database&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;app&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;owner&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;app&lt;/span&gt;&lt;span class="pi"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The generator fills &lt;code&gt;instances: 3&lt;/code&gt; and turns &lt;code&gt;storage: 10Gi&lt;/code&gt; into&lt;br&gt;
&lt;code&gt;storage.size&lt;/code&gt;. The operator creates the &lt;code&gt;app&lt;/code&gt; database, the &lt;code&gt;app&lt;/code&gt; role and&lt;br&gt;
a Secret &lt;code&gt;app-db-app&lt;/code&gt; with the credentials and a connection URI, which&lt;br&gt;
replaces the password Secret the Bitnami chart rendered.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://kadet-hub.org/glossary/#kapitan" rel="noopener noreferrer"&gt;Kapitan&lt;/a&gt; 0.36.3 checks neither &lt;a href="https://kadet-hub.org/glossary/#digest" rel="noopener noreferrer"&gt;digests&lt;/a&gt; nor &lt;a href="https://kadet-hub.org/glossary/#attestation" rel="noopener noreferrer"&gt;attestations&lt;/a&gt; when it fetches, so&lt;br&gt;
&lt;code&gt;kapitan compile --fetch&lt;/code&gt; gives you no integrity check. It also needs the&lt;br&gt;
&lt;code&gt;kapitan[oci]&lt;/code&gt; extra for OCI sources; without it the compile stops with&lt;br&gt;
"The 'oras' package is required for OCI dependencies". The example&lt;br&gt;
therefore verifies first and fetches itself:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;https://github.com/kadet-hub/registry/.github/workflows/release.yml@refs/heads/main
gh attestation verify &lt;span class="s2"&gt;"oci://ghcr.io/kadet-hub/index@&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;oras resolve ghcr.io/kadet-hub/index:latest&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--owner&lt;/span&gt; kadet-hub &lt;span class="nt"&gt;--cert-identity&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$id&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
gh attestation verify oci://ghcr.io/kadet-hub/cloudnative-pg@sha256:281417a4... &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--owner&lt;/span&gt; kadet-hub &lt;span class="nt"&gt;--cert-identity&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$id&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
oras pull &lt;span class="nt"&gt;-o&lt;/span&gt; .oci ghcr.io/kadet-hub/cloudnative-pg@sha256:281417a4...
&lt;span class="nb"&gt;tar&lt;/span&gt; &lt;span class="nt"&gt;-xf&lt;/span&gt; .oci/cloudnative-pg.tar &lt;span class="nt"&gt;-C&lt;/span&gt; lib/cloudnative-pg
uvx &lt;span class="nt"&gt;--python&lt;/span&gt; 3.13 &lt;span class="nt"&gt;--from&lt;/span&gt; &lt;span class="s1"&gt;'kapitan[omegaconf,reclass-rs]==0.36.3'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--with&lt;/span&gt; &lt;span class="nv"&gt;omegaconf&lt;/span&gt;&lt;span class="o"&gt;==&lt;/span&gt;2.4.0.dev4 kapitan compile &lt;span class="nt"&gt;--inventory-backend&lt;/span&gt; omegaconf
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Between the two &lt;code&gt;verify&lt;/code&gt; calls, &lt;code&gt;run.sh&lt;/code&gt; checks that the &lt;a href="https://kadet-hub.org/glossary/#index" rel="noopener noreferrer"&gt;attested index&lt;/a&gt;&lt;br&gt;
lists that digest as version 1.0.1 and not &lt;a href="https://kadet-hub.org/glossary/#yanked" rel="noopener noreferrer"&gt;yanked&lt;/a&gt;. The omegaconf pin is not&lt;br&gt;
optional: without it the resolver installed a newer omegaconf and the&lt;br&gt;
compile failed with &lt;code&gt;cannot import name 'ListMergeMode'&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The compile produced one file, &lt;code&gt;compiled/app-db/manifests/databases/app-db-cluster.yml&lt;/code&gt;.&lt;br&gt;
Plain &lt;code&gt;kubeconform -strict&lt;/code&gt; reports "could not find schema for Cluster",&lt;br&gt;
so the generator's shipped CRD schemas go in as the schema location:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubeconform &lt;span class="nt"&gt;-strict&lt;/span&gt; &lt;span class="nt"&gt;-summary&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-schema-location&lt;/span&gt; &lt;span class="s1"&gt;'lib/cloudnative-pg/schemas/{{.Group}}/{{.ResourceKind}}_{{.ResourceAPIVersion}}.json'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  compiled/
&lt;span class="c"&gt;# Summary: 1 resource found in 1 file - Valid: 1, Invalid: 0, Errors: 0, Skipped: 0&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The schemas are closed, so a value carried over from Bitnami habits fails.&lt;br&gt;
Adding &lt;code&gt;replicaCount: 2&lt;/code&gt; under &lt;code&gt;spec&lt;/code&gt; gives&lt;br&gt;
"additional properties 'replicaCount' not allowed". A schema check is not&lt;br&gt;
the operator's admission webhook; &lt;a href="https://kadet-hub.org/guides/checking-custom-resources-against-crds/" rel="noopener noreferrer"&gt;Validate Kubernetes custom resources&lt;br&gt;
against their CRDs&lt;/a&gt; covers what it&lt;br&gt;
misses.&lt;/p&gt;

&lt;h2&gt;
  
  
  Trade-offs
&lt;/h2&gt;

&lt;p&gt;A generator renders manifests, not images. In this example the images come&lt;br&gt;
from the CloudNativePG project, pinned by digest, and patching them is now&lt;br&gt;
your job: bump the digest, recompile, review the diff. Renovate can propose&lt;br&gt;
the generator updates with the&lt;br&gt;
&lt;a href="https://github.com/kadet-hub/renovate-config" rel="noopener noreferrer"&gt;kadet-hub preset&lt;/a&gt;, but not&lt;br&gt;
the PostgreSQL image inside your inventory.&lt;/p&gt;

&lt;p&gt;The generator does not install the operator or the Barman Cloud Plugin, and&lt;br&gt;
it is built for one CloudNativePG line. When you upgrade the operator, pick&lt;br&gt;
a generator version built for it.&lt;/p&gt;

&lt;p&gt;Kapitan is one more tool to learn. For a single database the upstream&lt;br&gt;
&lt;code&gt;cluster&lt;/code&gt; chart with a pinned image is less to set up. Rendering pays off&lt;br&gt;
when the same pattern repeats across clusters and environments, and when&lt;br&gt;
you want the rendered YAML in review instead of in a release. The&lt;br&gt;
&lt;a href="https://kadet-hub.org/guides/generator-helm-or-jinja2/" rel="noopener noreferrer"&gt;generator, Helm chart or jinja2&lt;/a&gt; guide&lt;br&gt;
compares the inputs.&lt;/p&gt;

&lt;p&gt;A local compile runs the generator's Python with your environment and&lt;br&gt;
credentials. The consumer workflow described in&lt;br&gt;
&lt;a href="https://kadet-hub.org/blog/kapitan-generators-as-supply-chain-artifacts/" rel="noopener noreferrer"&gt;Kapitan generators as supply-chain artifacts&lt;/a&gt;&lt;br&gt;
runs the same compile in a sandbox without network.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://kadet-hub.org/blog/bitnami/" rel="noopener noreferrer"&gt;kadet-hub.org&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>kubernetes</category>
      <category>devops</category>
      <category>kapitan</category>
      <category>gitops</category>
    </item>
    <item>
      <title>cdk8s vs Tanka vs Kapitan for Kubernetes config</title>
      <dc:creator>Moep90</dc:creator>
      <pubDate>Tue, 27 Oct 2026 10:52:28 +0000</pubDate>
      <link>https://dev.to/kadet-hub/cdk8s-vs-tanka-vs-kapitan-for-kubernetes-config-4og0</link>
      <guid>https://dev.to/kadet-hub/cdk8s-vs-tanka-vs-kapitan-for-kubernetes-config-4og0</guid>
      <description>&lt;p&gt;Pick cdk8s if your team writes TypeScript and wants the compiler to reject a&lt;br&gt;
bad value before anything renders. Pick Tanka if you want a small, fast,&lt;br&gt;
Kubernetes-only toolchain and accept Jsonnet as the language. Pick &lt;a href="https://kadet-hub.org/glossary/#kapitan" rel="noopener noreferrer"&gt;Kapitan&lt;/a&gt; if&lt;br&gt;
one &lt;a href="https://kadet-hub.org/glossary/#inventory" rel="noopener noreferrer"&gt;inventory&lt;/a&gt; has to drive Kubernetes and Terraform and you would rather keep&lt;br&gt;
values in YAML than maintain template code. All three rendered the same app to&lt;br&gt;
identical manifests and passed the derived-value test that Helm and Kustomize&lt;br&gt;
failed in the &lt;a href="https://kadet-hub.org/blog/same-app/" rel="noopener noreferrer"&gt;companion post&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://github.com/kadet-hub/examples/tree/main/config-tools" rel="noopener noreferrer"&gt;example&lt;/a&gt;&lt;br&gt;
builds the app with each tool, and &lt;code&gt;./check&lt;/code&gt; exits 0 only when the normalized&lt;br&gt;
manifests match per environment. Versions, commands and raw output behind&lt;br&gt;
every number are in its &lt;code&gt;NOTES.md&lt;/code&gt;.&lt;/p&gt;
&lt;h2&gt;
  
  
  The app and the check
&lt;/h2&gt;

&lt;p&gt;The spec is the companion's: namespace &lt;code&gt;shop&lt;/code&gt;, apps &lt;code&gt;web&lt;/code&gt; and &lt;code&gt;api&lt;/code&gt; with a&lt;br&gt;
Deployment, Service and ConfigMap each, restricted Pod Security, dev and prod,&lt;br&gt;
PodDisruptionBudgets in prod only, and an api ConfigMap entry &lt;code&gt;web_url&lt;/code&gt; built&lt;br&gt;
from the web Service port.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Tanka 0.39.4, jsonnet-bundler 0.6.0, k8s-libsonnet 1.36.&lt;/li&gt;
&lt;li&gt;cdk8s 2.70.111 in TypeScript 5.9.3, using the generated low-level &lt;code&gt;Kube*&lt;/code&gt;
classes for Kubernetes 1.36.&lt;/li&gt;
&lt;li&gt;Kapitan 0.36.3 with the kadet-hub &lt;a href="https://kadet-hub.org/generators/kubernetes/" rel="noopener noreferrer"&gt;kubernetes generator&lt;/a&gt;
1.2.0 pinned by &lt;a href="https://kadet-hub.org/glossary/#digest" rel="noopener noreferrer"&gt;digest&lt;/a&gt;, as in the companion minus &lt;code&gt;endpoints.json&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;code&gt;check&lt;/code&gt; sorts objects and keys and drops one label,&lt;br&gt;
&lt;code&gt;app.kubernetes.io/managed-by&lt;/code&gt;, which only the Kapitan generator sets and&lt;br&gt;
which names the tool. Nothing else is ignored. Changing one replica count in a&lt;br&gt;
copy makes it exit 1.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;./check
&lt;span class="go"&gt;dev: tanka = kapitan (6 objects)
dev: cdk8s = kapitan (6 objects)
prod: tanka = kapitan (8 objects)
prod: cdk8s = kapitan (8 objects)
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  How much you write
&lt;/h2&gt;

&lt;p&gt;Hand-written files only; lock files, fetched libraries and generated code are&lt;br&gt;
listed separately.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;web only&lt;/th&gt;
&lt;th&gt;both apps&lt;/th&gt;
&lt;th&gt;fetched or generated&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Tanka&lt;/td&gt;
&lt;td&gt;8 files, 138 lines&lt;/td&gt;
&lt;td&gt;8 files, 167 lines&lt;/td&gt;
&lt;td&gt;k8s-libsonnet, 30037 lines&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;cdk8s&lt;/td&gt;
&lt;td&gt;5 files, 148 lines&lt;/td&gt;
&lt;td&gt;5 files, 159 lines&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;imports/k8s.ts&lt;/code&gt;, 41214 lines, committed&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Kapitan&lt;/td&gt;
&lt;td&gt;5 files, 65 lines&lt;/td&gt;
&lt;td&gt;6 files, 101 lines&lt;/td&gt;
&lt;td&gt;generator, 4166 lines Python&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;For the Helm vs Kustomize vs Jsonnet comparison: Helm needed 156 lines and&lt;br&gt;
Kustomize 299 for both apps in the companion.&lt;/p&gt;

&lt;p&gt;The gap is about who owns the template. Tanka and cdk8s ship building blocks,&lt;br&gt;
not an opinion on what a Deployment looks like, so the example carries its&lt;br&gt;
own: &lt;code&gt;lib/shop/app.libsonnet&lt;/code&gt; (58 lines) and &lt;code&gt;app.ts&lt;/code&gt; (87 lines). Both spell&lt;br&gt;
out fields Kubernetes would default, such as probe thresholds and&lt;br&gt;
&lt;code&gt;defaultMode: 420&lt;/code&gt;, to match the generator. In Kapitan the template is the&lt;br&gt;
generator, maintained elsewhere and pinned by digest.&lt;/p&gt;

&lt;p&gt;cdk8s has an opinionated layer too, &lt;a href="https://cdk8s.io/docs/latest/plus/" rel="noopener noreferrer"&gt;cdk8s+&lt;/a&gt;.&lt;br&gt;
An 11-line &lt;code&gt;cdk8s-plus-36&lt;/code&gt; program for the web Deployment produced a&lt;br&gt;
&lt;code&gt;cdk8s.io/metadata.addr&lt;/code&gt; selector, 1000m/512Mi requests, 1500m/2048Mi limits,&lt;br&gt;
a read-only root filesystem and a TCP startup probe. Reasonable defaults, but&lt;br&gt;
not the spec's, so the example stays on the low-level classes.&lt;/p&gt;
&lt;h2&gt;
  
  
  Adding the second app
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;files changed&lt;/th&gt;
&lt;th&gt;lines added&lt;/th&gt;
&lt;th&gt;lines removed&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Tanka&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;29&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;cdk8s&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;16&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Kapitan&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;38&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;All three reuse their template, so &lt;code&gt;api&lt;/code&gt; is configuration only. cdk8s is&lt;br&gt;
smallest partly through layout: its per-environment values are one object&lt;br&gt;
literal per line, the Jsonnet and YAML files use one line per field. Helm needed 25 lines here and Kustomize 149.&lt;/p&gt;
&lt;h2&gt;
  
  
  A value derived from another
&lt;/h2&gt;

&lt;p&gt;All three compute &lt;code&gt;web_url&lt;/code&gt; from the web Service port. In a copy I changed&lt;br&gt;
only that port to 8081 in each tool. &lt;code&gt;check&lt;/code&gt; still passed, and all three&lt;br&gt;
rendered &lt;code&gt;web_url: http://web.shop.svc:8081&lt;/code&gt;. In the same test Helm and&lt;br&gt;
Kustomize rendered the stale &lt;code&gt;:80&lt;/code&gt; without an error. For Tanka vs Helm and&lt;br&gt;
cdk8s vs Helm this is the main difference: a language lets one value reference&lt;br&gt;
another, separate Helm values files cannot.&lt;/p&gt;

&lt;p&gt;The mechanisms differ. Jsonnet binds &lt;code&gt;$._config&lt;/code&gt; late, so an environment that&lt;br&gt;
overrides the web port also changes the api URL. In cdk8s it is an ordinary&lt;br&gt;
variable, so the dependency follows your code structure. Kapitan &lt;a href="https://kadet-hub.org/glossary/#interpolation" rel="noopener noreferrer"&gt;interpolates&lt;/a&gt;&lt;br&gt;
&lt;code&gt;${...}&lt;/code&gt; across the whole inventory, so any &lt;a href="https://kadet-hub.org/glossary/#class" rel="noopener noreferrer"&gt;class&lt;/a&gt; or &lt;a href="https://kadet-hub.org/glossary/#target" rel="noopener noreferrer"&gt;target&lt;/a&gt; can reference any&lt;br&gt;
value in another file.&lt;/p&gt;
&lt;h2&gt;
  
  
  Typos and type safety
&lt;/h2&gt;

&lt;p&gt;I misspelled &lt;code&gt;pdb_min_available&lt;/code&gt; as &lt;code&gt;pdb_min_availabe&lt;/code&gt; in the prod web values&lt;br&gt;
of each tool:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;result&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Tanka&lt;/td&gt;
&lt;td&gt;renders, exit 0, web PodDisruptionBudget missing&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;cdk8s&lt;/td&gt;
&lt;td&gt;synth fails: &lt;code&gt;error TS2561: ... 'pdbMinAvailabe' does not exist in type 'Sizing'. Did you mean to write 'pdbMinAvailable'?&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Kapitan&lt;/td&gt;
&lt;td&gt;compiles, exit 0, &lt;code&gt;web-scaling.yaml&lt;/code&gt; missing&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;cdk8s catches it because the example declares an interface for its values,&lt;br&gt;
and the generated imports type every Kubernetes field for the editor. Jsonnet&lt;br&gt;
has no types: reading a missing field fails, adding a misspelled one does not,&lt;br&gt;
unless you write &lt;code&gt;assert&lt;/code&gt;s. &lt;a href="https://github.com/grafana/jsonnet-language-server" rel="noopener noreferrer"&gt;jsonnet-language-server&lt;/a&gt;&lt;br&gt;
adds completion. Kapitan's inventory is YAML, and neither Kapitan nor the&lt;br&gt;
generator rejected the unknown key. An &lt;a href="https://kadet-hub.org/glossary/#output-diff" rel="noopener noreferrer"&gt;output diff&lt;/a&gt; in CI, like &lt;code&gt;check&lt;/code&gt;, caught&lt;br&gt;
it in all three.&lt;/p&gt;
&lt;h2&gt;
  
  
  Toolchain and speed
&lt;/h2&gt;

&lt;p&gt;Install is one run with cold caches and depends on the network. Node.js 26 and&lt;br&gt;
Python 3.13 were already present and are not counted. Render covers both&lt;br&gt;
environments, five warm runs.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;install&lt;/th&gt;
&lt;th&gt;on disk&lt;/th&gt;
&lt;th&gt;render&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Tanka&lt;/td&gt;
&lt;td&gt;12.1 s for &lt;code&gt;tk&lt;/code&gt; and &lt;code&gt;jb&lt;/code&gt;, 3.4 s &lt;code&gt;jb install&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;29 MB + 7.3 MB&lt;/td&gt;
&lt;td&gt;0.10 to 0.11 s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;cdk8s&lt;/td&gt;
&lt;td&gt;4.2 s &lt;code&gt;npm ci&lt;/code&gt;, 173 packages&lt;/td&gt;
&lt;td&gt;95 MB&lt;/td&gt;
&lt;td&gt;3.46 to 3.59 s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Kapitan&lt;/td&gt;
&lt;td&gt;63.1 s &lt;code&gt;uv run&lt;/code&gt;, 89 packages; 13.3 s generator verify and pull&lt;/td&gt;
&lt;td&gt;285 MB + 6.2 MB&lt;/td&gt;
&lt;td&gt;1.61 to 1.67 s&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Tanka is two static Go binaries and by far the fastest. Much of the cdk8s&lt;br&gt;
time is ts-node type-checking the 41214-line import file: transpile-only, the&lt;br&gt;
same synth took 2.27 to 2.36 s. Kapitan reports 0.68 s for the compile, the&lt;br&gt;
rest is &lt;code&gt;uv run&lt;/code&gt; and Python startup. Its install is large because the base&lt;br&gt;
package depends on cloud SDKs for its secret backends:&lt;br&gt;
&lt;code&gt;google-api-python-client&lt;/code&gt; alone is 112 of the 267 MB installed.&lt;/p&gt;
&lt;h2&gt;
  
  
  Reuse and packages
&lt;/h2&gt;

&lt;p&gt;Tanka libraries are Git repositories that&lt;br&gt;
&lt;a href="https://github.com/jsonnet-bundler/jsonnet-bundler" rel="noopener noreferrer"&gt;jsonnet-bundler&lt;/a&gt; pulls&lt;br&gt;
and locks by commit, with no central registry. jsonnet-bundler had no commits in the&lt;br&gt;
last 12 months and its latest release, v0.6.0, dates from August 2024.&lt;/p&gt;

&lt;p&gt;cdk8s uses npm or the package manager of its other languages, and&lt;br&gt;
&lt;a href="https://constructs.dev/" rel="noopener noreferrer"&gt;Construct Hub&lt;/a&gt; indexes published constructs.&lt;/p&gt;

&lt;p&gt;The kadet-hub &lt;a href="https://kadet-hub.org/" rel="noopener noreferrer"&gt;catalog&lt;/a&gt; lists 36 Kapitan generators, each an &lt;a href="https://kadet-hub.org/glossary/#oci-artifact" rel="noopener noreferrer"&gt;OCI artifact&lt;/a&gt;&lt;br&gt;
with a &lt;a href="https://kadet-hub.org/glossary/#attestation" rel="noopener noreferrer"&gt;build attestation&lt;/a&gt;. The registry is young: created on 2026-10-08, one&lt;br&gt;
human committer so far.&lt;/p&gt;

&lt;p&gt;All three consume Helm charts: Tanka through&lt;br&gt;
&lt;a href="https://tanka.dev/helm/" rel="noopener noreferrer"&gt;&lt;code&gt;tanka-util&lt;/code&gt;&lt;/a&gt;, cdk8s through &lt;code&gt;cdk8s import helm:&lt;/code&gt;&lt;br&gt;
and its &lt;code&gt;Helm&lt;/code&gt; construct, Kapitan through its &lt;code&gt;helm&lt;/code&gt; input type.&lt;/p&gt;
&lt;h2&gt;
  
  
  Kapitan and Jsonnet
&lt;/h2&gt;

&lt;p&gt;Kapitan has a &lt;code&gt;jsonnet&lt;/code&gt; input type, so the choice is less exclusive than it&lt;br&gt;
looks. I pointed a Kapitan target at the unchanged Tanka library and the&lt;br&gt;
vendored k8s-libsonnet, with an 11-line &lt;code&gt;main.jsonnet&lt;/code&gt; that reads the prod&lt;br&gt;
values from the inventory. After the same normalization the output matched&lt;br&gt;
&lt;code&gt;tk show environments/prod&lt;/code&gt;, all 8 objects. A team with a Jsonnet codebase&lt;br&gt;
can keep it and add inventory, &lt;a href="https://kadet-hub.org/glossary/#ref" rel="noopener noreferrer"&gt;refs&lt;/a&gt; and non-Kubernetes outputs, giving up&lt;br&gt;
&lt;code&gt;tk diff&lt;/code&gt; and &lt;code&gt;tk apply&lt;/code&gt; against a cluster.&lt;/p&gt;
&lt;h2&gt;
  
  
  Output beyond Kubernetes
&lt;/h2&gt;

&lt;p&gt;Tanka and cdk8s render Kubernetes objects. Plain Jsonnet can emit&lt;br&gt;
Terraform's &lt;a href="https://developer.hashicorp.com/terraform/language/syntax/json" rel="noopener noreferrer"&gt;JSON syntax&lt;/a&gt;,&lt;br&gt;
outside Tanka. CDKTF, the Terraform counterpart of cdk8s, was&lt;br&gt;
&lt;a href="https://github.com/hashicorp/terraform-cdk" rel="noopener noreferrer"&gt;archived on 10 December 2025&lt;/a&gt;&lt;br&gt;
with no further updates. Kapitan compiles any output from one inventory, and&lt;br&gt;
kadet-hub lists Terraform generators for AWS, AzureRM, Google Cloud and Open&lt;br&gt;
Telekom Cloud, all at 0.1.x.&lt;/p&gt;
&lt;h2&gt;
  
  
  GitOps
&lt;/h2&gt;

&lt;p&gt;Argo CD renders Helm, Kustomize and plain&lt;br&gt;
&lt;a href="https://argo-cd.readthedocs.io/en/stable/user-guide/jsonnet/" rel="noopener noreferrer"&gt;Jsonnet&lt;/a&gt;&lt;br&gt;
directories natively, Flux renders Helm and Kustomize. None of the three tools&lt;br&gt;
is native to either. A Tanka environment does not fit Argo CD's Jsonnet support:&lt;br&gt;
&lt;code&gt;tk eval&lt;/code&gt; returns nested objects (&lt;code&gt;apps.web.deployment&lt;/code&gt;) that Tanka flattens,&lt;br&gt;
and the namespace comes from &lt;code&gt;spec.json&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The usual routes are an Argo CD&lt;br&gt;
&lt;a href="https://argo-cd.readthedocs.io/en/stable/operator-manual/config-management-plugins/" rel="noopener noreferrer"&gt;config management plugin&lt;/a&gt;&lt;br&gt;
that runs the tool in the repo server, or rendering in CI and committing the&lt;br&gt;
YAML or pushing it with&lt;br&gt;
&lt;a href="https://fluxcd.io/flux/cmd/flux_push_artifact/" rel="noopener noreferrer"&gt;&lt;code&gt;flux push artifact&lt;/code&gt;&lt;/a&gt;. Tanka&lt;br&gt;
has &lt;code&gt;tk export&lt;/code&gt; for that, and Kapitan always writes files. cdk8s has one more&lt;br&gt;
option: &lt;code&gt;cdk8s synth --format helm&lt;/code&gt; writes a chart with &lt;code&gt;Chart.yaml&lt;/code&gt; and&lt;br&gt;
templates, which both Argo CD and Flux deploy natively.&lt;/p&gt;
&lt;h2&gt;
  
  
  Project activity
&lt;/h2&gt;

&lt;p&gt;Read-only GitHub API on 2026-10-17, window from 2025-10-17. Human commits&lt;br&gt;
exclude bot and automation accounts.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;repository&lt;/th&gt;
&lt;th&gt;stars&lt;/th&gt;
&lt;th&gt;commits&lt;/th&gt;
&lt;th&gt;human commits (authors)&lt;/th&gt;
&lt;th&gt;releases&lt;/th&gt;
&lt;th&gt;latest&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;grafana/tanka&lt;/td&gt;
&lt;td&gt;2690&lt;/td&gt;
&lt;td&gt;500&lt;/td&gt;
&lt;td&gt;41 (14)&lt;/td&gt;
&lt;td&gt;17&lt;/td&gt;
&lt;td&gt;v0.39.4, 2026-10-08&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;cdk8s-team/cdk8s&lt;/td&gt;
&lt;td&gt;4859&lt;/td&gt;
&lt;td&gt;499&lt;/td&gt;
&lt;td&gt;7 (4)&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;umbrella repo&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;cdk8s-team/cdk8s-core&lt;/td&gt;
&lt;td&gt;87&lt;/td&gt;
&lt;td&gt;578&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;90&lt;/td&gt;
&lt;td&gt;v2.70.111, 2026-10-12&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;cdk8s-team/cdk8s-cli&lt;/td&gt;
&lt;td&gt;41&lt;/td&gt;
&lt;td&gt;479&lt;/td&gt;
&lt;td&gt;8 (4)&lt;/td&gt;
&lt;td&gt;125&lt;/td&gt;
&lt;td&gt;v2.207.67, 2026-10-13&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;cdk8s-team/cdk8s-plus&lt;/td&gt;
&lt;td&gt;163&lt;/td&gt;
&lt;td&gt;635&lt;/td&gt;
&lt;td&gt;8 (6)&lt;/td&gt;
&lt;td&gt;330&lt;/td&gt;
&lt;td&gt;cdk8s-plus-37 v2.0.0, 2026-10-16&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;kapicorp/kapitan&lt;/td&gt;
&lt;td&gt;1931&lt;/td&gt;
&lt;td&gt;135&lt;/td&gt;
&lt;td&gt;93 (11)&lt;/td&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;v0.36.3, 2026-07-03&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;kadet-hub/registry&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;716&lt;/td&gt;
&lt;td&gt;711 (1)&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;index-47, 2026-10-16&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;441 of Tanka's 500 commits are Renovate updates. All 578 cdk8s-core commits&lt;br&gt;
were automated dependency upgrades; its frequent releases carry those.&lt;br&gt;
Kapitan releases less often and has the most human commits of the three tools.&lt;/p&gt;
&lt;h2&gt;
  
  
  Running it
&lt;/h2&gt;

&lt;p&gt;You need &lt;code&gt;tk&lt;/code&gt; and &lt;code&gt;jb&lt;/code&gt;, Node.js with npm, and uv, plus oras and a logged-in&lt;br&gt;
gh for the first generator fetch:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;git clone https://github.com/kadet-hub/examples
&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;cd &lt;/span&gt;examples/config-tools
&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;./check
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The first run installs dependencies from the lock files and verifies the&lt;br&gt;
generator's attestation before pulling it by digest; the&lt;br&gt;
&lt;a href="https://kadet-hub.org/blog/same-app/" rel="noopener noreferrer"&gt;companion post&lt;/a&gt; covers what that fetch does not check.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://kadet-hub.org/blog/config-tools/" rel="noopener noreferrer"&gt;kadet-hub.org&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>kubernetes</category>
      <category>devops</category>
      <category>kapitan</category>
      <category>gitops</category>
    </item>
    <item>
      <title>cosign verify-attestation vs gh attestation verify</title>
      <dc:creator>Moep90</dc:creator>
      <pubDate>Mon, 26 Oct 2026 21:50:37 +0000</pubDate>
      <link>https://dev.to/kadet-hub/cosign-verify-attestation-vs-gh-attestation-verify-5gp</link>
      <guid>https://dev.to/kadet-hub/cosign-verify-attestation-vs-gh-attestation-verify-5gp</guid>
      <description>&lt;p&gt;To verify an &lt;a href="https://kadet-hub.org/glossary/#oci-artifact" rel="noopener noreferrer"&gt;OCI artifact&lt;/a&gt; that carries GitHub &lt;a href="https://kadet-hub.org/glossary/#attestation" rel="noopener noreferrer"&gt;artifact attestations&lt;/a&gt;, run&lt;br&gt;
&lt;code&gt;gh attestation verify&lt;/code&gt; or &lt;code&gt;cosign verify-attestation&lt;/code&gt; with the exact&lt;br&gt;
workflow identity. gh reads the Sigstore bundles from GitHub's attestation&lt;br&gt;
API, which has them for every kadet-hub &lt;a href="https://kadet-hub.org/glossary/#generator" rel="noopener noreferrer"&gt;generator&lt;/a&gt; version. cosign reads&lt;br&gt;
them from the registry, where the release workflow has pushed them since&lt;br&gt;
17 October 2026. This post walks through both on a real generator, shows&lt;br&gt;
the failures you want to see, and ends with a script that does it in one&lt;br&gt;
call. Why a generator needs this at all is covered in&lt;br&gt;
&lt;a href="https://kadet-hub.org/guides/supply-chain-security-for-kapitan-generators/" rel="noopener noreferrer"&gt;supply-chain security for Kapitan generators&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Everything below ran on 2026-10-19 with gh 2.102.0, cosign v3.1.3 (sha256&lt;br&gt;
&lt;code&gt;4629c757…7f71&lt;/code&gt; against the release's &lt;code&gt;cosign_checksums.txt&lt;/code&gt;), jq 1.8.2 and&lt;br&gt;
oras 1.3.4.&lt;/p&gt;
&lt;h2&gt;
  
  
  What kadet-hub attests
&lt;/h2&gt;

&lt;p&gt;The &lt;a href="https://kadet-hub.org/guides/how-kadet-hub-works/" rel="noopener noreferrer"&gt;release workflow&lt;/a&gt; pushes each generator&lt;br&gt;
as an OCI artifact to &lt;code&gt;ghcr.io/kadet-hub/&amp;lt;name&amp;gt;&lt;/code&gt; and attests its &lt;a href="https://kadet-hub.org/glossary/#digest" rel="noopener noreferrer"&gt;digest&lt;/a&gt;&lt;br&gt;
up to three times: &lt;a href="https://kadet-hub.org/glossary/#provenance" rel="noopener noreferrer"&gt;SLSA provenance&lt;/a&gt; (&lt;code&gt;https://slsa.dev/provenance/v1&lt;/code&gt;), an SPDX 2.3&lt;br&gt;
&lt;a href="https://kadet-hub.org/glossary/#sbom" rel="noopener noreferrer"&gt;SBOM&lt;/a&gt; (&lt;code&gt;https://spdx.dev/Document/v2.3&lt;/code&gt;) and the diff of its fixture output&lt;br&gt;
against the previous version (&lt;code&gt;https://kadet-hub.org/output-diff/v1&lt;/code&gt;). It&lt;br&gt;
also attests the &lt;a href="https://kadet-hub.org/glossary/#index" rel="noopener noreferrer"&gt;index&lt;/a&gt; that lists every version with its digest, and&lt;br&gt;
publishes that index as a GitHub release &lt;code&gt;index-&amp;lt;serial&amp;gt;&lt;/code&gt; with a Sigstore&lt;br&gt;
bundle next to it.&lt;/p&gt;

&lt;p&gt;Each attestation of a digest also goes to ghcr.io as an OCI referrer of&lt;br&gt;
that digest, which is where cosign looks.&lt;/p&gt;

&lt;p&gt;All of them are signed by one identity:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;https://github.com/kadet-hub/registry/.github/workflows/release.yml@refs/heads/main
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A workflow on another branch of the same repository can still get a valid&lt;br&gt;
signature, but its certificate names its own file and ref. Matching this&lt;br&gt;
string exactly is what rejects it.&lt;/p&gt;

&lt;p&gt;The worked example is &lt;code&gt;mutate&lt;/code&gt; 0.1.0 from index 48, the first version&lt;br&gt;
published with referrers. It is mutate's first version, so there is&lt;br&gt;
nothing to diff against: the index records &lt;code&gt;"base": null&lt;/code&gt; with&lt;br&gt;
&lt;code&gt;"error": "no earlier listed version"&lt;/code&gt;, and the release attested&lt;br&gt;
provenance and SBOM only.&lt;/p&gt;
&lt;h2&gt;
  
  
  Start from the signed index
&lt;/h2&gt;

&lt;p&gt;The digest you pin should come from a verified index, not from a tag:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;ID&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;https://github.com/kadet-hub/registry/.github/workflows/release.yml@refs/heads/main
gh release download index-48 &lt;span class="nt"&gt;--repo&lt;/span&gt; kadet-hub/registry &lt;span class="nt"&gt;--pattern&lt;/span&gt; &lt;span class="s1"&gt;'index.json*'&lt;/span&gt;
gh attestation verify index.json &lt;span class="nt"&gt;--bundle&lt;/span&gt; index.json.sigstore.json &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--owner&lt;/span&gt; kadet-hub &lt;span class="nt"&gt;--cert-identity&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$ID&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
jq &lt;span class="nt"&gt;-r&lt;/span&gt; &lt;span class="s1"&gt;'.serial, .generators.mutate["0.1.0"].digest'&lt;/span&gt; index.json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;48
sha256:428e548bb10c6dda0105220c3a9f135d3a964a419303e30849a06f6d9e194a15
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When stdout is not a terminal, &lt;code&gt;gh attestation verify&lt;/code&gt; prints nothing on&lt;br&gt;
success: 0 bytes on stdout and stderr, exit code 0. In CI the exit code is&lt;br&gt;
your only signal unless you ask for JSON. On a TTY it prints a summary&lt;br&gt;
that starts with the policy it enforced:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;The following policy criteria will be enforced:
- Predicate type must match:............. https://slsa.dev/provenance/v1
- Source Repository Owner URI must match: https://github.com/kadet-hub
- Subject Alternative Name must match:... https://github.com/kadet-hub/registry/.github/workflows/release.yml@refs/heads/main
- OIDC Issuer must match:................ https://token.actions.githubusercontent.com
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Also check &lt;code&gt;yanked&lt;/code&gt; is null for the version, and that &lt;code&gt;serial&lt;/code&gt; is not lower&lt;br&gt;
than the last one you accepted. The serial is the rollback protection.&lt;/p&gt;
&lt;h2&gt;
  
  
  Verify the artifact with gh
&lt;/h2&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;D&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;sha256:428e548bb10c6dda0105220c3a9f135d3a964a419303e30849a06f6d9e194a15
gh attestation verify &lt;span class="s2"&gt;"oci://ghcr.io/kadet-hub/mutate@&lt;/span&gt;&lt;span class="nv"&gt;$D&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--owner&lt;/span&gt; kadet-hub &lt;span class="nt"&gt;--cert-identity&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$ID&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;--format&lt;/span&gt; json &lt;span class="se"&gt;\&lt;/span&gt;
  | jq &lt;span class="nt"&gt;-r&lt;/span&gt; &lt;span class="s1"&gt;'.[].verificationResult.statement.predicateType'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;https://slsa.dev/provenance/v1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Only provenance, the predicate type of the policy above, unless you pass&lt;br&gt;
&lt;code&gt;--predicate-type&lt;/code&gt; (&lt;a href="https://cli.github.com/manual/gh_attestation_verify" rel="noopener noreferrer"&gt;manual&lt;/a&gt;).&lt;br&gt;
With the SPDX URI it also exits 0. With the output-diff URI it fails with&lt;br&gt;
an HTTP 404 from the attestations API, since mutate 0.1.0 has none.&lt;br&gt;
&lt;code&gt;--bundle-from-oci&lt;/code&gt; reads the bundles from ghcr.io instead of the API and&lt;br&gt;
prints the same result. To see every attestation for a digest, download&lt;br&gt;
them:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;gh attestation download &lt;span class="s2"&gt;"oci://ghcr.io/kadet-hub/mutate@&lt;/span&gt;&lt;span class="nv"&gt;$D&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;--owner&lt;/span&gt; kadet-hub
jq &lt;span class="nt"&gt;-r&lt;/span&gt; &lt;span class="s1"&gt;'.dsseEnvelope.payload | @base64d | fromjson | .predicateType'&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$D&lt;/span&gt;&lt;span class="s2"&gt;.jsonl"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;https://spdx.dev/Document/v2.3
https://slsa.dev/provenance/v1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The file holds one Sigstore bundle (&lt;code&gt;application/vnd.dev.sigstore.bundle.v0.3+json&lt;/code&gt;)&lt;br&gt;
per line, each with a Rekor entry.&lt;/p&gt;
&lt;h2&gt;
  
  
  The same with cosign
&lt;/h2&gt;

&lt;p&gt;The registry lists the same two bundles as referrers of the digest:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ oras discover "ghcr.io/kadet-hub/mutate@$D"
ghcr.io/kadet-hub/mutate@sha256:428e548bb10c6dda0105220c3a9f135d3a964a419303e30849a06f6d9e194a15
└── application/vnd.dev.sigstore.bundle.v0.3+json
    ├── sha256:d197b25d45961fe128b5294f3d3741f941b77395195109b20e89d948a3db1afa
    │   └── [annotations]
    │       ├── org.opencontainers.image.created: "2026-10-19T16:59:12.130Z"
    │       ├── dev.sigstore.bundle.content: dsse-envelope
    │       └── dev.sigstore.bundle.predicateType: https://slsa.dev/provenance/v1
    └── sha256:b928bfd9c69cb04e9675212a21f6ef0bcff2b4a9420a8721a19a281f23957ae6
        └── [annotations]
            ├── org.opencontainers.image.created: "2026-10-19T16:59:18.390Z"
            ├── dev.sigstore.bundle.content: dsse-envelope
            └── dev.sigstore.bundle.predicateType: https://spdx.dev/Document/v2.3
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;cosign verifies them in place:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;cosign verify-attestation &lt;span class="nt"&gt;--type&lt;/span&gt; slsaprovenance1 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--certificate-identity&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$ID&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--certificate-oidc-issuer&lt;/span&gt; https://token.actions.githubusercontent.com &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="s2"&gt;"ghcr.io/kadet-hub/mutate@&lt;/span&gt;&lt;span class="nv"&gt;$D&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; provenance.json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Verification for ghcr.io/kadet-hub/mutate@sha256:428e548bb10c6dda0105220c3a9f135d3a964a419303e30849a06f6d9e194a15 --
The following checks were performed on each of these signatures:
  - The cosign claims were validated
  - Existence of the claims in the transparency log was verified offline
  - The code-signing certificate was verified using trusted certificate authority certificates
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That summary goes to stderr. stdout gets the verified DSSE envelope:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;jq &lt;span class="nt"&gt;-r&lt;/span&gt; &lt;span class="s1"&gt;'.payload | @base64d | fromjson | .predicateType, .subject[0].name'&lt;/span&gt; provenance.json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;https://slsa.dev/provenance/v1
ghcr.io/kadet-hub/mutate
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;slsaprovenance1&lt;/code&gt; is cosign's short name for the provenance URI. For the&lt;br&gt;
SBOM, pass the URI itself, &lt;code&gt;--type https://spdx.dev/Document/v2.3&lt;/code&gt;. The&lt;br&gt;
short name &lt;code&gt;spdxjson&lt;/code&gt; fails here, and the error lists what the digest&lt;br&gt;
carries:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Error: none of the attestations matched the predicate type: spdxjson, found: https://slsa.dev/provenance/v1,https://spdx.dev/Document/v2.3
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;cosign verify&lt;/code&gt; with the same identity and issuer also exits 0. kadet-hub signs nothing&lt;br&gt;
with &lt;code&gt;cosign sign&lt;/code&gt;; cosign v3 accepts the two attestation bundles as&lt;br&gt;
signatures and prints one entry per bundle with its predicate type as&lt;br&gt;
&lt;code&gt;critical.type&lt;/code&gt;. It checks no predicate type, so use &lt;code&gt;verify-attestation&lt;/code&gt;&lt;br&gt;
with &lt;code&gt;--type&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Versions published before 17 October 2026 have no referrers: on&lt;br&gt;
kubernetes 1.2.0, &lt;code&gt;oras discover&lt;/code&gt; lists none and &lt;code&gt;cosign verify-attestation&lt;/code&gt;&lt;br&gt;
fails with &lt;code&gt;no matching attestations:&lt;/code&gt;. For those, cosign verifies the&lt;br&gt;
bundles that gh downloads, one bundle per file. The approach is the one&lt;br&gt;
from the &lt;a href="https://blog.sigstore.dev/cosign-verify-bundles/" rel="noopener noreferrer"&gt;Sigstore blog&lt;/a&gt;&lt;br&gt;
(cosign v2.4.0), with two changes v3.1.3 needed here. On mutate's&lt;br&gt;
downloaded bundles it gives the same answer as the registry:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sed&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt; 2p &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$D&lt;/span&gt;&lt;span class="s2"&gt;.jsonl"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; provenance.sigstore.json   &lt;span class="c"&gt;# the line with slsa.dev/provenance/v1&lt;/span&gt;
cosign verify-blob-attestation &lt;span class="nt"&gt;--bundle&lt;/span&gt; provenance.sigstore.json &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--certificate-identity&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$ID&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--certificate-oidc-issuer&lt;/span&gt; https://token.actions.githubusercontent.com &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--type&lt;/span&gt; https://slsa.dev/provenance/v1 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--digest&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;D&lt;/span&gt;&lt;span class="p"&gt;#sha256&lt;/span&gt;:&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;--digestAlg&lt;/span&gt; sha256
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Verified OK
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Passing the whole two-bundle &lt;code&gt;.jsonl&lt;/code&gt; fails with &lt;code&gt;invalid character '{'&lt;br&gt;
after top-level value&lt;/code&gt;. &lt;code&gt;--type&lt;/code&gt; must match the predicate URI: the default&lt;br&gt;
&lt;code&gt;custom&lt;/code&gt; fails with &lt;code&gt;invalid predicate type, expected custom got&lt;br&gt;
https://slsa.dev/provenance/v1&lt;/code&gt;. Instead of &lt;code&gt;--digest&lt;/code&gt;, you can pass the&lt;br&gt;
manifest itself as the blob, since its sha256 is the digest:&lt;br&gt;
&lt;code&gt;oras manifest fetch "ghcr.io/kadet-hub/mutate@$D" &amp;gt; manifest.json&lt;/code&gt;.&lt;br&gt;
The index works directly with its release bundle:&lt;br&gt;
&lt;code&gt;cosign verify-blob-attestation --bundle index.json.sigstore.json … --type slsaprovenance1 index.json&lt;/code&gt;.&lt;br&gt;
The &lt;code&gt;index.json.intoto.jsonl&lt;/code&gt; asset does not, because it carries no&lt;br&gt;
certificate.&lt;/p&gt;

&lt;p&gt;cosign fetched the Sigstore public good trusted root through TUF.&lt;br&gt;
&lt;code&gt;gh attestation trusted-root&lt;/code&gt; prints two roots: public good, and GitHub's&lt;br&gt;
own instance, which GitHub uses for private repositories&lt;br&gt;
(&lt;a href="https://docs.github.com/en/actions/how-tos/secure-your-work/use-artifact-attestations/verify-attestations-offline" rel="noopener noreferrer"&gt;docs&lt;/a&gt;).&lt;br&gt;
Passing the first as &lt;code&gt;--trusted-root&lt;/code&gt; to &lt;code&gt;cosign verify-attestation&lt;/code&gt;&lt;br&gt;
verifies. The second fails with &lt;code&gt;not enough verified log entries from&lt;br&gt;
transparency log: 0 &amp;lt; 1&lt;/code&gt;, since it has no Rekor log.&lt;/p&gt;

&lt;p&gt;cosign also checks individual GitHub claims, for example&lt;br&gt;
&lt;code&gt;--certificate-github-workflow-repository kadet-hub/registry&lt;br&gt;
--certificate-github-workflow-ref refs/heads/main&lt;/code&gt;. Do not pin&lt;br&gt;
&lt;code&gt;--certificate-github-workflow-trigger push&lt;/code&gt;: kadet-hub's release workflow&lt;br&gt;
also runs on a schedule and on manual dispatch.&lt;/p&gt;
&lt;h2&gt;
  
  
  Failures you want to see
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Case&lt;/th&gt;
&lt;th&gt;gh&lt;/th&gt;
&lt;th&gt;cosign&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;identity &lt;code&gt;@refs/heads/dev&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;&lt;code&gt;verifying with issuer "sigstore.dev"&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;expected SAN value ".../release.yml@refs/heads/dev", got ".../release.yml@refs/heads/main"&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;index with &lt;code&gt;serial&lt;/code&gt; changed&lt;/td&gt;
&lt;td&gt;&lt;code&gt;verifying with issuer "sigstore.dev"&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;provided artifact digests do not match digests in statement&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;another generator's digest under &lt;code&gt;ghcr.io/kadet-hub/mutate&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;MANIFEST_UNKNOWN&lt;/code&gt; from ghcr.io&lt;/td&gt;
&lt;td&gt;&lt;code&gt;no matching attestations:&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;repository &lt;code&gt;kadet-hub/generators&lt;/code&gt;, gh with &lt;code&gt;--bundle-from-oci&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;&lt;code&gt;expected SourceRepositoryURI to be https://github.com/kadet-hub/generators, got https://github.com/kadet-hub/registry&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;expected GithubWorkflowRepository to be "kadet-hub/generators", got "kadet-hub/registry"&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;issuer &lt;code&gt;https://accounts.google.com&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;&lt;code&gt;expected Issuer to be https://accounts.google.com, got https://token.actions.githubusercontent.com&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;expected issuer value "https://accounts.google.com", got "https://token.actions.githubusercontent.com"&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;All exit 1. gh's message for a wrong identity or a tampered file is the&lt;br&gt;
same, so it tells you that verification failed, not why. Without&lt;br&gt;
&lt;code&gt;--bundle-from-oci&lt;/code&gt;, &lt;code&gt;--repo kadet-hub/generators&lt;/code&gt; fails earlier with an&lt;br&gt;
HTTP 404 from the attestations API. cosign finds no referrers for another&lt;br&gt;
generator's digest; with mutate's downloaded bundle and that digest,&lt;br&gt;
&lt;code&gt;verify-blob-attestation&lt;/code&gt; reports &lt;code&gt;provided artifact digest does not match&lt;br&gt;
any digest in statement&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;One case passes that should worry you: &lt;code&gt;--certificate-identity-regexp&lt;br&gt;
'^https://github.com/kadet-hub/'&lt;/code&gt; verifies and exits 0. That pattern&lt;br&gt;
accepts any workflow on any branch in the organization. Use the exact&lt;br&gt;
identity.&lt;/p&gt;
&lt;h2&gt;
  
  
  Read the provenance
&lt;/h2&gt;

&lt;p&gt;Verification proves who signed. The predicate tells you what ran.&lt;br&gt;
Abridged, from &lt;code&gt;jq '.payload | @base64d | fromjson | .predicate' provenance.json&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="nl"&gt;"externalParameters"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"workflow"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"ref"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"refs/heads/main"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"repository"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://github.com/kadet-hub/registry"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"path"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;".github/workflows/release.yml"&lt;/span&gt;&lt;span class="p"&gt;}}&lt;/span&gt;&lt;span class="err"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="nl"&gt;"internalParameters"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"github"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"event_name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"push"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"runner_environment"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"github-hosted"&lt;/span&gt;&lt;span class="p"&gt;}}&lt;/span&gt;&lt;span class="err"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="nl"&gt;"resolvedDependencies"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[{&lt;/span&gt;&lt;span class="nl"&gt;"uri"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"git+https://github.com/kadet-hub/registry@refs/heads/main"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"digest"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"gitCommit"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"c043c5ab78befe28cfab9a0698b5735cb39799fe"&lt;/span&gt;&lt;span class="p"&gt;}}]&lt;/span&gt;&lt;span class="err"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="nl"&gt;"runDetails"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"builder"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://github.com/kadet-hub/registry/.github/workflows/release.yml@refs/heads/main"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"metadata"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"invocationId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://github.com/kadet-hub/registry/actions/runs/39288368802/attempts/1"&lt;/span&gt;&lt;span class="p"&gt;}}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Field&lt;/th&gt;
&lt;th&gt;Check&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;externalParameters.workflow&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;path, ref and repository match the identity you trust&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;resolvedDependencies[].digest.gitCommit&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;the registry commit; read the workflow at that commit if you audit it&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;internalParameters.github.event_name&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;what started the run (&lt;code&gt;push&lt;/code&gt; here); the release workflow also runs on &lt;code&gt;schedule&lt;/code&gt; and &lt;code&gt;workflow_dispatch&lt;/code&gt;, so record it rather than require one value&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;runner_environment&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;github-hosted&lt;/code&gt;; a self-hosted runner is a different trust boundary&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;invocationId&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;the run, with its logs; the index attestation of serial 48 names the same run&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The commit here is the registry's, the merge that listed mutate, not the&lt;br&gt;
generator's. The generator source (&lt;code&gt;925525b…&lt;/code&gt; in &lt;code&gt;kadet-hub/generators&lt;/code&gt;)&lt;br&gt;
appears in the index and in the SBOM's &lt;code&gt;downloadLocation&lt;/code&gt;,&lt;br&gt;
&lt;code&gt;git+https://github.com/kadet-hub/generators.git@925525b2fabb1c2fb33c00f1b00b0d2495edce06#mutate&lt;/code&gt;.&lt;br&gt;
Those are statements by the same signed workflow, not something Sigstore&lt;br&gt;
checks.&lt;/p&gt;
&lt;h2&gt;
  
  
  One script
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://github.com/kadet-hub/examples/tree/main/verify-oci" rel="noopener noreferrer"&gt;&lt;code&gt;verify-oci/verify&lt;/code&gt;&lt;/a&gt;&lt;br&gt;
needs gh, jq and sha256sum. It downloads the latest index release,&lt;br&gt;
verifies it, enforces a minimum serial, rejects &lt;a href="https://kadet-hub.org/glossary/#yanked" rel="noopener noreferrer"&gt;yanked&lt;/a&gt; versions, verifies&lt;br&gt;
provenance, SBOM and &lt;a href="https://kadet-hub.org/glossary/#output-diff" rel="noopener noreferrer"&gt;output diff&lt;/a&gt; for the digest, checks the diff hash&lt;br&gt;
against the index, and confirms that a tampered index, another branch,&lt;br&gt;
another repository and another workflow are rejected. Any failure exits&lt;br&gt;
non-zero.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ ./verify mutate 0.1.0 48
index.json: https://slsa.dev/provenance/v1
serial: 48
mutate 0.1.0: sha256:428e548bb10c6dda0105220c3a9f135d3a964a419303e30849a06f6d9e194a15
https://slsa.dev/provenance/v1: https://github.com/kadet-hub/registry/actions/runs/39288368802/attempts/1
https://spdx.dev/Document/v2.3: https://github.com/kadet-hub/registry/actions/runs/39288368802/attempts/1
rejected as expected: tampered index
rejected as expected: branch other than main
rejected as expected: wrong source repository
rejected as expected: workflow other than release.yml
OK: mutate 0.1.0 verified against index 48
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The output diff is checked only when the index records one, which is why&lt;br&gt;
mutate 0.1.0 stops at two predicates. Versions published before the SBOM&lt;br&gt;
existed, such as keda 1.0.0, are checked for provenance only.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://kadet-hub.org/blog/verify-oci/" rel="noopener noreferrer"&gt;kadet-hub.org&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>kubernetes</category>
      <category>devops</category>
      <category>kapitan</category>
      <category>gitops</category>
    </item>
    <item>
      <title>gVisor in CI: sandboxing untrusted code on GitHub Actions</title>
      <dc:creator>Moep90</dc:creator>
      <pubDate>Mon, 26 Oct 2026 21:50:28 +0000</pubDate>
      <link>https://dev.to/kadet-hub/gvisor-in-ci-sandboxing-untrusted-code-on-github-actions-3j65</link>
      <guid>https://dev.to/kadet-hub/gvisor-in-ci-sandboxing-untrusted-code-on-github-actions-3j65</guid>
      <description>&lt;p&gt;If your CI executes code that contributors send you, a plain container is not&lt;br&gt;
a boundary. Run it under gVisor with the network off and a read-only root&lt;br&gt;
filesystem, keep a syscall trace, and treat everything else (timeouts, what&lt;br&gt;
you mount in, your tokens) as separate problems. This is what the&lt;br&gt;
&lt;a href="https://kadet-hub.org/" rel="noopener noreferrer"&gt;kadet-hub&lt;/a&gt; &lt;a href="https://kadet-hub.org/glossary/#gate" rel="noopener noreferrer"&gt;gate&lt;/a&gt; does to compile contributor-supplied &lt;a href="https://kadet-hub.org/glossary/#kapitan" rel="noopener noreferrer"&gt;Kapitan&lt;/a&gt; &lt;a href="https://kadet-hub.org/glossary/#generator" rel="noopener noreferrer"&gt;generators&lt;/a&gt;,&lt;br&gt;
and the pieces transfer to any project that runs plugins, formatters, test&lt;br&gt;
code or build scripts from a pull request.&lt;/p&gt;

&lt;h2&gt;
  
  
  The problem
&lt;/h2&gt;

&lt;p&gt;A pull request that adds a generator ships Python that the gate has to run to&lt;br&gt;
see what it produces. The same shape shows up anywhere CI runs contributor&lt;br&gt;
code: a linter plugin, a pytest suite, a Terraform provider, a code&lt;br&gt;
formatter. Once that code runs, in-language guardrails do not hold, because&lt;br&gt;
the attacker writes the code and picks the API.&lt;/p&gt;

&lt;p&gt;Three defenses that look like confinement are not. Each of these runs with&lt;br&gt;
host Python 3.13 (pinned through &lt;code&gt;uv&lt;/code&gt;), &lt;code&gt;python -I&lt;/code&gt;:&lt;/p&gt;

&lt;p&gt;A Python audit hook that blocks &lt;code&gt;subprocess&lt;/code&gt;:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;blocked: subprocess blocked by audit hook
via fork_exec
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;The hook rejects &lt;code&gt;subprocess.run&lt;/code&gt;, then &lt;code&gt;_posixsubprocess.fork_exec&lt;/code&gt; spawns&lt;br&gt;
the process directly. &lt;code&gt;fork_exec&lt;/code&gt; never raises the &lt;code&gt;subprocess.Popen&lt;/code&gt; audit&lt;br&gt;
event, so the hook does not see it. CPython's own &lt;code&gt;subprocess&lt;/code&gt; is the only&lt;br&gt;
thing that raises that event.&lt;/p&gt;

&lt;p&gt;Blocking DNS, against a raw IP (192.0.2.1 is TEST-NET-1):&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;name lookup blocked: DNS blocked
raw-IP connect reached network, result: TimeoutError timed out
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;&lt;code&gt;getaddrinfo&lt;/code&gt; is patched to refuse, and the &lt;code&gt;connect()&lt;/code&gt; to a literal address&lt;br&gt;
leaves the host anyway. Name resolution was never the exfiltration path.&lt;/p&gt;

&lt;p&gt;An rlimit and a private &lt;code&gt;HOME&lt;/code&gt;:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;HOME = $HOME
RLIMIT_NOFILE = (1048576, 1048576)
read /etc/passwd -&amp;gt; root:x:0:0:Super User:/root:/bin/bash
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;Pointing &lt;code&gt;HOME&lt;/code&gt; at a scratch directory and capping file descriptors does&lt;br&gt;
nothing to stop a read of a path outside &lt;code&gt;HOME&lt;/code&gt;. Resource limits bound&lt;br&gt;
resource use, not what the filesystem exposes.&lt;/p&gt;

&lt;h2&gt;
  
  
  What gVisor changes
&lt;/h2&gt;

&lt;p&gt;gVisor (&lt;code&gt;runsc&lt;/code&gt;) is a userspace kernel. The sandboxed process makes syscalls&lt;br&gt;
to gVisor, not to the host kernel, so the host attack surface the process can&lt;br&gt;
reach is small. Two options close the holes above. The runs below use the&lt;br&gt;
same scripts inside &lt;code&gt;runsc --rootless --network=none run&lt;/code&gt;, in an OCI bundle&lt;br&gt;
whose root filesystem is &lt;code&gt;python:3.13-slim&lt;/code&gt; exported read-only, with no host&lt;br&gt;
mounts.&lt;/p&gt;

&lt;p&gt;With &lt;code&gt;--network=none&lt;/code&gt; the sandbox has no interfaces. In this rootless&lt;br&gt;
bundle setup the network namespace from &lt;code&gt;runsc spec&lt;/code&gt; already cuts it off,&lt;br&gt;
so the flag matters most where the namespace is shared:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;name lookup blocked: DNS blocked
raw-IP connect reached network, result: OSError [Errno 101] Network is unreachable
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;The raw-IP connect now fails at once rather than reaching the network. For a&lt;br&gt;
compile that needs no network, this is the single highest-value flag.&lt;/p&gt;

&lt;p&gt;A read-only minimal rootfs with no host mounts:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;HOME = /root
RLIMIT_NOFILE = (1024, 1024)
read /etc/passwd -&amp;gt; root:x:0:0:root:/root:/bin/bash
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;The read resolves inside the container's own filesystem (note the &lt;code&gt;root&lt;/code&gt;&lt;br&gt;
GECOS field, where the host showed &lt;code&gt;Super User&lt;/code&gt;). Host paths are gone: a probe&lt;br&gt;
in the same bundle gets &lt;code&gt;FileNotFoundError&lt;/code&gt; for the host home and for&lt;br&gt;
host scratch directories, while a file bind-mounted at &lt;code&gt;/mnt/secret&lt;/code&gt; is still&lt;br&gt;
readable. That last point matters: gVisor confines the filesystem to what you&lt;br&gt;
give it, so anything you mount in is in scope.&lt;/p&gt;

&lt;p&gt;This is the kadet-hub &lt;code&gt;sandbox/run&lt;/code&gt; shape. The container runs&lt;br&gt;
&lt;code&gt;--network none --read-only --user 10001:10001 --cap-drop ALL&lt;br&gt;
--security-opt no-new-privileges&lt;/code&gt; with &lt;code&gt;/tmp&lt;/code&gt; and &lt;code&gt;/out&lt;/code&gt; as &lt;code&gt;noexec&lt;/code&gt; tmpfs,&lt;br&gt;
the project mounted read-only, and no environment inherited from the runner.&lt;/p&gt;

&lt;h2&gt;
  
  
  The one gVisor does not stop, and the trace that does
&lt;/h2&gt;

&lt;p&gt;gVisor is not an audit-hook sandbox. The &lt;code&gt;fork_exec&lt;/code&gt; bypass runs inside it&lt;br&gt;
unchanged: the process is allowed to start a child. What catches it is the&lt;br&gt;
syscall trace. Run the same script under &lt;code&gt;runsc --strace&lt;br&gt;
--strace-syscalls=execve,... --debug-log=...&lt;/code&gt; and the spawn is in the log:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;python3 E execve(... /bin/echo, [... "/bin/echo", "via", "fork_exec"], ...)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;The execve the audit hook never observed is right there, because gVisor is the&lt;br&gt;
kernel the process talks to and writes the trace outside the sandbox, where&lt;br&gt;
the code being judged cannot disable or forge it, and it follows child&lt;br&gt;
processes. The gate installs the &lt;code&gt;runsc-trace&lt;/code&gt; runtime and fails a compile&lt;br&gt;
whose trace shows any &lt;code&gt;execve&lt;/code&gt; other than the interpreter, Kapitan's known&lt;br&gt;
probes and the declared binaries, any outbound &lt;code&gt;connect&lt;/code&gt;, &lt;code&gt;sendto&lt;/code&gt; or&lt;br&gt;
&lt;code&gt;sendmsg&lt;/code&gt; to an &lt;code&gt;AF_INET&lt;/code&gt; address, a read of a decoy file, and a few other&lt;br&gt;
syscalls. That rule set is SEC-10 in the gate spec. The design reasoning is&lt;br&gt;
in &lt;a href="https://kadet-hub.org/guides/how-kadet-hub-works/" rel="noopener noreferrer"&gt;how kadet-hub works&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  What gVisor does not cover
&lt;/h2&gt;

&lt;p&gt;gVisor is a kernel boundary, not a policy. Four things stay your job.&lt;/p&gt;

&lt;p&gt;CPU and wall-clock exhaustion. A sandboxed process can spin or allocate&lt;br&gt;
forever. gVisor does not time it out; you set a job timeout and a memory&lt;br&gt;
bound. The gate runs each compile under a step timeout and a tmpfs size cap.&lt;/p&gt;

&lt;p&gt;Data you mount in. Everything inside the rootfs and every bind mount is&lt;br&gt;
reachable. Mount the contributor's input read-only, mount nothing else, and&lt;br&gt;
never mount a checkout that carries secrets or a writable host path.&lt;/p&gt;

&lt;p&gt;Your CI token. gVisor does not hide &lt;code&gt;GITHUB_TOKEN&lt;/code&gt; or other secrets that the&lt;br&gt;
workflow puts in the environment or in a mounted file. The sandbox run should&lt;br&gt;
inherit no environment from the runner, which is why &lt;code&gt;sandbox/run&lt;/code&gt; uses an&lt;br&gt;
explicit &lt;code&gt;--env-file&lt;/code&gt; and the gate passes &lt;code&gt;KUBECONFIG=/dev/null&lt;/code&gt; and an empty&lt;br&gt;
&lt;code&gt;HELM_PLUGINS&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The workflow trigger. Run untrusted pull requests under &lt;code&gt;pull_request&lt;/code&gt;, never&lt;br&gt;
&lt;code&gt;pull_request_target&lt;/code&gt;. &lt;code&gt;pull_request_target&lt;/code&gt; runs with the base repository's&lt;br&gt;
secrets and a writable token, so a sandbox around the code does not help if&lt;br&gt;
the workflow itself hands it the keys. GitHub caps the &lt;code&gt;permissions&lt;/code&gt; key for&lt;br&gt;
fork pull requests under &lt;code&gt;pull_request&lt;/code&gt;; set &lt;code&gt;permissions: {}&lt;/code&gt; anyway and add&lt;br&gt;
back only what a step needs.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cost
&lt;/h2&gt;

&lt;p&gt;Measured on one workstation, five runs each, same slim rootfs. A trivial&lt;br&gt;
&lt;code&gt;python -c pass&lt;/code&gt; takes about 0.02 s native and about 0.12 s under gVisor; a&lt;br&gt;
bare sandbox boot (&lt;code&gt;runsc do /bin/true&lt;/code&gt;) is about 0.07 s. So gVisor adds&lt;br&gt;
roughly 0.07 to 0.10 s of startup per run plus per-syscall overhead. For a CI&lt;br&gt;
compile that already takes seconds, the sandbox is not the bottleneck.&lt;/p&gt;

&lt;h2&gt;
  
  
  gVisor vs a plain Docker container (runc) {#gvisor-vs-docker}
&lt;/h2&gt;

&lt;p&gt;The runs so far are unconfined and under rootless &lt;code&gt;runsc&lt;/code&gt;. To put gVisor&lt;br&gt;
directly against a plain container, the same three scripts also run under&lt;br&gt;
&lt;code&gt;docker run&lt;/code&gt; with Docker's default runtime (runc) and under &lt;code&gt;docker run&lt;br&gt;
--runtime=runsc&lt;/code&gt;, every other option equal, so only the runtime changes.&lt;br&gt;
The script is piped in on stdin and nothing else is mounted. These were&lt;br&gt;
measured on the example repository's CI, one &lt;code&gt;ubuntu-24.04&lt;/code&gt; runner, in&lt;br&gt;
&lt;a href="https://github.com/kadet-hub/examples/actions/runs/39571252377" rel="noopener noreferrer"&gt;run 39571252377&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Swapping runc for runsc changes nothing the three scripts can show. The&lt;br&gt;
&lt;code&gt;fork_exec&lt;/code&gt; child runs under both, because neither runtime is an audit-hook&lt;br&gt;
sandbox. The raw-IP &lt;code&gt;connect&lt;/code&gt; reaches the network and times out at&lt;br&gt;
192.0.2.1 under both, because the default Docker network routes out either&lt;br&gt;
way; gVisor blocks it only with &lt;code&gt;--network=none&lt;/code&gt;, as above. The read of&lt;br&gt;
&lt;code&gt;/etc/passwd&lt;/code&gt; returns the image's own file under both, because the image,&lt;br&gt;
not the runtime, is the filesystem the process sees.&lt;/p&gt;

&lt;p&gt;What differs is the kernel the process talks to:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;runc:  Linux 6.17.0-1022-azure
runsc: Linux 4.19.0-gvisor
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;Under runc the sandboxed code's syscalls reach the runner's own kernel, so&lt;br&gt;
a bug in that kernel is reachable from the code being judged. Under runsc&lt;br&gt;
they reach gVisor's userspace kernel, and the host kernel surface the code&lt;br&gt;
can touch is small. That is what a plain container does not give you: the&lt;br&gt;
network and the spawn are bounded by flags and the trace, which you set on&lt;br&gt;
runc too, while the runtime is what moves the host kernel out of reach.&lt;/p&gt;

&lt;p&gt;Cost of the swap on the same runner, five runs each: a &lt;code&gt;python -c pass&lt;/code&gt; in&lt;br&gt;
the container is about 0.20 s under runc and about 0.27 s under runsc, so&lt;br&gt;
runsc adds roughly 0.07 s per run. The absolute figures are higher than the&lt;br&gt;
rootless &lt;code&gt;runsc ... do&lt;/code&gt; numbers above because each &lt;code&gt;docker run&lt;/code&gt; starts a&lt;br&gt;
container through the daemon, which dominates both runtimes.&lt;/p&gt;

&lt;p&gt;This measures gVisor against Docker's default runtime. It does not answer&lt;br&gt;
"gVisor vs Docker sandbox", Docker's Sandboxes product, or gVisor against&lt;br&gt;
microVM runtimes such as Firecracker or Kata, which would need microVM&lt;br&gt;
measurements this example does not take.&lt;/p&gt;

&lt;h2&gt;
  
  
  Decoy credentials
&lt;/h2&gt;

&lt;p&gt;Detection beats hope for the untargeted credential stealer. The gate seeds the&lt;br&gt;
sandbox &lt;code&gt;HOME&lt;/code&gt; with decoy &lt;code&gt;~/.aws/credentials&lt;/code&gt;, &lt;code&gt;~/.kube/config&lt;/code&gt;,&lt;br&gt;
&lt;code&gt;~/.ssh/id_ed25519&lt;/code&gt; and a &lt;code&gt;~/.gnupg&lt;/code&gt; key, plus decoy &lt;code&gt;AWS_SECRET_ACCESS_KEY&lt;/code&gt;,&lt;br&gt;
&lt;code&gt;GITHUB_TOKEN&lt;/code&gt; and &lt;code&gt;VAULT_TOKEN&lt;/code&gt; in the environment, each random per run. Two&lt;br&gt;
checks fire: the trace fails the run on any &lt;code&gt;open&lt;/code&gt; of a decoy file, and the&lt;br&gt;
output is scanned for any decoy value in plain, base64 or hex form. A stealer&lt;br&gt;
that scrapes &lt;code&gt;HOME&lt;/code&gt; and writes what it found into the generated manifests&lt;br&gt;
trips both. Code written specifically against this gate will avoid the decoys;&lt;br&gt;
the point is the untargeted collector, which is most of them.&lt;/p&gt;

&lt;h2&gt;
  
  
  A minimal GitHub Actions job
&lt;/h2&gt;

&lt;p&gt;This installs &lt;code&gt;runsc&lt;/code&gt; with a pinned release and a SHA-512 check (the digest&lt;br&gt;
and release are what kadet-hub pins), then runs the example rootless. Actions&lt;br&gt;
are pinned by commit SHA, &lt;code&gt;permissions&lt;/code&gt; is empty, and there are no caches.&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;name: sandboxed-compile
on: pull_request
permissions: {}
jobs:
  run:
    runs-on: ubuntu-24.04
    steps:
      - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
        with:
          persist-credentials: false
      - name: Install gVisor
        env:
          GVISOR_RELEASE: "20261005"
          GVISOR_SHA512: 27fed731b432d6a39d908956623fd273979fda66b3ccf4b9c12413ea417c516e18a9dc3cced403fb28efe495d64dce4251f51cf8c4d9f4a72f9833bd96b6bb35
        run: |
          curl -fsSLo runsc.tar.zstd \
            "https://storage.googleapis.com/gvisor/releases/release/${GVISOR_RELEASE}/x86_64/gvisor.tar.zstd"
          echo "${GVISOR_SHA512}  runsc.tar.zstd" | sha512sum -c -
          # runsc starts its sidecars from gvisor-bin/ next to it: unpack everything
          sudo tar --zstd -xf runsc.tar.zstd -C /usr/local/bin
      - name: Allow unprivileged user namespaces
        # ubuntu-24.04 restricts them with AppArmor; rootless runsc fails
        # with "fork/exec /proc/self/exe: permission denied" otherwise
        run: sudo sysctl -w kernel.apparmor_restrict_unprivileged_userns=0
      - name: Run contributor code sandboxed
        run: runsc --rootless --network=none --ignore-cgroups do python3 -I pr-code.py
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;The example repository runs this setup weekly on &lt;code&gt;ubuntu-24.04&lt;/code&gt;. Its first&lt;br&gt;
run failed without the &lt;code&gt;sysctl&lt;/code&gt; step: Ubuntu 24.04 lets AppArmor restrict&lt;br&gt;
unprivileged user namespaces, and rootless &lt;code&gt;runsc&lt;/code&gt; needs them. The step only&lt;br&gt;
changes the throwaway runner.&lt;/p&gt;

&lt;p&gt;The gate itself goes further, running under Docker with the &lt;code&gt;runsc-trace&lt;/code&gt;&lt;br&gt;
runtime so it can judge the syscall trace, which needs daemon configuration&lt;br&gt;
and root on the runner. The rootless &lt;code&gt;runsc ... do&lt;/code&gt; form above needs neither&lt;br&gt;
and is enough to get the network and filesystem boundary in a plain job.&lt;/p&gt;

&lt;p&gt;The escape scripts and a &lt;code&gt;run&lt;/code&gt; script that reproduces every output above are&lt;br&gt;
in &lt;a href="https://github.com/kadet-hub/examples/tree/main/gvisor-ci" rel="noopener noreferrer"&gt;the example&lt;/a&gt;.&lt;br&gt;
For how this fits the rest of the registry, see&lt;br&gt;
&lt;a href="https://kadet-hub.org/blog/kapitan-generators-as-supply-chain-artifacts/" rel="noopener noreferrer"&gt;kadet-hub generators as supply-chain artifacts&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://kadet-hub.org/blog/gvisor-ci/" rel="noopener noreferrer"&gt;kadet-hub.org&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>kubernetes</category>
      <category>devops</category>
      <category>kapitan</category>
      <category>gitops</category>
    </item>
    <item>
      <title>Helm vs Kustomize vs Kapitan on one Kubernetes app</title>
      <dc:creator>Moep90</dc:creator>
      <pubDate>Mon, 26 Oct 2026 11:28:52 +0000</pubDate>
      <link>https://dev.to/kadet-hub/helm-vs-kustomize-vs-kapitan-on-one-kubernetes-app-5dh7</link>
      <guid>https://dev.to/kadet-hub/helm-vs-kustomize-vs-kapitan-on-one-kubernetes-app-5dh7</guid>
      <description>&lt;p&gt;Comparisons between Helm, Kustomize and &lt;a href="https://kadet-hub.org/glossary/#kapitan" rel="noopener noreferrer"&gt;Kapitan&lt;/a&gt; usually compare features.&lt;br&gt;
This one compares output. The&lt;br&gt;
&lt;a href="https://github.com/kadet-hub/examples/tree/main/same-app" rel="noopener noreferrer"&gt;example&lt;/a&gt; renders&lt;br&gt;
the same small app with all three tools, and &lt;code&gt;./check&lt;/code&gt; fails unless the&lt;br&gt;
manifests match for each environment. Every number below comes from that&lt;br&gt;
directory.&lt;/p&gt;
&lt;h2&gt;
  
  
  The app
&lt;/h2&gt;

&lt;p&gt;Two services in namespace &lt;code&gt;shop&lt;/code&gt;: &lt;code&gt;web&lt;/code&gt; and &lt;code&gt;api&lt;/code&gt;. Each has a Deployment, a&lt;br&gt;
ClusterIP Service and a ConfigMap mounted into the pod. Pods run with the&lt;br&gt;
restricted Pod Security profile. The environments differ in these values:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;dev&lt;/th&gt;
&lt;th&gt;prod&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;web image tag&lt;/td&gt;
&lt;td&gt;1.5.0-rc.1&lt;/td&gt;
&lt;td&gt;1.4.2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;web replicas, requests&lt;/td&gt;
&lt;td&gt;1, 50m/64Mi&lt;/td&gt;
&lt;td&gt;3, 250m/256Mi&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;api image tag&lt;/td&gt;
&lt;td&gt;2.0.0-rc.3&lt;/td&gt;
&lt;td&gt;1.9.1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;api replicas, requests&lt;/td&gt;
&lt;td&gt;1, 50m/64Mi&lt;/td&gt;
&lt;td&gt;2, 500m/512Mi&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;log level&lt;/td&gt;
&lt;td&gt;debug&lt;/td&gt;
&lt;td&gt;info&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;PodDisruptionBudget&lt;/td&gt;
&lt;td&gt;none&lt;/td&gt;
&lt;td&gt;web and api&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The api reads the web URL from its ConfigMap, &lt;code&gt;web_url:&lt;br&gt;
http://web.shop.svc:80&lt;/code&gt;. That value matters later.&lt;/p&gt;

&lt;p&gt;The Helm side is one generic chart rendered once per app with a base and an&lt;br&gt;
environment values file. The Kustomize side is a base per app and an overlay&lt;br&gt;
per environment. The Kapitan side is an &lt;a href="https://kadet-hub.org/glossary/#inventory" rel="noopener noreferrer"&gt;inventory&lt;/a&gt; that feeds the&lt;br&gt;
&lt;a href="https://kadet-hub.org/generators/kubernetes/" rel="noopener noreferrer"&gt;kubernetes generator&lt;/a&gt; from kadet-hub, pinned by&lt;br&gt;
&lt;a href="https://kadet-hub.org/glossary/#digest" rel="noopener noreferrer"&gt;digest&lt;/a&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;dependencies&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;oci&lt;/span&gt;
    &lt;span class="na"&gt;source&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ghcr.io/kadet-hub/kubernetes:1.2.0@sha256:52017626e77dd5bf7293ce465ff0c3dc7a61dae1ae8353a56a53ffebe1bfb67c&lt;/span&gt;
    &lt;span class="na"&gt;output_path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;lib/kubernetes&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;check&lt;/code&gt; normalizes all three outputs (one document per object, sorted, keys&lt;br&gt;
sorted) and diffs them. It drops one label, &lt;code&gt;app.kubernetes.io/managed-by&lt;/code&gt;,&lt;br&gt;
because it names the tool. Everything else must match.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;./check
&lt;span class="go"&gt;dev: helm = kapitan (6 objects)
dev: kustomize = kapitan (6 objects)
prod: helm = kapitan (8 objects)
prod: kustomize = kapitan (8 objects)
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  One app
&lt;/h2&gt;

&lt;p&gt;I built &lt;code&gt;web&lt;/code&gt; alone first and counted the files and lines each tool needs,&lt;br&gt;
excluding rendered output and the fetched generator:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;files&lt;/th&gt;
&lt;th&gt;lines&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Helm&lt;/td&gt;
&lt;td&gt;9&lt;/td&gt;
&lt;td&gt;131&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Kustomize&lt;/td&gt;
&lt;td&gt;9&lt;/td&gt;
&lt;td&gt;152&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Kapitan&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;65&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The numbers need context. The generator sets fields explicitly that&lt;br&gt;
Kubernetes would default anyway: the rolling update strategy, every probe&lt;br&gt;
threshold, &lt;code&gt;defaultMode: 420&lt;/code&gt;, &lt;code&gt;sessionAffinity: None&lt;/code&gt;. The Helm templates&lt;br&gt;
and the Kustomize base spell them out so the output matches. A chart written&lt;br&gt;
from scratch would be shorter.&lt;/p&gt;

&lt;p&gt;The bigger point is who owns the template. Of Helm's 131 lines, 108 are the&lt;br&gt;
chart. You write and maintain it. Kapitan's 65 lines are inventory only. The&lt;br&gt;
templating lives in the generator, 4166 lines of Python that someone else&lt;br&gt;
maintains and that you pin, verify and run. It is the same trade as pulling a&lt;br&gt;
generic chart from a chart repository, with the difference that the generator&lt;br&gt;
reads the whole inventory instead of one values tree.&lt;/p&gt;

&lt;p&gt;The one-app prod &lt;a href="https://kadet-hub.org/glossary/#target" rel="noopener noreferrer"&gt;target&lt;/a&gt; in Kapitan, &lt;code&gt;inventory/targets/prod.yml&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;classes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;common&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;apps.web&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
&lt;span class="na"&gt;parameters&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;web_tag&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;1.4.2&lt;/span&gt;
  &lt;span class="na"&gt;log_level&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;info&lt;/span&gt;
  &lt;span class="na"&gt;components&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;web&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;replicas&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;3&lt;/span&gt;
      &lt;span class="na"&gt;resources&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;requests&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;cpu&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;250m&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;memory&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;256Mi&lt;/span&gt;&lt;span class="pi"&gt;}&lt;/span&gt;
        &lt;span class="na"&gt;limits&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;memory&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;512Mi&lt;/span&gt;&lt;span class="pi"&gt;}&lt;/span&gt;
      &lt;span class="na"&gt;pdb_min_available&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;2&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Adding a second app
&lt;/h2&gt;

&lt;p&gt;Then I added &lt;code&gt;api&lt;/code&gt; with the same pattern and diffed against the one-app&lt;br&gt;
version:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;files changed&lt;/th&gt;
&lt;th&gt;lines added&lt;/th&gt;
&lt;th&gt;lines removed&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Helm&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;25&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Kustomize&lt;/td&gt;
&lt;td&gt;9&lt;/td&gt;
&lt;td&gt;149&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Kapitan&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;47&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Helm wins this round. The chart is generic, so a new app is three values&lt;br&gt;
files. Kapitan's 47 lines include 9 for the JSON output described below; the&lt;br&gt;
api itself is 38 lines, mostly the component block in&lt;br&gt;
&lt;code&gt;classes/apps/api.yml&lt;/code&gt;. Kustomize has no parameters, so the usual move is&lt;br&gt;
copying the base and renaming. You can share one base through &lt;code&gt;namePrefix&lt;/code&gt;&lt;br&gt;
and replacements, but labels, selectors, container names and mount paths&lt;br&gt;
then become patches of their own.&lt;/p&gt;
&lt;h2&gt;
  
  
  A value derived from another
&lt;/h2&gt;

&lt;p&gt;The api needs the web Service URL. In Kapitan it is an &lt;a href="https://kadet-hub.org/glossary/#interpolation" rel="noopener noreferrer"&gt;interpolation&lt;/a&gt;, so it&lt;br&gt;
follows the web definition:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;web_url&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;http://web.${namespace}.svc:${components:web:ports:http:service_port}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Helm releases cannot read each other's values, so the Helm side repeats the&lt;br&gt;
literal. The Kustomize side does too:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-rn&lt;/span&gt; web_url helm kustomize kapitan/inventory
&lt;span class="go"&gt;helm/values/api-prod.yaml:6:  web_url: http://web.shop.svc:80
helm/values/api-dev.yaml:6:  web_url: http://web.shop.svc:80
kustomize/overlays/prod/api.yaml:7:  web_url: http://web.shop.svc:80
kustomize/overlays/dev/api.yaml:7:  web_url: http://web.shop.svc:80
kapitan/inventory/classes/apps/api.yml:20:            web_url:
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To show what that costs, I changed only the web Service port to 8081 in each&lt;br&gt;
tool, in a copy of the example, and ran &lt;code&gt;check&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="go"&gt;--- /tmp/tmp.TpmPDYldGS/prod.kapitan
+++ /tmp/tmp.TpmPDYldGS/prod.helm
@@ -1,7 +1,7 @@
 apiVersion: v1
 data:
   log_level: info
-  web_url: http://web.shop.svc:8081
+  web_url: http://web.shop.svc:80
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The same diff appears for Kustomize and for dev, four mismatches in total,&lt;br&gt;
and &lt;code&gt;check&lt;/code&gt; exits 1. Kapitan followed the change. In Helm and Kustomize the&lt;br&gt;
stale URL renders without an error.&lt;/p&gt;

&lt;p&gt;Both tools have a workaround. An umbrella chart with &lt;code&gt;global&lt;/code&gt; values can&lt;br&gt;
share an input between subcharts, but you restructure the values around it.&lt;br&gt;
Kustomize &lt;code&gt;replacements&lt;/code&gt; can copy the Service port into part of the string&lt;br&gt;
with &lt;code&gt;delimiter: ":"&lt;/code&gt; and &lt;code&gt;index: 2&lt;/code&gt;. I tested that in a copy of the prod&lt;br&gt;
overlay: six lines, and the URL followed. It works per overlay and per field,&lt;br&gt;
and it breaks when the string format changes. The example keeps the&lt;br&gt;
literal.&lt;/p&gt;
&lt;h2&gt;
  
  
  Output that is not Kubernetes
&lt;/h2&gt;

&lt;p&gt;The same inventory also renders &lt;code&gt;endpoints.json&lt;/code&gt; per environment, through a&lt;br&gt;
six-line jinja2 template and a three-line compile entry:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="nl"&gt;"api"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"url"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"http://api.shop.svc:80"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"image"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ghcr.io/example/api:1.9.1"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="nl"&gt;"web"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"url"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"http://web.shop.svc:80"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"image"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ghcr.io/example/web:1.4.2"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That file can feed smoke tests, monitoring or docs. With the port change above&lt;br&gt;
it says 8081 as well. Helm and Kustomize render Kubernetes objects only. The&lt;br&gt;
equivalent there is a separate script that parses values files or rendered&lt;br&gt;
YAML, and it is one more place to keep in sync. The same mechanism covers&lt;br&gt;
Terraform: kadet-hub lists generators for AWS, AzureRM, Google Cloud and Open&lt;br&gt;
Telekom Cloud that read the same inventory.&lt;/p&gt;
&lt;h2&gt;
  
  
  What Helm and Kustomize do better
&lt;/h2&gt;

&lt;p&gt;Helm's ecosystem is the main reason to stay. Most vendors publish a chart,&lt;br&gt;
Artifact Hub makes them searchable, and &lt;code&gt;helm install&lt;/code&gt;, &lt;code&gt;upgrade&lt;/code&gt; and &lt;code&gt;rollback&lt;/code&gt;&lt;br&gt;
keep release history in the cluster. Kapitan has no release concept. It&lt;br&gt;
writes files, and you apply them with &lt;code&gt;kubectl&lt;/code&gt;, Argo CD or Flux. For&lt;br&gt;
third-party software, a vendor chart maintained by the vendor beats a&lt;br&gt;
generator maintained by someone else. Kapitan can render charts through its&lt;br&gt;
&lt;code&gt;helm&lt;/code&gt; input type, so you do not have to give them up.&lt;/p&gt;

&lt;p&gt;Kustomize ships inside &lt;code&gt;kubectl&lt;/code&gt;. In this example &lt;code&gt;kubectl kustomize&lt;/code&gt; and the&lt;br&gt;
standalone binary produced identical output. Nothing to install, and patching&lt;br&gt;
vendor YAML you do not control is where Kustomize is strongest. Kapitan has a&lt;br&gt;
&lt;code&gt;kustomize&lt;/code&gt; input type for that case too.&lt;/p&gt;

&lt;p&gt;Both have a gentler start. A values file and a patch are plain YAML.&lt;br&gt;
Kapitan asks you to learn &lt;a href="https://kadet-hub.org/glossary/#class" rel="noopener noreferrer"&gt;classes&lt;/a&gt;, targets, interpolation and the generator's&lt;br&gt;
schema before the first manifest, and it needs a Python toolchain. Argo CD and Flux render Helm and Kustomize natively. With Kapitan you&lt;br&gt;
commit compiled output or add a compile step in CI.&lt;/p&gt;
&lt;h2&gt;
  
  
  Fetching generators honestly
&lt;/h2&gt;

&lt;p&gt;Kapitan 0.36.3 checks neither &lt;a href="https://kadet-hub.org/glossary/#attestation" rel="noopener noreferrer"&gt;attestations&lt;/a&gt; nor digests when it fetches&lt;br&gt;
dependencies, so &lt;code&gt;kapitan compile --fetch&lt;/code&gt; gives no integrity check. It also&lt;br&gt;
runs generator code with your environment and credentials. &lt;code&gt;check&lt;/code&gt; does not&lt;br&gt;
use &lt;code&gt;--fetch&lt;/code&gt;. On first run it verifies the generator's attestation with&lt;br&gt;
&lt;code&gt;gh attestation verify&lt;/code&gt; against the kadet-hub release workflow, pulls by&lt;br&gt;
digest with &lt;code&gt;oras&lt;/code&gt; and unpacks into &lt;code&gt;kapitan/lib/kubernetes&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;That is not the full consumer flow. It does not check the &lt;a href="https://kadet-hub.org/glossary/#index" rel="noopener noreferrer"&gt;attested index&lt;/a&gt;&lt;br&gt;
for &lt;a href="https://kadet-hub.org/glossary/#yanked" rel="noopener noreferrer"&gt;yanked&lt;/a&gt; versions, and it compiles on your machine without a sandbox. In&lt;br&gt;
CI, use the &lt;a href="https://github.com/kadet-hub/registry/blob/main/docs/consumers.md#workflow" rel="noopener noreferrer"&gt;consumer&lt;br&gt;
workflow&lt;/a&gt;,&lt;br&gt;
which does both and compiles in gVisor without network. The&lt;br&gt;
&lt;a href="https://kadet-hub.org/guides/supply-chain-security-for-kapitan-generators/" rel="noopener noreferrer"&gt;supply-chain guide&lt;/a&gt;&lt;br&gt;
explains why.&lt;/p&gt;
&lt;h2&gt;
  
  
  Running it
&lt;/h2&gt;

&lt;p&gt;You need helm, kustomize and uv, plus oras and a logged-in gh for the first fetch:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;git clone https://github.com/kadet-hub/examples
&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;cd &lt;/span&gt;examples/same-app
&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;./check
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;check&lt;/code&gt; runs Kapitan through &lt;code&gt;uv run&lt;/code&gt; with&lt;br&gt;
&lt;code&gt;kapitan[omegaconf,reclass-rs]==0.36.3&lt;/code&gt;, the version the kadet-hub sandbox&lt;br&gt;
pins. Versions and every command behind the numbers above are in &lt;code&gt;NOTES.md&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;If your stack is one or two apps built from vendor charts, Helm and&lt;br&gt;
Kustomize are the better fit. Kapitan pays off once values start depending&lt;br&gt;
on each other across apps, or the same facts need to reach Kubernetes,&lt;br&gt;
Terraform and docs. To start, see &lt;a href="https://kadet-hub.org/guides/generator-helm-or-jinja2/" rel="noopener noreferrer"&gt;Generator, Helm chart or&lt;br&gt;
jinja2?&lt;/a&gt; and the&lt;br&gt;
&lt;a href="https://kadet-hub.org/" rel="noopener noreferrer"&gt;catalog&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://kadet-hub.org/blog/same-app/" rel="noopener noreferrer"&gt;kadet-hub.org&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>kubernetes</category>
      <category>devops</category>
      <category>kapitan</category>
      <category>gitops</category>
    </item>
    <item>
      <title>kube-prometheus-stack default Grafana dashboards, one by one</title>
      <dc:creator>Moep90</dc:creator>
      <pubDate>Mon, 26 Oct 2026 11:28:42 +0000</pubDate>
      <link>https://dev.to/kadet-hub/kube-prometheus-stack-default-grafana-dashboards-one-by-one-545g</link>
      <guid>https://dev.to/kadet-hub/kube-prometheus-stack-default-grafana-dashboards-one-by-one-545g</guid>
      <description>&lt;p&gt;kube-prometheus-stack 92.11.0, the release of 20 October 2026, renders 29&lt;br&gt;
Grafana dashboards by default, each as a ConfigMap that Grafana's sidecar&lt;br&gt;
loads. &lt;code&gt;grafana.defaultDashboardsEnabled&lt;/code&gt; turns all of them off at once,&lt;br&gt;
and the switches for scraped components remove some of them along with&lt;br&gt;
the scraping. No value turns off a single dashboard. This post shows which&lt;br&gt;
dashboard depends on which switch and removes single dashboards from the&lt;br&gt;
rendered manifests, first with Kustomize over &lt;code&gt;helm template&lt;/code&gt;, then with&lt;br&gt;
&lt;a href="https://kadet-hub.org/glossary/#kapitan" rel="noopener noreferrer"&gt;Kapitan&lt;/a&gt;'s &lt;a href="https://kadet-hub.org/generators/mutate/" rel="noopener noreferrer"&gt;mutate&lt;/a&gt;&lt;br&gt;
&lt;a href="https://kadet-hub.org/glossary/#generator" rel="noopener noreferrer"&gt;generator&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Each patch was rendered from the 92.11.0 chart with Helm 4.3.1, Kustomize&lt;br&gt;
5.8.3, and Kapitan 0.36.3 with mutate 0.1.0, and the result was installed&lt;br&gt;
on a kind cluster on Kubernetes 1.37.0. The guide&lt;br&gt;
&lt;a href="https://kadet-hub.org/guides/patch-helm-chart/" rel="noopener noreferrer"&gt;Patch a Helm chart&lt;/a&gt; compares the places such a&lt;br&gt;
patch can live.&lt;/p&gt;
&lt;h2&gt;
  
  
  Disable single default dashboards {#disable-default-dashboards}
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://github.com/prometheus-community/helm-charts/issues/3763" rel="noopener noreferrer"&gt;prometheus-community/helm-charts#3763&lt;/a&gt;,&lt;br&gt;
opened in September 2023 and at 23 thumbs up, asks for a switch per default&lt;br&gt;
dashboard. The reporter's workaround, overwriting the ConfigMaps by hand,&lt;br&gt;
lasts until the next deploy. The one maintainer reply, from jkroepke in&lt;br&gt;
October 2025, is "just open a PR to solve this." Pull request #7015, opened&lt;br&gt;
in June 2026, adds a map &lt;code&gt;grafana.defaultDashboardsDisabled&lt;/code&gt;. It has no&lt;br&gt;
review and conflicts with the current chart.&lt;/p&gt;

&lt;p&gt;Every dashboard template starts with the same condition, here from&lt;br&gt;
&lt;a href="https://github.com/prometheus-community/helm-charts/blob/kube-prometheus-stack-92.11.0/charts/kube-prometheus-stack/templates/grafana/dashboards-1.14/grafana-overview.yaml#L7-L12" rel="noopener noreferrer"&gt;&lt;code&gt;grafana-overview.yaml&lt;/code&gt;&lt;/a&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="pi"&gt;{{&lt;/span&gt;&lt;span class="nv"&gt;- if and (or .Values.grafana.enabled .Values.grafana.forceDeployDashboards) (semverCompare "&amp;gt;=1.14.0-0" $kubeTargetVersion) (semverCompare "&amp;lt;9.9.9-9" $kubeTargetVersion) .Values.grafana.defaultDashboardsEnabled&lt;/span&gt; &lt;span class="pi"&gt;}}&lt;/span&gt;
&lt;span class="na"&gt;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;v1&lt;/span&gt;
&lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ConfigMap&lt;/span&gt;
&lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;namespace&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{{&lt;/span&gt; &lt;span class="nv"&gt;template "kube-prometheus-stack-grafana.namespace" .&lt;/span&gt; &lt;span class="pi"&gt;}}&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{{&lt;/span&gt; &lt;span class="nv"&gt;printf "%s-%s" (include "kube-prometheus-stack.fullname" $) "grafana-overview" | trunc 63 | trimSuffix "-"&lt;/span&gt; &lt;span class="pi"&gt;}}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Some templates add a component switch to that condition. Sixteen add&lt;br&gt;
nothing, so &lt;code&gt;defaultDashboardsEnabled&lt;/code&gt; is their only switch:&lt;br&gt;
cluster-total, grafana-overview, the eight k8s-resources dashboards from&lt;br&gt;
cluster to workloads-namespace, namespace-by-pod, namespace-by-workload,&lt;br&gt;
persistentvolumesusage, pod-total, prometheus and workload-total.&lt;/p&gt;
&lt;h3&gt;
  
  
  Which dashboard hangs on which switch
&lt;/h3&gt;

&lt;p&gt;Setting a component's &lt;code&gt;enabled&lt;/code&gt; to false removes its dashboard and also&lt;br&gt;
what the chart renders to monitor that component. Rendered with each&lt;br&gt;
switch set to false in turn, compared with the default render:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Switch set to false&lt;/th&gt;
&lt;th&gt;Dashboard ConfigMaps removed&lt;/th&gt;
&lt;th&gt;Also removed&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;kubeApiServer.enabled&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;apiserver&lt;/td&gt;
&lt;td&gt;ServiceMonitor, 4 PrometheusRules&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;kubeControllerManager.enabled&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;controller-manager&lt;/td&gt;
&lt;td&gt;Service, ServiceMonitor, 1 PrometheusRule&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;kubeScheduler.enabled&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;scheduler&lt;/td&gt;
&lt;td&gt;Service, ServiceMonitor, 2 PrometheusRules&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;kubeEtcd.enabled&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;etcd&lt;/td&gt;
&lt;td&gt;Service, ServiceMonitor, 1 PrometheusRule&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;kubeProxy.enabled&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;proxy&lt;/td&gt;
&lt;td&gt;Service, ServiceMonitor, 1 PrometheusRule&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;coreDns.enabled&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;k8s-coredns&lt;/td&gt;
&lt;td&gt;Service, ServiceMonitor&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;kubelet.enabled&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;kubelet&lt;/td&gt;
&lt;td&gt;ServiceMonitor, PrometheusRule kubelet.rules&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;nodeExporter.enabled&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;node-cluster-rsrc-use, node-rsrc-use, nodes, nodes-aix, nodes-darwin&lt;/td&gt;
&lt;td&gt;the node-exporter DaemonSet, Service, ServiceAccount and ServiceMonitor&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;alertmanager.enabled&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;alertmanager-overview&lt;/td&gt;
&lt;td&gt;Alertmanager, its Secret, Service, ServiceAccount and ServiceMonitor&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Where a component is not scraped at all, as the control plane of many&lt;br&gt;
managed clusters, the switch removes the dashboard together with the&lt;br&gt;
monitoring objects that have nothing to scrape. To keep scraping and&lt;br&gt;
alerting for a component and drop only its dashboard, or to drop one of&lt;br&gt;
the sixteen dashboards without a switch, delete the ConfigMap from the&lt;br&gt;
rendered output.&lt;br&gt;
&lt;code&gt;nodeExporter.forceDeployDashboards&lt;/code&gt; and&lt;br&gt;
&lt;code&gt;alertmanager.forceDeployDashboards&lt;/code&gt; keep the dashboards of a disabled&lt;br&gt;
node-exporter or Alertmanager, and &lt;code&gt;nodeExporter.operatingSystems.aix.enabled&lt;/code&gt;&lt;br&gt;
and &lt;code&gt;.darwin.enabled&lt;/code&gt; remove only nodes-aix and nodes-darwin.&lt;/p&gt;
&lt;h3&gt;
  
  
  Dashboard names
&lt;/h3&gt;

&lt;p&gt;A dashboard ConfigMap is named &lt;code&gt;&amp;lt;fullname&amp;gt;-&amp;lt;dashboard&amp;gt;&lt;/code&gt;, cut to 63&lt;br&gt;
characters. The chart's &lt;code&gt;kube-prometheus-stack.fullname&lt;/code&gt; starts from the&lt;br&gt;
release name when that contains &lt;code&gt;kube-prometheus-stack&lt;/code&gt;, and from&lt;br&gt;
&lt;code&gt;&amp;lt;release&amp;gt;-kube-prometheus-stack&lt;/code&gt; otherwise, and cuts either to 26&lt;br&gt;
characters. Release &lt;code&gt;kube-prometheus-stack&lt;/code&gt; therefore names a dashboard&lt;br&gt;
&lt;code&gt;kube-prometheus-stack-grafana-overview&lt;/code&gt;, and release &lt;code&gt;monitoring&lt;/code&gt;, whose&lt;br&gt;
&lt;code&gt;monitoring-kube-prometheus-stack&lt;/code&gt; is cut to &lt;code&gt;monitoring-kube-prometheus&lt;/code&gt;,&lt;br&gt;
names it &lt;code&gt;monitoring-kube-prometheus-grafana-overview&lt;/code&gt;. The 63-character&lt;br&gt;
cut never applies to the default dashboards: the longest name, for a&lt;br&gt;
fullname of the full 26 characters, has 60.&lt;/p&gt;
&lt;h3&gt;
  
  
  Kustomize over helm template
&lt;/h3&gt;

&lt;p&gt;For release &lt;code&gt;monitoring&lt;/code&gt; in namespace &lt;code&gt;monitoring&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;helm template monitoring oci://ghcr.io/prometheus-community/charts/kube-prometheus-stack &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--version&lt;/span&gt; 92.11.0 &lt;span class="nt"&gt;--namespace&lt;/span&gt; monitoring &lt;span class="nt"&gt;--skip-tests&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; helm.yaml
kustomize build &lt;span class="nb"&gt;.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;--skip-tests&lt;/code&gt; leaves out the Grafana subchart's test Pod, as Kapitan's&lt;br&gt;
&lt;code&gt;helm&lt;/code&gt; input does by default.&lt;/p&gt;

&lt;p&gt;with this &lt;code&gt;kustomization.yaml&lt;/code&gt; next to &lt;code&gt;helm.yaml&lt;/code&gt;, which deletes the&lt;br&gt;
dashboards Grafana Overview and Kubernetes / Compute Resources /&lt;br&gt;
Multi-Cluster:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;resources&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;helm.yaml&lt;/span&gt;
&lt;span class="na"&gt;patches&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;patch&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|-&lt;/span&gt;
      &lt;span class="s"&gt;$patch: delete&lt;/span&gt;
      &lt;span class="s"&gt;apiVersion: v1&lt;/span&gt;
      &lt;span class="s"&gt;kind: ConfigMap&lt;/span&gt;
      &lt;span class="s"&gt;metadata:&lt;/span&gt;
        &lt;span class="s"&gt;name: monitoring-kube-prometheus-grafana-overview&lt;/span&gt;
        &lt;span class="s"&gt;namespace: monitoring&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;patch&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|-&lt;/span&gt;
      &lt;span class="s"&gt;$patch: delete&lt;/span&gt;
      &lt;span class="s"&gt;apiVersion: v1&lt;/span&gt;
      &lt;span class="s"&gt;kind: ConfigMap&lt;/span&gt;
      &lt;span class="s"&gt;metadata:&lt;/span&gt;
        &lt;span class="s"&gt;name: monitoring-kube-prometheus-k8s-resources-multicluster&lt;/span&gt;
        &lt;span class="s"&gt;namespace: monitoring&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A strategic merge patch with &lt;code&gt;$patch: delete&lt;/code&gt; removes the object it names.&lt;br&gt;
Kustomize's output kept every other object of the render; it only dropped&lt;br&gt;
&lt;code&gt;metadata.annotations&lt;/code&gt; where the chart renders it empty or null. Over a&lt;br&gt;
render for release &lt;code&gt;kube-prometheus-stack&lt;/code&gt;, where the names differ, the&lt;br&gt;
build fails instead of passing without effect:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Error: no resource matches strategic merge patch "ConfigMap.v1.[noGrp]/monitoring-kube-prometheus-grafana-overview.monitoring": no matches for Id ConfigMap.v1.[noGrp]/monitoring-kube-prometheus-grafana-overview.monitoring; failed to find unique target for patch ConfigMap.v1.[noGrp]/monitoring-kube-prometheus-grafana-overview.monitoring
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Kapitan's mutate
&lt;/h3&gt;

&lt;p&gt;The &lt;a href="https://kadet-hub.org/glossary/#class" rel="noopener noreferrer"&gt;class&lt;/a&gt; pins the chart by version and by the&lt;br&gt;
&lt;a href="https://kadet-hub.org/glossary/#digest" rel="noopener noreferrer"&gt;digest&lt;/a&gt; of its OCI artifact, and renders it as release&lt;br&gt;
&lt;code&gt;monitoring&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;parameters&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;kapitan&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;dependencies&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;helm&lt;/span&gt;
        &lt;span class="na"&gt;source&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;oci://ghcr.io/prometheus-community/charts/kube-prometheus-stack@sha256:5b9b0215085f00f61c0fb2aa01ec402609d91c0a6a91bc84ca85cce2ed69e635&lt;/span&gt;
        &lt;span class="na"&gt;chart_name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;kube-prometheus-stack&lt;/span&gt;
        &lt;span class="na"&gt;version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;92.11.0&lt;/span&gt;
        &lt;span class="na"&gt;output_path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;charts/kube-prometheus-stack&lt;/span&gt;
    &lt;span class="na"&gt;compile&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;input_type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;helm&lt;/span&gt;
        &lt;span class="na"&gt;input_paths&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;charts/kube-prometheus-stack&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
        &lt;span class="na"&gt;output_path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;kube-prometheus-stack&lt;/span&gt;
        &lt;span class="na"&gt;helm_params&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;monitoring&lt;/span&gt;
          &lt;span class="na"&gt;namespace&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;monitoring&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The mutations delete the same two ConfigMaps:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;parameters&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;mutations&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;no-grafana-overview-dashboard&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;match&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;v1&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;ConfigMap&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;monitoring-kube-prometheus-grafana-overview&lt;/span&gt;&lt;span class="pi"&gt;}&lt;/span&gt;
      &lt;span class="na"&gt;delete&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
    &lt;span class="na"&gt;no-multicluster-dashboard&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;match&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;v1&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;ConfigMap&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;monitoring-kube-prometheus-k8s-resources-multicluster&lt;/span&gt;&lt;span class="pi"&gt;}&lt;/span&gt;
      &lt;span class="na"&gt;delete&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;a href="https://kadet-hub.org/glossary/#target" rel="noopener noreferrer"&gt;target&lt;/a&gt; adds mutate as a second compile entry on&lt;br&gt;
the chart's &lt;code&gt;output_path&lt;/code&gt;, as the guide shows. Each dashboard template is&lt;br&gt;
a file of its own, so both files become a single comment line:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;kube-prometheus-stack/templates/grafana/dashboards-1.14/grafana-overview.yaml: # all objects removed by mutation no-grafana-overview-dashboard
kube-prometheus-stack/templates/grafana/dashboards-1.14/k8s-resources-multicluster.yaml: # all objects removed by mutation no-multicluster-dashboard
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The mutated render lost exactly the two ConfigMaps compared with the plain&lt;br&gt;
one, and kept the same 27 dashboards as the Kustomize output. With the&lt;br&gt;
class changed to release &lt;code&gt;kube-prometheus-stack&lt;/code&gt;, the compile stops:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;mutate: mutation no-grafana-overview-dashboard: expects 1+, matched 0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  On kind
&lt;/h3&gt;

&lt;p&gt;The mutated output went to a kind cluster on Kubernetes 1.37.0, the CRDs&lt;br&gt;
first and everything with a server-side apply. Grafana, the operator,&lt;br&gt;
kube-state-metrics, Prometheus, Alertmanager and node-exporter all became&lt;br&gt;
ready, and the Kustomize output passed a server-side dry run against the&lt;br&gt;
same cluster. The cluster held 27 dashboard ConfigMaps. Grafana's search&lt;br&gt;
API listed 27 dashboards, with the same titles as those ConfigMaps:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Grafana lists 27 dashboards, the mutated render has 27
neither removed dashboard is listed: ["Grafana Overview","Kubernetes / Compute Resources /  Multi-Cluster"]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The ServiceMonitor &lt;code&gt;monitoring-kube-prometheus-kube-controller-manager&lt;/code&gt;&lt;br&gt;
stayed in place, so the controller manager is still scraped, without its&lt;br&gt;
dashboard.&lt;/p&gt;
&lt;h3&gt;
  
  
  With the Grafana Operator
&lt;/h3&gt;

&lt;p&gt;With &lt;code&gt;grafana.operator.dashboardsConfigMapRefEnabled: true&lt;/code&gt; and&lt;br&gt;
&lt;code&gt;grafana.operator.matchLabels&lt;/code&gt; set, the chart also renders a&lt;br&gt;
&lt;code&gt;GrafanaDashboard&lt;/code&gt; per dashboard, with the same name as its ConfigMap and&lt;br&gt;
a &lt;code&gt;configMapRef&lt;/code&gt; to it. The patches above delete only the ConfigMaps and&lt;br&gt;
would leave two GrafanaDashboards that reference ConfigMaps that no longer&lt;br&gt;
exist. With the operator, the Kustomize patch deletes both kinds:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;resources&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;helm.yaml&lt;/span&gt;
&lt;span class="na"&gt;patches&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;patch&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|-&lt;/span&gt;
      &lt;span class="s"&gt;$patch: delete&lt;/span&gt;
      &lt;span class="s"&gt;apiVersion: v1&lt;/span&gt;
      &lt;span class="s"&gt;kind: ConfigMap&lt;/span&gt;
      &lt;span class="s"&gt;metadata:&lt;/span&gt;
        &lt;span class="s"&gt;name: monitoring-kube-prometheus-grafana-overview&lt;/span&gt;
        &lt;span class="s"&gt;namespace: monitoring&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;patch&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|-&lt;/span&gt;
      &lt;span class="s"&gt;$patch: delete&lt;/span&gt;
      &lt;span class="s"&gt;apiVersion: v1&lt;/span&gt;
      &lt;span class="s"&gt;kind: ConfigMap&lt;/span&gt;
      &lt;span class="s"&gt;metadata:&lt;/span&gt;
        &lt;span class="s"&gt;name: monitoring-kube-prometheus-k8s-resources-multicluster&lt;/span&gt;
        &lt;span class="s"&gt;namespace: monitoring&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;patch&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|-&lt;/span&gt;
      &lt;span class="s"&gt;$patch: delete&lt;/span&gt;
      &lt;span class="s"&gt;apiVersion: grafana.integreatly.org/v1beta1&lt;/span&gt;
      &lt;span class="s"&gt;kind: GrafanaDashboard&lt;/span&gt;
      &lt;span class="s"&gt;metadata:&lt;/span&gt;
        &lt;span class="s"&gt;name: monitoring-kube-prometheus-grafana-overview&lt;/span&gt;
        &lt;span class="s"&gt;namespace: monitoring&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;patch&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|-&lt;/span&gt;
      &lt;span class="s"&gt;$patch: delete&lt;/span&gt;
      &lt;span class="s"&gt;apiVersion: grafana.integreatly.org/v1beta1&lt;/span&gt;
      &lt;span class="s"&gt;kind: GrafanaDashboard&lt;/span&gt;
      &lt;span class="s"&gt;metadata:&lt;/span&gt;
        &lt;span class="s"&gt;name: monitoring-kube-prometheus-k8s-resources-multicluster&lt;/span&gt;
        &lt;span class="s"&gt;namespace: monitoring&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;and each mutation matches every kind by name and expects two objects:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;parameters&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;mutations&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;no-grafana-overview-dashboard&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;match&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;*"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;monitoring-kube-prometheus-grafana-overview&lt;/span&gt;&lt;span class="pi"&gt;}&lt;/span&gt;
      &lt;span class="na"&gt;delete&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
      &lt;span class="na"&gt;expects&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;2&lt;/span&gt;
    &lt;span class="na"&gt;no-multicluster-dashboard&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;match&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;*"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;monitoring-kube-prometheus-k8s-resources-multicluster&lt;/span&gt;&lt;span class="pi"&gt;}&lt;/span&gt;
      &lt;span class="na"&gt;delete&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
      &lt;span class="na"&gt;expects&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;2&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Rendered with those two values, both removed exactly the two ConfigMaps&lt;br&gt;
and the two GrafanaDashboards, and no remaining GrafanaDashboard refers to&lt;br&gt;
a missing ConfigMap. The Grafana Operator itself was not installed, so&lt;br&gt;
this variant is tested on the rendered output only.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://kadet-hub.org/blog/kube-prometheus-stack-dashboards/" rel="noopener noreferrer"&gt;kadet-hub.org&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>kubernetes</category>
      <category>devops</category>
      <category>kapitan</category>
      <category>gitops</category>
    </item>
    <item>
      <title>Kustomize delete resource: Kyverno PDBs and ESO webhooks</title>
      <dc:creator>Moep90</dc:creator>
      <pubDate>Sun, 25 Oct 2026 19:55:55 +0000</pubDate>
      <link>https://dev.to/kadet-hub/kustomize-delete-resource-kyverno-pdbs-and-eso-webhooks-27cl</link>
      <guid>https://dev.to/kadet-hub/kustomize-delete-resource-kyverno-pdbs-and-eso-webhooks-27cl</guid>
      <description>&lt;p&gt;Some Helm charts render an object you need to own yourself and offer no&lt;br&gt;
value to leave it out. This post covers two such cases from open issues:&lt;br&gt;
the Kyverno chart creates a PodDisruptionBudget for every controller that&lt;br&gt;
runs more than one replica, and the External Secrets chart renders its two&lt;br&gt;
ValidatingWebhookConfigurations whenever it renders the webhook. In both,&lt;br&gt;
the object is deleted from the rendered manifests, first with a Kustomize&lt;br&gt;
patch over &lt;code&gt;helm template&lt;/code&gt;, then with &lt;a href="https://kadet-hub.org/glossary/#kapitan" rel="noopener noreferrer"&gt;Kapitan&lt;/a&gt;'s&lt;br&gt;
&lt;a href="https://kadet-hub.org/generators/mutate/" rel="noopener noreferrer"&gt;mutate&lt;/a&gt; &lt;a href="https://kadet-hub.org/glossary/#generator" rel="noopener noreferrer"&gt;generator&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Each patch was rendered from the chart version named in its section with&lt;br&gt;
Helm 4.3.1, Kustomize 5.8.3, and Kapitan 0.36.3 with mutate 0.1.0, and the&lt;br&gt;
result was installed on a kind cluster on Kubernetes 1.37.0. The guide&lt;br&gt;
&lt;a href="https://kadet-hub.org/guides/patch-helm-chart/" rel="noopener noreferrer"&gt;Patch a Helm chart&lt;/a&gt; compares the places such a&lt;br&gt;
patch can live.&lt;/p&gt;
&lt;h2&gt;
  
  
  Kyverno Helm chart: PodDisruptionBudgets with more than one replica {#kyverno-pdb}
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://github.com/kyverno/kyverno/issues/11469" rel="noopener noreferrer"&gt;kyverno/kyverno#11469&lt;/a&gt;,&lt;br&gt;
opened in October 2024, reports that the chart's PodDisruptionBudgets cannot&lt;br&gt;
be turned off. The reporter wants to supply PDBs of their own instead. The&lt;br&gt;
maintainer realshuting&lt;br&gt;
&lt;a href="https://github.com/kyverno/kyverno/issues/11469#issuecomment-2437149508" rel="noopener noreferrer"&gt;asked&lt;/a&gt;&lt;br&gt;
the authors of the change that introduced the behaviour whether there was a&lt;br&gt;
reason to keep PDBs on for more than one replica, and the question is&lt;br&gt;
unanswered. In chart 3.9.2, published to the chart repository and to&lt;br&gt;
ghcr.io on 21 October 2026, the PDB template of each of the four&lt;br&gt;
controllers has the same condition, here from&lt;br&gt;
&lt;a href="https://github.com/kyverno/kyverno/blob/kyverno-chart-3.9.2/charts/kyverno/templates/admission-controller/poddisruptionbudget.yaml#L1" rel="noopener noreferrer"&gt;&lt;code&gt;admission-controller/poddisruptionbudget.yaml&lt;/code&gt;&lt;/a&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="pi"&gt;{{&lt;/span&gt;&lt;span class="nv"&gt;- if or .Values.admissionController.podDisruptionBudget.enabled (gt (int .Values.admissionController.replicas) 1) -&lt;/span&gt;&lt;span class="pi"&gt;}}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;podDisruptionBudget.enabled: false&lt;/code&gt; is the default, and with more than one&lt;br&gt;
replica the &lt;code&gt;or&lt;/code&gt; ignores it. The chart's &lt;code&gt;values.yaml&lt;/code&gt; says so: "Will&lt;br&gt;
always be enabled if replicas &amp;gt; 1. This non-declarative behavior should&lt;br&gt;
ideally be avoided, but changing it now would be breaking." With these&lt;br&gt;
values the chart rendered four PDBs, and the same four with&lt;br&gt;
&lt;code&gt;podDisruptionBudget.enabled=false&lt;/code&gt; set for every controller:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;admissionController&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;replicas&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;3&lt;/span&gt;
&lt;span class="na"&gt;backgroundController&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;replicas&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;2&lt;/span&gt;
&lt;span class="na"&gt;cleanupController&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;replicas&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;2&lt;/span&gt;
&lt;span class="na"&gt;reportsController&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;replicas&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;2&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;All four carry the label &lt;code&gt;app.kubernetes.io/part-of: kyverno&lt;/code&gt;. Its value is&lt;br&gt;
the chart's fullname, so it follows the release name: release &lt;code&gt;policy&lt;/code&gt;&lt;br&gt;
gives &lt;code&gt;policy-kyverno&lt;/code&gt;.&lt;/p&gt;
&lt;h3&gt;
  
  
  Kustomize over helm template
&lt;/h3&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;helm template kyverno oci://ghcr.io/kyverno/charts/kyverno &lt;span class="nt"&gt;--version&lt;/span&gt; 3.9.2 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--namespace&lt;/span&gt; kyverno &lt;span class="nt"&gt;-f&lt;/span&gt; kyverno-values.yaml &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; helm.yaml
kustomize build &lt;span class="nb"&gt;.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;with this &lt;code&gt;kustomization.yaml&lt;/code&gt; next to &lt;code&gt;helm.yaml&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;resources&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;helm.yaml&lt;/span&gt;
&lt;span class="na"&gt;patches&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;target&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;PodDisruptionBudget&lt;/span&gt;
      &lt;span class="na"&gt;labelSelector&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;app.kubernetes.io/part-of=kyverno&lt;/span&gt;
    &lt;span class="na"&gt;patch&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|-&lt;/span&gt;
      &lt;span class="s"&gt;$patch: delete&lt;/span&gt;
      &lt;span class="s"&gt;apiVersion: policy/v1&lt;/span&gt;
      &lt;span class="s"&gt;kind: PodDisruptionBudget&lt;/span&gt;
      &lt;span class="s"&gt;metadata:&lt;/span&gt;
        &lt;span class="s"&gt;name: ignored&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With a &lt;code&gt;target&lt;/code&gt;, Kustomize applies the patch to every object the selector&lt;br&gt;
matches, here the four PDBs, and the &lt;code&gt;metadata.name&lt;/code&gt; in the patch is&lt;br&gt;
required but not used. The output was the render without the four PDBs and&lt;br&gt;
otherwise unchanged. A selector that matches nothing is not an error: over&lt;br&gt;
a render for release &lt;code&gt;policy&lt;/code&gt;, the build passed and kept all four PDBs.&lt;/p&gt;
&lt;h3&gt;
  
  
  Kapitan's mutate
&lt;/h3&gt;

&lt;p&gt;The &lt;a href="https://kadet-hub.org/glossary/#class" rel="noopener noreferrer"&gt;class&lt;/a&gt; pins the chart by version and by the&lt;br&gt;
&lt;a href="https://kadet-hub.org/glossary/#digest" rel="noopener noreferrer"&gt;digest&lt;/a&gt; of its OCI artifact, and reads the same values&lt;br&gt;
file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;parameters&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;kapitan&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;dependencies&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;helm&lt;/span&gt;
        &lt;span class="na"&gt;source&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;oci://ghcr.io/kyverno/charts/kyverno@sha256:83054f30148c3cfc001087c1f5772eda7c7224e9b74b7f0e70c39c9cafe009d1&lt;/span&gt;
        &lt;span class="na"&gt;chart_name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;kyverno&lt;/span&gt;
        &lt;span class="na"&gt;version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;3.9.2&lt;/span&gt;
        &lt;span class="na"&gt;output_path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;charts/kyverno&lt;/span&gt;
    &lt;span class="na"&gt;compile&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;input_type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;helm&lt;/span&gt;
        &lt;span class="na"&gt;input_paths&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;charts/kyverno&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
        &lt;span class="na"&gt;output_path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;kyverno&lt;/span&gt;
        &lt;span class="na"&gt;helm_values_files&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;kyverno-values.yaml&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
        &lt;span class="na"&gt;helm_params&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;kyverno&lt;/span&gt;
          &lt;span class="na"&gt;namespace&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;kyverno&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The mutation deletes the PDBs by label and states how many it expects:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;parameters&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;mutations&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;no-pdbs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;match&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;policy/v1&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;PodDisruptionBudget&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;labels&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;app.kubernetes.io/part-of&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;kyverno&lt;/span&gt;&lt;span class="pi"&gt;}}&lt;/span&gt;
      &lt;span class="na"&gt;delete&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
      &lt;span class="na"&gt;expects&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;4&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;a href="https://kadet-hub.org/glossary/#target" rel="noopener noreferrer"&gt;target&lt;/a&gt; adds mutate as a second compile entry on&lt;br&gt;
the chart's &lt;code&gt;output_path&lt;/code&gt;, as the guide shows. Each PDB file became a&lt;br&gt;
single comment line, and the rest of the render was unchanged. With the&lt;br&gt;
class changed to release &lt;code&gt;policy&lt;/code&gt;, the compile stops:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;mutate: mutation no-pdbs: expects 4, matched 0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Helm hooks in the rendered output
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;helm template&lt;/code&gt; also renders the chart's hooks: a post-upgrade Job&lt;br&gt;
&lt;code&gt;kyverno-migrate-resources&lt;/code&gt; with its ServiceAccount, ClusterRole and&lt;br&gt;
ClusterRoleBinding, and the pre-delete Jobs &lt;code&gt;kyverno-scale-to-zero&lt;/code&gt; and&lt;br&gt;
&lt;code&gt;kyverno-rm-webhooks&lt;/code&gt;. &lt;code&gt;helm install&lt;/code&gt; runs each of them only at its hook.&lt;br&gt;
&lt;code&gt;kubectl apply&lt;/code&gt; of the whole output runs them at once, and on kind the&lt;br&gt;
Job &lt;code&gt;kyverno-scale-to-zero&lt;/code&gt; left every controller at zero replicas:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;kyverno-admission-controller    0/0     0            0           5s
kyverno-background-controller   0/0     0            0           5s
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The test therefore applied every object except those with a&lt;br&gt;
&lt;code&gt;helm.sh/hook&lt;/code&gt; annotation, as &lt;code&gt;helm install&lt;/code&gt; would. A tool that maps Helm&lt;br&gt;
hooks to its own, as Argo CD does, needs no such filter.&lt;/p&gt;
&lt;h3&gt;
  
  
  On kind
&lt;/h3&gt;

&lt;p&gt;The four controllers, with three admission controller replicas and two of&lt;br&gt;
each other, all reported Available, and the namespace held no&lt;br&gt;
PodDisruptionBudget. The Kustomize output, without hooks, passed a&lt;br&gt;
server-side dry run. To check that Kyverno still enforces policies, the&lt;br&gt;
test added a ValidatingPolicy that requires the label &lt;code&gt;team&lt;/code&gt; on ConfigMaps&lt;br&gt;
in the namespace &lt;code&gt;demo&lt;/code&gt;. A ConfigMap without it was refused:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;error: failed to create configmap: admission webhook "vpol.validate.kyverno.svc-fail" denied the request: Policy require-team-label failed: label team is required
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  External Secrets Helm chart: the ValidatingWebhookConfigurations {#external-secrets-webhooks}
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://github.com/external-secrets/external-secrets/issues/6961" rel="noopener noreferrer"&gt;external-secrets/external-secrets#6961&lt;/a&gt;,&lt;br&gt;
opened in September 2026, asks for a value that leaves the two&lt;br&gt;
ValidatingWebhookConfigurations out of the chart while it still deploys the&lt;br&gt;
webhook. The reporter applies cluster-scoped objects from a separate chart&lt;br&gt;
and wants the configurations to come from there, with the&lt;br&gt;
external-secrets chart used as published. The maintainers asked for the&lt;br&gt;
use case. alekc&lt;br&gt;
&lt;a href="https://github.com/external-secrets/external-secrets/issues/6961#issuecomment-5868041712" rel="noopener noreferrer"&gt;called it&lt;/a&gt;&lt;br&gt;
"a very niche usecase which would be best suited by a custom helm chart for&lt;br&gt;
your org" and wrote that such a toggle "would mean allowing users to deploy&lt;br&gt;
a broken (unless they take separate steps outside of the helm chart)&lt;br&gt;
deployment of eso". evrardj-roche closed the pull request with the toggle&lt;br&gt;
"temporarily until we have clarified the multi-tenant story" and pointed&lt;br&gt;
to the multi-namespace design in #7023.&lt;/p&gt;

&lt;p&gt;In chart 2.13.0, the release of 16 October 2026,&lt;br&gt;
&lt;a href="https://github.com/external-secrets/external-secrets/blob/helm-chart-2.13.0/deploy/charts/external-secrets/templates/validatingwebhook.yaml#L1-L5" rel="noopener noreferrer"&gt;&lt;code&gt;validatingwebhook.yaml&lt;/code&gt;&lt;/a&gt;&lt;br&gt;
renders &lt;code&gt;secretstore-validate&lt;/code&gt; and &lt;code&gt;externalsecret-validate&lt;/code&gt; under the same&lt;br&gt;
condition as the webhook itself:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="pi"&gt;{{&lt;/span&gt;&lt;span class="nv"&gt;- if .Values.webhook.create&lt;/span&gt; &lt;span class="pi"&gt;}}&lt;/span&gt;
&lt;span class="na"&gt;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;admissionregistration.k8s.io/v1&lt;/span&gt;
&lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ValidatingWebhookConfiguration&lt;/span&gt;
&lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;secretstore-validate&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;webhook.create=false&lt;/code&gt; removes the configurations and also the webhook's&lt;br&gt;
Deployment, Service, Secret and ServiceAccount. The configurations have&lt;br&gt;
&lt;code&gt;failurePolicy: Fail&lt;/code&gt; and no &lt;code&gt;caBundle&lt;/code&gt;. cert-controller adds it, and its&lt;br&gt;
&lt;a href="https://github.com/external-secrets/external-secrets/blob/helm-chart-2.13.0/deploy/charts/external-secrets/templates/cert-controller-rbac.yaml#L38-L47" rel="noopener noreferrer"&gt;ClusterRole&lt;/a&gt;&lt;br&gt;
allows it to update only these two names:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;apiGroups&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;admissionregistration.k8s.io"&lt;/span&gt;
    &lt;span class="na"&gt;resources&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;validatingwebhookconfigurations"&lt;/span&gt;
    &lt;span class="na"&gt;resourceNames&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;secretstore-validate"&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;externalsecret-validate"&lt;/span&gt;
    &lt;span class="na"&gt;verbs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;update"&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;patch"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Kustomize over helm template
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;helm template external-secrets oci://ghcr.io/external-secrets/charts/external-secrets &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--version&lt;/span&gt; 2.13.0 &lt;span class="nt"&gt;--namespace&lt;/span&gt; external-secrets &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; helm.yaml
kustomize build &lt;span class="nb"&gt;.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;with this &lt;code&gt;kustomization.yaml&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;resources&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;helm.yaml&lt;/span&gt;
&lt;span class="na"&gt;patches&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;target&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ValidatingWebhookConfiguration&lt;/span&gt;
      &lt;span class="na"&gt;labelSelector&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;external-secrets.io/component=webhook&lt;/span&gt;
    &lt;span class="na"&gt;patch&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|-&lt;/span&gt;
      &lt;span class="s"&gt;$patch: delete&lt;/span&gt;
      &lt;span class="s"&gt;apiVersion: admissionregistration.k8s.io/v1&lt;/span&gt;
      &lt;span class="s"&gt;kind: ValidatingWebhookConfiguration&lt;/span&gt;
      &lt;span class="s"&gt;metadata:&lt;/span&gt;
        &lt;span class="s"&gt;name: ignored&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The label &lt;code&gt;external-secrets.io/component: webhook&lt;/code&gt; is also on the webhook's&lt;br&gt;
Service and Secret, so the target names the kind as well. The output&lt;br&gt;
was the render without the two configurations and otherwise unchanged.&lt;/p&gt;
&lt;h3&gt;
  
  
  Kapitan's mutate
&lt;/h3&gt;

&lt;p&gt;The class pins chart 2.13.0 by OCI digest like the Kyverno one, and the&lt;br&gt;
mutation expects exactly the two configurations:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;parameters&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;mutations&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;no-validating-webhooks&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;match&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;admissionregistration.k8s.io/v1&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;ValidatingWebhookConfiguration&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;labels&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;external-secrets.io/component&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;webhook&lt;/span&gt;&lt;span class="pi"&gt;}}&lt;/span&gt;
      &lt;span class="na"&gt;delete&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
      &lt;span class="na"&gt;expects&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;2&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  On kind
&lt;/h3&gt;

&lt;p&gt;Without the configurations, the controller and the webhook Deployments&lt;br&gt;
became Available, and cert-controller did not become Ready. Its readiness&lt;br&gt;
probe kept failing with HTTP status 500:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;external-secrets-cert-controller-56d7b45fc5-btm8q ready=false restarts=0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With no webhook registered, a SecretStore whose fake&lt;br&gt;
provider has an empty key was accepted. The test then applied the two&lt;br&gt;
configurations as the plain render has them, names and labels included,&lt;br&gt;
standing in for the separate chart. cert-controller injected the CA and&lt;br&gt;
became Ready:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;secretstore-validate caBundle bytes: 1196
externalsecret-validate caBundle bytes: 1196
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A valid SecretStore passed the webhook, which added its warning about the&lt;br&gt;
fake provider, and the one with the empty key was now refused:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Error from server (Forbidden): error when creating "eso-store-invalid.yaml": admission webhook "validate.secretstore.external-secrets.io" denied the request: key must be set in data 0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The deletion is half of the change the issue asks for. The other chart has&lt;br&gt;
to apply both configurations with these names, or cert-controller cannot&lt;br&gt;
update them and stays unready.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://kadet-hub.org/blog/kustomize-delete-resource-helm-chart/" rel="noopener noreferrer"&gt;kadet-hub.org&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>kubernetes</category>
      <category>devops</category>
      <category>kapitan</category>
      <category>gitops</category>
    </item>
    <item>
      <title>Kustomize JSON patch add: fields a Helm chart does not template</title>
      <dc:creator>Moep90</dc:creator>
      <pubDate>Sun, 25 Oct 2026 19:55:45 +0000</pubDate>
      <link>https://dev.to/kadet-hub/kustomize-json-patch-add-fields-a-helm-chart-does-not-template-534h</link>
      <guid>https://dev.to/kadet-hub/kustomize-json-patch-add-fields-a-helm-chart-does-not-template-534h</guid>
      <description>&lt;p&gt;A chart can only set the fields its templates write. When a Kubernetes&lt;br&gt;
field you need is missing from them, the value you add to &lt;code&gt;values.yaml&lt;/code&gt;&lt;br&gt;
is silently ignored, unless the chart has a values schema that rejects it.&lt;br&gt;
This post sets nine such fields from open issues on the rendered manifests,&lt;br&gt;
first with a Kustomize patch over &lt;code&gt;helm template&lt;/code&gt;, then with&lt;br&gt;
&lt;a href="https://kadet-hub.org/glossary/#kapitan" rel="noopener noreferrer"&gt;Kapitan&lt;/a&gt;'s &lt;a href="https://kadet-hub.org/generators/mutate/" rel="noopener noreferrer"&gt;mutate&lt;/a&gt;&lt;br&gt;
&lt;a href="https://kadet-hub.org/glossary/#generator" rel="noopener noreferrer"&gt;generator&lt;/a&gt;. Most of them are RFC 6902 JSON Patch&lt;br&gt;
operations, and mutate's &lt;code&gt;json6902&lt;/code&gt; takes the same operations as&lt;br&gt;
Kustomize, so the two versions of each patch read almost the same.&lt;/p&gt;

&lt;p&gt;Each patch was rendered from the chart version named in its section with&lt;br&gt;
Helm 4.3.1, Kustomize 5.8.3, and Kapitan 0.36.3 with mutate 0.1.0. In every&lt;br&gt;
case the Kustomize output and the mutate output set the same value and&lt;br&gt;
changed nothing else in the render. The mutate output was installed on&lt;br&gt;
kind clusters on Kubernetes 1.37.0 with three worker nodes, and each&lt;br&gt;
Kustomize output's patched object passed a server-side dry run there.&lt;br&gt;
Charts from HTTP&lt;br&gt;
repositories matched the sha256 their &lt;code&gt;index.yaml&lt;/code&gt; lists. The guide&lt;br&gt;
&lt;a href="https://kadet-hub.org/guides/patch-helm-chart/" rel="noopener noreferrer"&gt;Patch a Helm chart&lt;/a&gt; compares the places such a&lt;br&gt;
patch can live.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://kadet-hub.org/glossary/#class" rel="noopener noreferrer"&gt;class&lt;/a&gt; for each chart pins its version, like this&lt;br&gt;
one for Vault, and the &lt;a href="https://kadet-hub.org/glossary/#target" rel="noopener noreferrer"&gt;target&lt;/a&gt; adds mutate as a second&lt;br&gt;
compile entry on the chart's &lt;code&gt;output_path&lt;/code&gt;, as the guide shows:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;parameters&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;kapitan&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;dependencies&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;helm&lt;/span&gt;
        &lt;span class="na"&gt;source&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;https://helm.releases.hashicorp.com&lt;/span&gt;
        &lt;span class="na"&gt;chart_name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;vault&lt;/span&gt;
        &lt;span class="na"&gt;version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;0.34.1&lt;/span&gt;
        &lt;span class="na"&gt;output_path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;charts/vault&lt;/span&gt;
    &lt;span class="na"&gt;compile&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;input_type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;helm&lt;/span&gt;
        &lt;span class="na"&gt;input_paths&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;charts/vault&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
        &lt;span class="na"&gt;output_path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;vault&lt;/span&gt;
        &lt;span class="na"&gt;helm_values_files&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;values/vault.yaml&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
        &lt;span class="na"&gt;helm_params&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;vault&lt;/span&gt;
          &lt;span class="na"&gt;namespace&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;vault&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Vault Helm chart: Service appProtocol {#vault-service-appprotocol}
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://github.com/hashicorp/vault-helm/issues/1108" rel="noopener noreferrer"&gt;hashicorp/vault-helm#1108&lt;/a&gt;,&lt;br&gt;
opened in April 2025 and at 9 thumbs up, asks for &lt;code&gt;appProtocol: https&lt;/code&gt; on&lt;br&gt;
the Vault Service. Without it, Gateway API implementations such as GKE's&lt;br&gt;
Gateway controller talk plain HTTP to a Vault that serves HTTPS, according&lt;br&gt;
to the issue and the GKE documentation it links. No maintainer has&lt;br&gt;
replied. Chart 0.34.1, released on 13 August 2026, writes the ports in&lt;br&gt;
&lt;a href="https://github.com/hashicorp/vault-helm/blob/v0.34.1/templates/server-service.yaml#L45-L54" rel="noopener noreferrer"&gt;&lt;code&gt;server-service.yaml&lt;/code&gt;&lt;/a&gt;&lt;br&gt;
without it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;  &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{{&lt;/span&gt; &lt;span class="nv"&gt;include "vault.scheme" .&lt;/span&gt; &lt;span class="pi"&gt;}}&lt;/span&gt;
      &lt;span class="na"&gt;port&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{{&lt;/span&gt; &lt;span class="nv"&gt;.Values.server.service.port&lt;/span&gt; &lt;span class="pi"&gt;}}&lt;/span&gt;
      &lt;span class="na"&gt;targetPort&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{{&lt;/span&gt; &lt;span class="nv"&gt;.Values.server.service.targetPort&lt;/span&gt; &lt;span class="pi"&gt;}}&lt;/span&gt;
      &lt;span class="pi"&gt;{{&lt;/span&gt;&lt;span class="nv"&gt;- if and (.Values.server.service.nodePort) (eq (.Values.server.service.type | toString) "NodePort")&lt;/span&gt; &lt;span class="pi"&gt;}}&lt;/span&gt;
      &lt;span class="na"&gt;nodePort&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{{&lt;/span&gt; &lt;span class="nv"&gt;.Values.server.service.nodePort&lt;/span&gt; &lt;span class="pi"&gt;}}&lt;/span&gt;
      &lt;span class="pi"&gt;{{&lt;/span&gt;&lt;span class="nv"&gt;- end&lt;/span&gt; &lt;span class="pi"&gt;}}&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;https-internal&lt;/span&gt;
      &lt;span class="na"&gt;port&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;8201&lt;/span&gt;
      &lt;span class="na"&gt;targetPort&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;8201&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The port takes its name from the scheme, &lt;code&gt;https&lt;/code&gt; once &lt;code&gt;global.tlsDisable&lt;/code&gt;&lt;br&gt;
is false, which this values file sets:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;global&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;tlsDisable&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;resources&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;helm.yaml&lt;/span&gt;
&lt;span class="na"&gt;patches&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;target&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Service&lt;/span&gt;
      &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;vault&lt;/span&gt;
    &lt;span class="na"&gt;patch&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|-&lt;/span&gt;
      &lt;span class="s"&gt;- op: test&lt;/span&gt;
        &lt;span class="s"&gt;path: /spec/ports/0/name&lt;/span&gt;
        &lt;span class="s"&gt;value: https&lt;/span&gt;
      &lt;span class="s"&gt;- op: add&lt;/span&gt;
        &lt;span class="s"&gt;path: /spec/ports/0/appProtocol&lt;/span&gt;
        &lt;span class="s"&gt;value: https&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;parameters&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;mutations&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;vault-app-protocol&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;match&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;v1&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;Service&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;vault&lt;/span&gt;&lt;span class="pi"&gt;}&lt;/span&gt;
      &lt;span class="na"&gt;json6902&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;op&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;test&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;/spec/ports/0/name&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;value&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;https&lt;/span&gt;&lt;span class="pi"&gt;}&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;op&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;add&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;/spec/ports/0/appProtocol&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;value&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;https&lt;/span&gt;&lt;span class="pi"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;test&lt;/code&gt; makes both patches refuse to mark a plain HTTP port as &lt;code&gt;https&lt;/code&gt;. With&lt;br&gt;
the chart's default &lt;code&gt;tlsDisable: true&lt;/code&gt; the first port is named &lt;code&gt;http&lt;/code&gt;, and&lt;br&gt;
both stopped:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Error: testing value /spec/ports/0/name failed: test failed
mutate: mutation vault-app-protocol: vault/templates/server-service.yaml: json6902 test of '/spec/ports/0/name' failed
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The Kustomize target &lt;code&gt;name: vault&lt;/code&gt; matched the Service &lt;code&gt;vault&lt;/code&gt; only, not&lt;br&gt;
&lt;code&gt;vault-internal&lt;/code&gt; or &lt;code&gt;vault-agent-injector-svc&lt;/code&gt;. The test applied the&lt;br&gt;
chart's Services to kind, without the Vault server, and the stored Service&lt;br&gt;
carries the field:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;https 8200 appProtocol=https
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  CloudNativePG cluster chart: topologySpreadConstraints {#cloudnative-pg-topology-spread-constraints}
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://github.com/cloudnative-pg/charts/issues/817" rel="noopener noreferrer"&gt;cloudnative-pg/charts#817&lt;/a&gt;,&lt;br&gt;
opened in February 2026, asks for &lt;code&gt;topologySpreadConstraints&lt;/code&gt; on the&lt;br&gt;
Cluster that the &lt;code&gt;cluster&lt;/code&gt; chart renders. The CloudNativePG operator&lt;br&gt;
supports the field, but the chart's&lt;br&gt;
&lt;a href="https://github.com/cloudnative-pg/charts/blob/cluster-v0.9.0/charts/cluster/templates/cluster.yaml#L42-L45" rel="noopener noreferrer"&gt;&lt;code&gt;cluster.yaml&lt;/code&gt;&lt;/a&gt;&lt;br&gt;
only templates &lt;code&gt;affinity&lt;/code&gt;. Pull request #818, opened the same day, adds&lt;br&gt;
it. It has had no review and now conflicts with the chart's main branch.&lt;br&gt;
In its thread, a user pointed out that the label &lt;code&gt;cnpg.io/cluster&lt;/code&gt; alone&lt;br&gt;
also matches pooler and Job pods, and suggested adding&lt;br&gt;
&lt;code&gt;cnpg.io/podRole: instance&lt;/code&gt; to the selector.&lt;/p&gt;

&lt;p&gt;On node-local storage that selector has a catch. CloudNativePG creates&lt;br&gt;
each instance's volume through an initdb or join Job, whose pod gets the&lt;br&gt;
instance's constraints but not the label &lt;code&gt;cnpg.io/podRole&lt;/code&gt;, so the&lt;br&gt;
constraint does not count it. kind's &lt;code&gt;local-path&lt;/code&gt; volumes are bound to the&lt;br&gt;
node of the first pod that mounts them. In a run with that selector, the&lt;br&gt;
join Job of the third instance landed on the node of the second, the&lt;br&gt;
volume followed it, and the third instance stayed Pending:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;0/4 nodes are available: 1 node(s) didn't match pod topology spread constraints, 1 node(s) had untolerated taint(s), 2 node(s) didn't match PersistentVolume's node affinity. preemption: 0/4 nodes are available: 1 No preemption victims found for incoming pod, 3 Preemption is not helpful for scheduling.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A selector that counts the Job pods too and leaves out only poolers spread&lt;br&gt;
all three instances. Until a chart release has the value, chart 0.9.0&lt;br&gt;
takes this patch:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;resources&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;helm.yaml&lt;/span&gt;
&lt;span class="na"&gt;patches&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;target&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;group&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;postgresql.cnpg.io&lt;/span&gt;
      &lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Cluster&lt;/span&gt;
      &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;pg-cluster&lt;/span&gt;
    &lt;span class="na"&gt;patch&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|-&lt;/span&gt;
      &lt;span class="s"&gt;- op: add&lt;/span&gt;
        &lt;span class="s"&gt;path: /spec/topologySpreadConstraints&lt;/span&gt;
        &lt;span class="s"&gt;value:&lt;/span&gt;
          &lt;span class="s"&gt;- maxSkew: 1&lt;/span&gt;
            &lt;span class="s"&gt;topologyKey: kubernetes.io/hostname&lt;/span&gt;
            &lt;span class="s"&gt;whenUnsatisfiable: DoNotSchedule&lt;/span&gt;
            &lt;span class="s"&gt;labelSelector:&lt;/span&gt;
              &lt;span class="s"&gt;matchLabels:&lt;/span&gt;
                &lt;span class="s"&gt;cnpg.io/cluster: pg-cluster&lt;/span&gt;
              &lt;span class="s"&gt;matchExpressions:&lt;/span&gt;
                &lt;span class="s"&gt;- key: cnpg.io/podRole&lt;/span&gt;
                  &lt;span class="s"&gt;operator: NotIn&lt;/span&gt;
                  &lt;span class="s"&gt;values: [pooler]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;parameters&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;mutations&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;cluster-topology-spread&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;match&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;postgresql.cnpg.io/v1&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;Cluster&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;pg-cluster&lt;/span&gt;&lt;span class="pi"&gt;}&lt;/span&gt;
      &lt;span class="na"&gt;json6902&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;op&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;add&lt;/span&gt;
          &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;/spec/topologySpreadConstraints&lt;/span&gt;
          &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;maxSkew&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;
              &lt;span class="na"&gt;topologyKey&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;kubernetes.io/hostname&lt;/span&gt;
              &lt;span class="na"&gt;whenUnsatisfiable&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;DoNotSchedule&lt;/span&gt;
              &lt;span class="na"&gt;labelSelector&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
                &lt;span class="na"&gt;matchLabels&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
                  &lt;span class="na"&gt;cnpg.io/cluster&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;pg-cluster&lt;/span&gt;
                &lt;span class="na"&gt;matchExpressions&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
                  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;cnpg.io/podRole&lt;/span&gt;
                    &lt;span class="na"&gt;operator&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;NotIn&lt;/span&gt;
                    &lt;span class="na"&gt;values&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;pooler&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With operator chart 0.29.1 on the cluster and the release named &lt;code&gt;pg&lt;/code&gt;, the&lt;br&gt;
Cluster &lt;code&gt;pg-cluster&lt;/code&gt; became Ready on a fresh cluster. Its three instance&lt;br&gt;
pods carry the constraint and run on three different workers:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;pg-cluster-1 node=set-fields-worker2 ready=true spread=kubernetes.io/hostname
pg-cluster-2 node=set-fields-worker ready=true spread=kubernetes.io/hostname
pg-cluster-3 node=set-fields-worker3 ready=true spread=kubernetes.io/hostname
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Harbor Helm chart: annotations on the core Deployment {#harbor-core-deployment-annotations}
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://github.com/goharbor/harbor-helm/issues/2221" rel="noopener noreferrer"&gt;goharbor/harbor-helm#2221&lt;/a&gt;,&lt;br&gt;
opened in August 2025, asks for annotations and labels on the components'&lt;br&gt;
Deployments, beyond the pod annotations the chart takes. One commenter&lt;br&gt;
needs an annotation on &lt;code&gt;harbor-core&lt;/code&gt; for Reloader. A collaborator&lt;br&gt;
suggested a Helm hook, which the reporter cannot use with Argo CD. Three&lt;br&gt;
pull requests for the values were closed unmerged by the stale bot. In chart&lt;br&gt;
1.19.4, released on 16 October 2026,&lt;br&gt;
&lt;a href="https://github.com/goharbor/harbor-helm/blob/v1.19.4/templates/core/core-dpl.yaml#L3-L9" rel="noopener noreferrer"&gt;&lt;code&gt;core-dpl.yaml&lt;/code&gt;&lt;/a&gt;&lt;br&gt;
writes no annotations into the Deployment's metadata, and&lt;br&gt;
&lt;code&gt;core.podAnnotations&lt;/code&gt;, whose comment in &lt;code&gt;values.yaml&lt;/code&gt; reads "Additional&lt;br&gt;
deployment annotations", goes to the pod template only:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{{&lt;/span&gt; &lt;span class="nv"&gt;template "harbor.core" .&lt;/span&gt; &lt;span class="pi"&gt;}}&lt;/span&gt;
  &lt;span class="na"&gt;namespace&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{{&lt;/span&gt; &lt;span class="nv"&gt;.Release.Namespace | quote&lt;/span&gt; &lt;span class="pi"&gt;}}&lt;/span&gt;
  &lt;span class="na"&gt;labels&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
&lt;span class="pi"&gt;{{&lt;/span&gt; &lt;span class="nv"&gt;include "harbor.labels" . | indent 4&lt;/span&gt; &lt;span class="pi"&gt;}}&lt;/span&gt;
    &lt;span class="na"&gt;component&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;core&lt;/span&gt;
    &lt;span class="na"&gt;app.kubernetes.io/component&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;core&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;An annotation is a key in a map, which a strategic merge patch and&lt;br&gt;
mutate's &lt;code&gt;merge&lt;/code&gt; add without touching other keys:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;resources&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;helm.yaml&lt;/span&gt;
&lt;span class="na"&gt;patches&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;patch&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|-&lt;/span&gt;
      &lt;span class="s"&gt;apiVersion: apps/v1&lt;/span&gt;
      &lt;span class="s"&gt;kind: Deployment&lt;/span&gt;
      &lt;span class="s"&gt;metadata:&lt;/span&gt;
        &lt;span class="s"&gt;name: harbor-core&lt;/span&gt;
        &lt;span class="s"&gt;namespace: harbor&lt;/span&gt;
        &lt;span class="s"&gt;annotations:&lt;/span&gt;
          &lt;span class="s"&gt;reloader.stakater.com/auto: "true"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;parameters&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;mutations&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;harbor-core-annotations&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;match&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;apps/v1&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;Deployment&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;harbor-core&lt;/span&gt;&lt;span class="pi"&gt;}&lt;/span&gt;
      &lt;span class="na"&gt;merge&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;annotations&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="na"&gt;reloader.stakater.com/auto&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;true"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Harbor generates secrets on every render, so two renders differ in their&lt;br&gt;
Secrets and in the Deployments' checksum annotations. The comparison of&lt;br&gt;
the renders left those out. On kind, every Harbor Deployment became&lt;br&gt;
Available, and the stored &lt;code&gt;harbor-core&lt;/code&gt; carries the annotation:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;harbor-core annotations: {"deployment.kubernetes.io/revision":"1","reloader.stakater.com/auto":"true"}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  ECK operator chart: ServiceMonitor labels and interval {#eck-operator-servicemonitor}
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://github.com/elastic/cloud-on-k8s/issues/7717" rel="noopener noreferrer"&gt;elastic/cloud-on-k8s#7717&lt;/a&gt;,&lt;br&gt;
opened in April 2024, asks for labels and the interval on the operator's&lt;br&gt;
ServiceMonitor, because a Prometheus from kube-prometheus-stack selects&lt;br&gt;
ServiceMonitors by its &lt;code&gt;release&lt;/code&gt; label. A commenter confirmed in August&lt;br&gt;
2026 that 3.5.0 ignores &lt;code&gt;serviceMonitor.labels&lt;/code&gt; and keeps a second&lt;br&gt;
ServiceMonitor by hand. The chart has no values schema, so the key is&lt;br&gt;
silently dropped. In 3.5.0, released on 4 August 2026,&lt;br&gt;
&lt;a href="https://github.com/elastic/cloud-on-k8s/blob/v3.5.0/deploy/eck-operator/templates/service-monitor.yaml#L1-L20" rel="noopener noreferrer"&gt;&lt;code&gt;service-monitor.yaml&lt;/code&gt;&lt;/a&gt;&lt;br&gt;
renders the ServiceMonitor only with metrics in secure mode, with the&lt;br&gt;
chart's own labels and a fixed interval:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="pi"&gt;{{&lt;/span&gt;&lt;span class="nv"&gt;- if and .Values.config.metrics.secureMode.enabled .Values.serviceMonitor.enabled&lt;/span&gt; &lt;span class="pi"&gt;}}&lt;/span&gt;
&lt;span class="na"&gt;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;monitoring.coreos.com/v1&lt;/span&gt;
&lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ServiceMonitor&lt;/span&gt;
&lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{{&lt;/span&gt; &lt;span class="nv"&gt;include "eck-operator.fullname" .&lt;/span&gt; &lt;span class="pi"&gt;}}&lt;/span&gt;
  &lt;span class="na"&gt;namespace&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{{&lt;/span&gt; &lt;span class="nv"&gt;ternary .Values.serviceMonitor.namespace .Release.Namespace (not (and (.Values.serviceMonitor) (empty .Values.serviceMonitor.namespace)))&lt;/span&gt; &lt;span class="pi"&gt;}}&lt;/span&gt;
  &lt;span class="na"&gt;labels&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{{&lt;/span&gt;&lt;span class="nv"&gt;- include "eck-operator.labels" . | nindent 4&lt;/span&gt; &lt;span class="pi"&gt;}}&lt;/span&gt;
&lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;namespaceSelector&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;matchNames&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="pi"&gt;{{&lt;/span&gt; &lt;span class="nv"&gt;.Release.Namespace&lt;/span&gt; &lt;span class="pi"&gt;}}&lt;/span&gt;
  &lt;span class="na"&gt;selector&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;matchLabels&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;app.kubernetes.io/name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{{&lt;/span&gt; &lt;span class="nv"&gt;include "eck-operator.name" .&lt;/span&gt; &lt;span class="pi"&gt;}}&lt;/span&gt;&lt;span class="s"&gt;-metrics-service&lt;/span&gt;
      &lt;span class="na"&gt;app.kubernetes.io/instance&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{{&lt;/span&gt; &lt;span class="nv"&gt;.Release.Name&lt;/span&gt; &lt;span class="pi"&gt;}}&lt;/span&gt;
  &lt;span class="na"&gt;endpoints&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;port&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;https&lt;/span&gt;
    &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;/metrics&lt;/span&gt;
    &lt;span class="na"&gt;scheme&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;https&lt;/span&gt;
    &lt;span class="na"&gt;interval&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;30s&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;These values turn the metrics endpoint and secure mode on:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;config&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;metrics&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;port&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;8080"&lt;/span&gt;
    &lt;span class="na"&gt;secureMode&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;enabled&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;resources&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;helm.yaml&lt;/span&gt;
&lt;span class="na"&gt;patches&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;target&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;group&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;monitoring.coreos.com&lt;/span&gt;
      &lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ServiceMonitor&lt;/span&gt;
      &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;elastic-operator&lt;/span&gt;
    &lt;span class="na"&gt;patch&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|-&lt;/span&gt;
      &lt;span class="s"&gt;- op: add&lt;/span&gt;
        &lt;span class="s"&gt;path: /metadata/labels/release&lt;/span&gt;
        &lt;span class="s"&gt;value: kube-prometheus-stack&lt;/span&gt;
      &lt;span class="s"&gt;- op: test&lt;/span&gt;
        &lt;span class="s"&gt;path: /spec/endpoints/0/interval&lt;/span&gt;
        &lt;span class="s"&gt;value: 30s&lt;/span&gt;
      &lt;span class="s"&gt;- op: replace&lt;/span&gt;
        &lt;span class="s"&gt;path: /spec/endpoints/0/interval&lt;/span&gt;
        &lt;span class="s"&gt;value: 60s&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;parameters&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;mutations&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;eck-servicemonitor&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;match&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;monitoring.coreos.com/v1&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;ServiceMonitor&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;elastic-operator&lt;/span&gt;&lt;span class="pi"&gt;}&lt;/span&gt;
      &lt;span class="na"&gt;json6902&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;op&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;add&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;/metadata/labels/release&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;value&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;kube-prometheus-stack&lt;/span&gt;&lt;span class="pi"&gt;}&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;op&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;test&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;/spec/endpoints/0/interval&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;value&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;30s&lt;/span&gt;&lt;span class="pi"&gt;}&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;op&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;replace&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;/spec/endpoints/0/interval&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;value&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;60s&lt;/span&gt;&lt;span class="pi"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;add&lt;/code&gt; on &lt;code&gt;/metadata/labels/release&lt;/code&gt; adds one key to the existing labels.&lt;br&gt;
The &lt;code&gt;test&lt;/code&gt; of &lt;code&gt;30s&lt;/code&gt; stops the patch if a release changes the interval. On&lt;br&gt;
kind, with the ServiceMonitor CRD of kube-prometheus-stack 92.11.0, the&lt;br&gt;
operator became ready and the stored ServiceMonitor has both values:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;release=kube-prometheus-stack interval=60s
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Temporal Helm chart: Service trafficDistribution {#temporal-service-trafficdistribution}
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://github.com/temporalio/helm-charts/issues/897" rel="noopener noreferrer"&gt;temporalio/helm-charts#897&lt;/a&gt;,&lt;br&gt;
opened in April 2026, asks for &lt;code&gt;spec.trafficDistribution&lt;/code&gt; on the server&lt;br&gt;
Services, at least the frontend. Its author sets the field today with a&lt;br&gt;
separate server-side apply after each install. No maintainer has replied.&lt;br&gt;
Chart 1.7.0, released on 12 September 2026, writes &lt;code&gt;type&lt;/code&gt;, &lt;code&gt;ports&lt;/code&gt; and&lt;br&gt;
&lt;code&gt;selector&lt;/code&gt; in&lt;br&gt;
&lt;a href="https://github.com/temporalio/helm-charts/blob/temporal-1.7.0/charts/temporal/templates/server-service.yaml#L15-L40" rel="noopener noreferrer"&gt;&lt;code&gt;server-service.yaml&lt;/code&gt;&lt;/a&gt;&lt;br&gt;
and has no values schema, so &lt;code&gt;server.frontend.service.trafficDistribution&lt;/code&gt;&lt;br&gt;
is ignored. The chart needs a database to render, and this values file&lt;br&gt;
names one that the test never contacted:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;server&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;config&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;persistence&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;datastores&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;default&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;sql&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="na"&gt;pluginName&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;postgres12&lt;/span&gt;
            &lt;span class="na"&gt;databaseName&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;temporal&lt;/span&gt;
            &lt;span class="na"&gt;connectAddr&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;db.example.internal:5432&lt;/span&gt;
        &lt;span class="na"&gt;visibility&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;sql&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="na"&gt;pluginName&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;postgres12&lt;/span&gt;
            &lt;span class="na"&gt;databaseName&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;temporal_visibility&lt;/span&gt;
            &lt;span class="na"&gt;connectAddr&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;db.example.internal:5432&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;resources&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;helm.yaml&lt;/span&gt;
&lt;span class="na"&gt;patches&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;target&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Service&lt;/span&gt;
      &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;temporal-frontend&lt;/span&gt;
    &lt;span class="na"&gt;patch&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|-&lt;/span&gt;
      &lt;span class="s"&gt;- op: add&lt;/span&gt;
        &lt;span class="s"&gt;path: /spec/trafficDistribution&lt;/span&gt;
        &lt;span class="s"&gt;value: PreferSameZone&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;parameters&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;mutations&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;temporal-traffic-distribution&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;match&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;v1&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;Service&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;temporal-frontend&lt;/span&gt;&lt;span class="pi"&gt;}&lt;/span&gt;
      &lt;span class="na"&gt;json6902&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;op&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;add&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;/spec/trafficDistribution&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;value&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;PreferSameZone&lt;/span&gt;&lt;span class="pi"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;PreferClose&lt;/code&gt;, the value in the issue, is the older name for&lt;br&gt;
&lt;code&gt;PreferSameZone&lt;/code&gt;, and the Kubernetes documentation&lt;br&gt;
&lt;a href="https://kubernetes.io/docs/reference/networking/virtual-ips/#traffic-distribution" rel="noopener noreferrer"&gt;lists it as deprecated&lt;/a&gt;.&lt;br&gt;
The field is generally available since Kubernetes 1.33. The value&lt;br&gt;
&lt;code&gt;PreferSameZone&lt;/code&gt; is enabled by default from 1.34, so a 1.33 cluster needs&lt;br&gt;
&lt;code&gt;PreferClose&lt;/code&gt;. Without a database the Temporal servers cannot start, so&lt;br&gt;
the test applied the chart's Services only, and the API server stored the&lt;br&gt;
field:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;temporal-frontend trafficDistribution: PreferSameZone
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  MetalLB Helm chart: the speaker without host networking {#metallb-speaker-hostnetwork}
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://github.com/metallb/metallb/issues/3005" rel="noopener noreferrer"&gt;metallb/metallb#3005&lt;/a&gt;,&lt;br&gt;
opened in April 2026, asks for &lt;code&gt;hostNetwork: false&lt;/code&gt; on the speaker&lt;br&gt;
DaemonSet. The reporter runs FRR on every node next to MetalLB, and FRR&lt;br&gt;
does not accept iBGP peering with an address in its own network&lt;br&gt;
namespace, so the speaker has to run in the pod network and peer with the&lt;br&gt;
node's FRR. Another user peers the speakers with Calico the same way. The&lt;br&gt;
maintainers asked for the use case. Pull request #3099 adds a&lt;br&gt;
&lt;code&gt;speaker.hostNetwork&lt;/code&gt; value, and on it the maintainer fedepaol&lt;br&gt;
&lt;a href="https://github.com/metallb/metallb/pull/3099#issuecomment-5583454159" rel="noopener noreferrer"&gt;wrote&lt;/a&gt;&lt;br&gt;
"I think it makes sense to document this, mentioning that l2 won't be&lt;br&gt;
able to work." It is not merged. Chart 0.16.1 hard-codes the field in&lt;br&gt;
&lt;a href="https://github.com/metallb/metallb/blob/v0.16.1/charts/metallb/templates/speaker.yaml#L172" rel="noopener noreferrer"&gt;&lt;code&gt;speaker.yaml&lt;/code&gt;&lt;/a&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;      &lt;span class="na"&gt;hostNetwork&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;By default the chart runs BGP in a separate frr-k8s DaemonSet, which keeps&lt;br&gt;
host networking whatever the speaker does. The test therefore uses&lt;br&gt;
MetalLB's native mode, in which the speaker itself speaks BGP:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;frrk8s&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;enabled&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
&lt;span class="na"&gt;speaker&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;frr&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;enabled&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Flipping &lt;code&gt;hostNetwork&lt;/code&gt; alone is not enough in 0.16.1. The speaker serves&lt;br&gt;
its health endpoint on &lt;code&gt;127.0.0.1&lt;/code&gt; inside its own network namespace&lt;br&gt;
(&lt;a href="https://github.com/metallb/metallb/blob/v0.16.1/speaker/main.go#L247" rel="noopener noreferrer"&gt;&lt;code&gt;speaker/main.go&lt;/code&gt;&lt;/a&gt;),&lt;br&gt;
and the chart's probes ask for &lt;code&gt;127.0.0.1&lt;/code&gt;, which the kubelet resolves in&lt;br&gt;
the node's network namespace. With only the field changed, no speaker&lt;br&gt;
became ready on kind, and every probe was refused:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Readiness probe failed: Get "http://127.0.0.1:17472/readyz": dial tcp 127.0.0.1:17472: connect: connection refused
Liveness probe failed: Get "http://127.0.0.1:17472/healthz": dial tcp 127.0.0.1:17472: connect: connection refused
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Pull request #3099 changes the field only, so it would leave the same&lt;br&gt;
probes in place. The patch below also removes both probes. The speaker&lt;br&gt;
then has no health check, and the kubelet restarts it only when the&lt;br&gt;
process exits:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;resources&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;helm.yaml&lt;/span&gt;
&lt;span class="na"&gt;patches&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;target&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;DaemonSet&lt;/span&gt;
      &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;metallb-speaker&lt;/span&gt;
    &lt;span class="na"&gt;patch&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|-&lt;/span&gt;
      &lt;span class="s"&gt;- op: test&lt;/span&gt;
        &lt;span class="s"&gt;path: /spec/template/spec/hostNetwork&lt;/span&gt;
        &lt;span class="s"&gt;value: true&lt;/span&gt;
      &lt;span class="s"&gt;- op: replace&lt;/span&gt;
        &lt;span class="s"&gt;path: /spec/template/spec/hostNetwork&lt;/span&gt;
        &lt;span class="s"&gt;value: false&lt;/span&gt;
      &lt;span class="s"&gt;- op: test&lt;/span&gt;
        &lt;span class="s"&gt;path: /spec/template/spec/containers/0/name&lt;/span&gt;
        &lt;span class="s"&gt;value: speaker&lt;/span&gt;
      &lt;span class="s"&gt;- op: remove&lt;/span&gt;
        &lt;span class="s"&gt;path: /spec/template/spec/containers/0/livenessProbe&lt;/span&gt;
      &lt;span class="s"&gt;- op: remove&lt;/span&gt;
        &lt;span class="s"&gt;path: /spec/template/spec/containers/0/readinessProbe&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;parameters&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;mutations&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;metallb-speaker-pod-network&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;match&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;apps/v1&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;DaemonSet&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;metallb-speaker&lt;/span&gt;&lt;span class="pi"&gt;}&lt;/span&gt;
      &lt;span class="na"&gt;json6902&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;op&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;test&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;/spec/template/spec/hostNetwork&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;value&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;true&lt;/span&gt;&lt;span class="pi"&gt;}&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;op&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;replace&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;/spec/template/spec/hostNetwork&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;value&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;false&lt;/span&gt;&lt;span class="pi"&gt;}&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;op&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;test&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;/spec/template/spec/containers/0/name&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;value&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;speaker&lt;/span&gt;&lt;span class="pi"&gt;}&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;op&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;remove&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;/spec/template/spec/containers/0/livenessProbe&lt;/span&gt;&lt;span class="pi"&gt;}&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;op&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;remove&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;/spec/template/spec/containers/0/readinessProbe&lt;/span&gt;&lt;span class="pi"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On kind, a speaker ran on each of the four nodes with its own pod IP.&lt;br&gt;
Without a readiness probe, ready means that the container runs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;metallb-speaker-6nsjd hostNetwork=false pod IP is the node IP: no ready=true restarts=0
metallb-speaker-6rr28 hostNetwork=false pod IP is the node IP: no ready=true restarts=0
metallb-speaker-8z7rr hostNetwork=false pod IP is the node IP: no ready=true restarts=0
metallb-speaker-hc622 hostNetwork=false pod IP is the node IP: no ready=true restarts=0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;L2 mode answers ARP requests for the service address on the node's&lt;br&gt;
interface, which a speaker in the pod network does not have. With an L2&lt;br&gt;
pool from the kind network and an nginx LoadBalancer Service, the test&lt;br&gt;
compared both settings from the host:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;speaker in the pod network: the LoadBalancer address from the host: not reachable
speaker in the host network (plain chart): the LoadBalancer address from the host: reachable
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The test did not set up a BGP peer, so it shows that the speakers run in&lt;br&gt;
the pod network, not that they announce routes over BGP from there.&lt;/p&gt;
&lt;h2&gt;
  
  
  Consul Helm chart: tolerations on the hook Jobs {#consul-hook-job-tolerations}
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://github.com/hashicorp/consul-k8s/issues/4469" rel="noopener noreferrer"&gt;hashicorp/consul-k8s#4469&lt;/a&gt;,&lt;br&gt;
opened in January 2025 and at 7 thumbs up, asks for tolerations and a&lt;br&gt;
nodeSelector on the Job that generates the gossip encryption key. When&lt;br&gt;
every node carries a taint, that Job and tls-init stay Pending, and the&lt;br&gt;
thread names a third Job, tls-init-cleanup. The workaround offered there is&lt;br&gt;
to &lt;code&gt;helm pull&lt;/code&gt; the chart and edit its templates, which a commenter who&lt;br&gt;
installs from HashiCorp's repository cannot do. Pull request #4625, opened&lt;br&gt;
in June 2025, adds the values and has had no review. In chart 2.0.4,&lt;br&gt;
released on 11 September 2026, none of the three Jobs sets either field in&lt;br&gt;
its pod spec, and 2.1.0-rc2 does not either. All three are Helm hooks, as&lt;br&gt;
&lt;a href="https://github.com/hashicorp/consul-k8s/blob/v2.0.4/charts/consul/templates/tls-init-job.yaml#L20-L23" rel="noopener noreferrer"&gt;&lt;code&gt;tls-init-job.yaml&lt;/code&gt;&lt;/a&gt;&lt;br&gt;
shows for tls-init:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;  &lt;span class="na"&gt;annotations&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;helm.sh/hook"&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt; &lt;span class="s"&gt;pre-install,pre-upgrade&lt;/span&gt;
    &lt;span class="s"&gt;"helm.sh/hook-weight"&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;1"&lt;/span&gt;
    &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;helm.sh/hook-delete-policy"&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt;  &lt;span class="s"&gt;hook-succeeded,before-hook-creation&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The gossip key Job runs at the same hooks, tls-init-cleanup at&lt;br&gt;
&lt;code&gt;pre-delete&lt;/code&gt;. These values turn on TLS and the gossip key, and&lt;br&gt;
&lt;code&gt;global.name&lt;/code&gt; gives the Jobs the names from the issue:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;global&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;consul&lt;/span&gt;
  &lt;span class="na"&gt;tls&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;enabled&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
  &lt;span class="na"&gt;gossipEncryption&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;autoGenerate&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The test tainted and labelled all three workers with&lt;br&gt;
&lt;code&gt;example.com/purpose=infra&lt;/code&gt; and set the chart's own tolerations and&lt;br&gt;
nodeSelector values for the server, the connect injector and the webhook&lt;br&gt;
cert manager. &lt;code&gt;global.acls.tolerations&lt;/code&gt; reaches the&lt;br&gt;
&lt;code&gt;consul-gateway-resources&lt;/code&gt; Job, which a commenter also mentions, and the&lt;br&gt;
two gateway cleanup Jobs, even with ACLs off. Those three have no&lt;br&gt;
nodeSelector value. With the plain chart, both pre-install Jobs stayed&lt;br&gt;
Pending:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;consul-tls-init-fh7nh: 0/4 nodes are available: 4 node(s) had untolerated taint(s). preemption: 0/4 nodes are available: 4 Preemption is not helpful for scheduling.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The three Jobs carry &lt;code&gt;app: consul&lt;/code&gt; whatever the release is called, and&lt;br&gt;
&lt;code&gt;component&lt;/code&gt; with their own name, so one Kustomize target with a set-based&lt;br&gt;
selector matches all three:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;resources&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;helm.yaml&lt;/span&gt;
&lt;span class="na"&gt;patches&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;target&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;group&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;batch&lt;/span&gt;
      &lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Job&lt;/span&gt;
      &lt;span class="na"&gt;labelSelector&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;app=consul,component in (tls-init,tls-init-cleanup,gossip-encryption-autogenerate)&lt;/span&gt;
    &lt;span class="na"&gt;patch&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|-&lt;/span&gt;
      &lt;span class="s"&gt;- op: add&lt;/span&gt;
        &lt;span class="s"&gt;path: /spec/template/spec/tolerations&lt;/span&gt;
        &lt;span class="s"&gt;value:&lt;/span&gt;
          &lt;span class="s"&gt;- key: example.com/purpose&lt;/span&gt;
            &lt;span class="s"&gt;operator: Equal&lt;/span&gt;
            &lt;span class="s"&gt;value: infra&lt;/span&gt;
            &lt;span class="s"&gt;effect: NoSchedule&lt;/span&gt;
      &lt;span class="s"&gt;- op: add&lt;/span&gt;
        &lt;span class="s"&gt;path: /spec/template/spec/nodeSelector&lt;/span&gt;
        &lt;span class="s"&gt;value:&lt;/span&gt;
          &lt;span class="s"&gt;example.com/purpose: infra&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;mutate's &lt;code&gt;match&lt;/code&gt; takes one value per key, so each Job gets a mutation of&lt;br&gt;
its own, and the three share the operations through an inventory key:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;parameters&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;hook_scheduling&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;op&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;add&lt;/span&gt;
      &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;/spec/template/spec/tolerations&lt;/span&gt;
      &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;key&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;example.com/purpose&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;operator&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;Equal&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;value&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;infra&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;effect&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;NoSchedule&lt;/span&gt;&lt;span class="pi"&gt;}&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;op&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;add&lt;/span&gt;
      &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;/spec/template/spec/nodeSelector&lt;/span&gt;
      &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;example.com/purpose&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;infra&lt;/span&gt;
  &lt;span class="na"&gt;mutations&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;consul-tls-init&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;match&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;batch/v1&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;Job&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;labels&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;app&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;consul&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;component&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;tls-init&lt;/span&gt;&lt;span class="pi"&gt;}}&lt;/span&gt;
      &lt;span class="na"&gt;json6902&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${hook_scheduling}&lt;/span&gt;
    &lt;span class="na"&gt;consul-tls-init-cleanup&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;match&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;batch/v1&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;Job&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;labels&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;app&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;consul&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;component&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;tls-init-cleanup&lt;/span&gt;&lt;span class="pi"&gt;}}&lt;/span&gt;
      &lt;span class="na"&gt;json6902&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${hook_scheduling}&lt;/span&gt;
    &lt;span class="na"&gt;consul-gossip-encryption-autogenerate&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;match&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;batch/v1&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;Job&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;labels&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;app&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;consul&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;component&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;gossip-encryption-autogenerate&lt;/span&gt;&lt;span class="pi"&gt;}}&lt;/span&gt;
      &lt;span class="na"&gt;json6902&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${hook_scheduling}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Without a cluster to ask, &lt;code&gt;helm template&lt;/code&gt; renders the chart's two&lt;br&gt;
PodDisruptionBudgets as &lt;code&gt;policy/v1beta1&lt;/code&gt;, which Kubernetes 1.25 removed,&lt;br&gt;
and the first server-side dry run refused them. Both renders therefore&lt;br&gt;
pass &lt;code&gt;--api-versions policy/v1/PodDisruptionBudget&lt;/code&gt;, which Kapitan takes as&lt;br&gt;
&lt;code&gt;api_versions&lt;/code&gt; in &lt;code&gt;helm_params&lt;/code&gt;.&lt;/p&gt;
&lt;h3&gt;
  
  
  Helm hooks with the patched Jobs
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;kubectl apply&lt;/code&gt; of the whole render would also start tls-init-cleanup,&lt;br&gt;
which deletes the Secrets &lt;code&gt;consul-ca-cert&lt;/code&gt;, &lt;code&gt;consul-ca-key&lt;/code&gt; and&lt;br&gt;
&lt;code&gt;consul-server-cert&lt;/code&gt;. The test applied the mutate output in the order of&lt;br&gt;
&lt;code&gt;helm install&lt;/code&gt;: the pre-install hooks, the templates, then the&lt;br&gt;
post-install Job. The server and the connect injector became ready. The&lt;br&gt;
pre-delete hooks, applied last as &lt;code&gt;helm uninstall&lt;/code&gt; would run them, removed&lt;br&gt;
the three Secrets. Every Job completed on a tainted worker:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;consul-tls-init node=cac-consul-worker2 taint=example.com/purpose=infra:NoSchedule complete=True
consul-gossip-encryption-autogenerate node=cac-consul-worker taint=example.com/purpose=infra:NoSchedule complete=True
consul-tls-init-cleanup node=cac-consul-worker2 taint=example.com/purpose=infra:NoSchedule complete=True
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For &lt;code&gt;helm install&lt;/code&gt; itself, Helm 4 passes hooks to the post-renderer&lt;br&gt;
together with the templates. Helm 3 never did. With Helm 3.22.1 and the&lt;br&gt;
same Kustomize patch as a post-renderer script, &lt;code&gt;helm template&lt;/code&gt; printed&lt;br&gt;
the three Jobs without the fields. A Helm 4 post-renderer is a plugin, and&lt;br&gt;
this one runs the Kustomize patch from the directory it is given:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;kustomize-pr&lt;/span&gt;
&lt;span class="na"&gt;version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;0.1.0&lt;/span&gt;
&lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;postrenderer/v1&lt;/span&gt;
&lt;span class="na"&gt;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;v1&lt;/span&gt;
&lt;span class="na"&gt;runtime&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;subprocess&lt;/span&gt;
&lt;span class="na"&gt;runtimeConfig&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;platformCommand&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;command&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;${HELM_PLUGIN_DIR}/kustomize.sh"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;#!/usr/bin/env bash&lt;/span&gt;
&lt;span class="c"&gt;# stdin: Helm's render, hooks included. $1: the directory with kustomization.yaml.&lt;/span&gt;
&lt;span class="nb"&gt;set&lt;/span&gt; &lt;span class="nt"&gt;-euo&lt;/span&gt; pipefail
&lt;span class="nv"&gt;tmp&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;mktemp&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
&lt;span class="nb"&gt;trap&lt;/span&gt; &lt;span class="s1"&gt;'rm -rf "$tmp"'&lt;/span&gt; EXIT
&lt;span class="nb"&gt;cp&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$1&lt;/span&gt;&lt;span class="s2"&gt;/kustomization.yaml"&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$tmp&lt;/span&gt;&lt;span class="s2"&gt;/"&lt;/span&gt;
&lt;span class="nb"&gt;cat&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$tmp&lt;/span&gt;&lt;span class="s2"&gt;/helm.yaml"&lt;/span&gt;
kustomize build &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$tmp&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;helm plugin &lt;span class="nb"&gt;install&lt;/span&gt; ./kustomize-pr
helm &lt;span class="nb"&gt;install &lt;/span&gt;consul consul &lt;span class="nt"&gt;--repo&lt;/span&gt; https://helm.releases.hashicorp.com &lt;span class="nt"&gt;--version&lt;/span&gt; 2.0.4 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-n&lt;/span&gt; consul &lt;span class="nt"&gt;-f&lt;/span&gt; consul-values.yaml &lt;span class="nt"&gt;--post-renderer&lt;/span&gt; kustomize-pr &lt;span class="nt"&gt;--post-renderer-args&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$PWD&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;--wait&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The plugin's output equalled the Kustomize build. With Helm 4.3.1 on kind,&lt;br&gt;
the release installed, and &lt;code&gt;helm uninstall&lt;/code&gt; ran tls-init-cleanup, each Job&lt;br&gt;
on a tainted worker:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Successfully assigned consul/consul-gossip-encryption-autogenerate-jdm6k to cac-consul-worker
Successfully assigned consul/consul-tls-init-qt7ql to cac-consul-worker3
Successfully assigned consul/consul-tls-init-cleanup-wkt87 to cac-consul-worker
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  APISIX ingress controller chart: pod labels {#apisix-ingress-controller-pod-labels}
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://github.com/apache/apisix-helm-chart/issues/978" rel="noopener noreferrer"&gt;apache/apisix-helm-chart#978&lt;/a&gt;,&lt;br&gt;
opened in June 2026 and at 11 thumbs up, asks for a &lt;code&gt;podLabels&lt;/code&gt; value in&lt;br&gt;
the apisix-ingress-controller chart, so that the controller's pods can&lt;br&gt;
carry labels for observability, scheduling or policies. Nobody has&lt;br&gt;
commented. The apisix chart in the same repository got &lt;code&gt;podLabels&lt;/code&gt; through&lt;br&gt;
pull request #965, merged on 30 September 2026. The controller chart takes&lt;br&gt;
&lt;code&gt;deployment.podAnnotations&lt;/code&gt;, but in 1.4.0, released on 17 September 2026,&lt;br&gt;
&lt;a href="https://github.com/apache/apisix-helm-chart/blob/apisix-ingress-controller-1.4.0/charts/apisix-ingress-controller/templates/deployment.yaml#L42-L43" rel="noopener noreferrer"&gt;&lt;code&gt;deployment.yaml&lt;/code&gt;&lt;/a&gt;&lt;br&gt;
gives the pod template the selector labels only:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;      &lt;span class="na"&gt;labels&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="pi"&gt;{{&lt;/span&gt;&lt;span class="nv"&gt;- include "apisix-ingress-controller-manager.selectorLabels" . | nindent 8&lt;/span&gt; &lt;span class="pi"&gt;}}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The chart's &lt;code&gt;labelsOverride&lt;/code&gt; replaces these selector labels, so a label&lt;br&gt;
set there also lands in the Deployment's &lt;code&gt;spec.selector&lt;/code&gt; and in the&lt;br&gt;
selectors of both Services. Over the installed chart, the API server&lt;br&gt;
refused the change:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;The Deployment "apisix-ingress-controller" is invalid: spec.selector: Invalid value: {"matchLabels":{"app.kubernetes.io/instance":"apisix-ingress-controller","app.kubernetes.io/name":"apisix-ingress-controller","team":"platform"}}: field is immutable
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;add&lt;/code&gt; on a path below the pod template's labels sets one label there and&lt;br&gt;
leaves the selector alone:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;resources&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;helm.yaml&lt;/span&gt;
&lt;span class="na"&gt;patches&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;target&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;group&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;apps&lt;/span&gt;
      &lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Deployment&lt;/span&gt;
      &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;apisix-ingress-controller&lt;/span&gt;
    &lt;span class="na"&gt;patch&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|-&lt;/span&gt;
      &lt;span class="s"&gt;- op: add&lt;/span&gt;
        &lt;span class="s"&gt;path: /spec/template/metadata/labels/team&lt;/span&gt;
        &lt;span class="s"&gt;value: platform&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;parameters&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;mutations&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;apisix-pod-labels&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;match&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;apps/v1&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;Deployment&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;apisix-ingress-controller&lt;/span&gt;&lt;span class="pi"&gt;}&lt;/span&gt;
      &lt;span class="na"&gt;json6902&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;op&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;add&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;/spec/template/metadata/labels/team&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;value&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;platform&lt;/span&gt;&lt;span class="pi"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A key with a slash such as &lt;code&gt;example.com/team&lt;/code&gt; goes into the path as&lt;br&gt;
&lt;code&gt;example.com~1team&lt;/code&gt;, and both tools then set the label &lt;code&gt;example.com/team&lt;/code&gt;.&lt;br&gt;
The chart generates its webhook certificate on every render, so the&lt;br&gt;
comparison of the renders left out that Secret and the webhook&lt;br&gt;
configuration's &lt;code&gt;caBundle&lt;/code&gt;. &lt;code&gt;helm template&lt;/code&gt; renders the chart's CRDs only&lt;br&gt;
with &lt;code&gt;--include-crds&lt;/code&gt;, which Kapitan's helm input passes by default. On&lt;br&gt;
kind, the controller became ready, and its pod carries the label:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;apisix-ingress-controller-85b5bb8fb4-9m4kc labels: {"app.kubernetes.io/instance":"apisix-ingress-controller","app.kubernetes.io/name":"apisix-ingress-controller","pod-template-hash":"85b5bb8fb4","team":"platform"}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Cilium Helm chart: ipFamilyPolicy on cilium-ingress {#cilium-ingress-ipfamilypolicy}
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://github.com/cilium/cilium/issues/27003" rel="noopener noreferrer"&gt;cilium/cilium#27003&lt;/a&gt;,&lt;br&gt;
opened in July 2023, asks for &lt;code&gt;ipFamilyPolicy&lt;/code&gt; and &lt;code&gt;ipFamilies&lt;/code&gt; on the&lt;br&gt;
Service of Cilium's ingress controller. Without them the Service gets the&lt;br&gt;
cluster's primary family only, so on a dual-stack cluster the Ingresses&lt;br&gt;
answer on IPv4 or on IPv6, not on both. The maintainer youngnick wrote in&lt;br&gt;
July 2023 that LB Service customisation was planned for that cycle, and&lt;br&gt;
pinned the issue in December 2023. Chart 1.20.3, released on 15 October&lt;br&gt;
2026,&lt;br&gt;
&lt;a href="https://github.com/cilium/cilium/blob/v1.20.3/install/kubernetes/cilium/templates/cilium-ingress-service.yaml#L21-L51" rel="noopener noreferrer"&gt;renders the Service&lt;/a&gt;&lt;br&gt;
with its ports, type, &lt;code&gt;loadBalancerClass&lt;/code&gt;,&lt;br&gt;
&lt;code&gt;allocateLoadBalancerNodePorts&lt;/code&gt;, &lt;code&gt;loadBalancerIP&lt;/code&gt; and&lt;br&gt;
&lt;code&gt;externalTrafficPolicy&lt;/code&gt;, and 1.21.0-pre.3 renders the same. The chart has&lt;br&gt;
this Service in shared mode only. In dedicated mode the operator creates a&lt;br&gt;
Service per Ingress, which a patch of the chart does not reach.&lt;/p&gt;

&lt;p&gt;The ingress controller needs Cilium's kube-proxy replacement, so the test&lt;br&gt;
ran Cilium as the CNI of a dual-stack kind cluster without kube-proxy:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;kubeProxyReplacement&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;span class="na"&gt;k8sServiceHost&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;cac-cilium-control-plane&lt;/span&gt;
&lt;span class="na"&gt;k8sServicePort&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;6443&lt;/span&gt;
&lt;span class="na"&gt;ipam&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;mode&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;kubernetes&lt;/span&gt;
&lt;span class="na"&gt;ipv6&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;enabled&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;span class="na"&gt;ingressController&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;enabled&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
  &lt;span class="na"&gt;loadbalancerMode&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;shared&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;resources&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;helm.yaml&lt;/span&gt;
&lt;span class="na"&gt;patches&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;target&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Service&lt;/span&gt;
      &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;cilium-ingress&lt;/span&gt;
    &lt;span class="na"&gt;patch&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|-&lt;/span&gt;
      &lt;span class="s"&gt;- op: add&lt;/span&gt;
        &lt;span class="s"&gt;path: /spec/ipFamilyPolicy&lt;/span&gt;
        &lt;span class="s"&gt;value: RequireDualStack&lt;/span&gt;
      &lt;span class="s"&gt;- op: add&lt;/span&gt;
        &lt;span class="s"&gt;path: /spec/ipFamilies&lt;/span&gt;
        &lt;span class="s"&gt;value: [IPv4, IPv6]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;parameters&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;mutations&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;cilium-ingress-dual-stack&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;match&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;v1&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;Service&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;cilium-ingress&lt;/span&gt;&lt;span class="pi"&gt;}&lt;/span&gt;
      &lt;span class="na"&gt;json6902&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;op&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;add&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;/spec/ipFamilyPolicy&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;value&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;RequireDualStack&lt;/span&gt;&lt;span class="pi"&gt;}&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;op&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;add&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;/spec/ipFamilies&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;value&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;IPv4&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;IPv6&lt;/span&gt;&lt;span class="pi"&gt;]}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The chart generates the Hubble certificates on every render, so the&lt;br&gt;
comparison of the renders left out those Secrets. The plain chart gave the&lt;br&gt;
Service one IPv4 cluster IP. The mutate output, applied over it, changed&lt;br&gt;
the existing Service in place, and 60 seconds later the operator had not&lt;br&gt;
reverted it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ipFamilyPolicy=SingleStack ipFamilies=["IPv4"] clusterIPs=["10.96.179.251"] loadBalancer=198.51.100.0
ipFamilyPolicy=RequireDualStack ipFamilies=["IPv4","IPv6"] clusterIPs=["10.96.179.251","fd00:10:96::b12c"] loadBalancer=198.51.100.0 2001:db8::
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;LB-IPAM had a pool with one block of each family from the documentation&lt;br&gt;
ranges and gave the Service an address of each. An Ingress for an echo&lt;br&gt;
backend then answered on all four addresses from a node, with &lt;code&gt;curl&lt;/code&gt; in&lt;br&gt;
the node's network namespace:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;10.96.179.251 from node: 200, from pod: 000
fd00:10:96::b12c from node: 200, from pod: 200
198.51.100.0 from node: 200, from pod: 000
2001:db8:: from node: 200, from pod: 200
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;From a pod, requests over IPv4 timed out on this test host, with the plain&lt;br&gt;
chart as well and on an IPv4-only kind cluster too. The patch does not&lt;br&gt;
cause it, and the test does not cover that path.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;ipFamilies&lt;/code&gt; with both families needs a dual-stack cluster, whatever the&lt;br&gt;
policy. On an IPv4-only kind cluster the server-side dry run refused the&lt;br&gt;
patched Service:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;* spec.ipFamilyPolicy: Invalid value: "RequireDualStack": this cluster is not configured for dual-stack services
* spec.ipFamilies[1]: Invalid value: "IPv6": not configured on this cluster
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With &lt;code&gt;PreferDualStack&lt;/code&gt; it still refused &lt;code&gt;ipFamilies[1]&lt;/code&gt;. &lt;code&gt;PreferDualStack&lt;/code&gt;&lt;br&gt;
without &lt;code&gt;ipFamilies&lt;/code&gt; passed there and got IPv4 only.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://kadet-hub.org/blog/kustomize-json-patch-helm-chart/" rel="noopener noreferrer"&gt;kadet-hub.org&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>kubernetes</category>
      <category>devops</category>
      <category>kapitan</category>
      <category>gitops</category>
    </item>
  </channel>
</rss>
