<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" xml:lang="en"><generator uri="https://gohugo.io/" version="0.163.2">Hugo</generator><title type="html">Jsonnet-as-a-Service</title><link href="https://jaas.projects.metio.wtf/" rel="alternate" type="text/html" title="html"/><link href="https://jaas.projects.metio.wtf/index.xml" rel="alternate" type="application/rss+xml" title="rss"/><link href="https://jaas.projects.metio.wtf/atom.xml" rel="self" type="application/atom+xml" title="atom"/><link href="https://jaas.projects.metio.wtf/humans.txt" rel="alternate" type="text/plain" title="humans"/><link href="https://jaas.projects.metio.wtf/foaf.rdf" rel="alternate" type="application/rdf+xml" title="foaf"/><link href="https://jaas.projects.metio.wtf/llms-full.txt" rel="alternate" type="text/plain" title="llms"/><link href="https://jaas.projects.metio.wtf/llms.txt" rel="alternate" type="text/plain" title="llmsindex"/><updated>2026-06-18T09:06:35+00:00</updated><author><name>metio.wtf</name></author><id>https://jaas.projects.metio.wtf/</id><entry><title type="html">Admission webhook</title><link href="https://jaas.projects.metio.wtf/usage/admission-webhook/?utm_source=atom_feed" rel="alternate" type="text/html"/><link href="https://jaas.projects.metio.wtf/usage/alerting/?utm_source=atom_feed" rel="related" type="text/html" title="Alerting"/><link href="https://jaas.projects.metio.wtf/usage/creating-sources/?utm_source=atom_feed" rel="related" type="text/html" title="Creating source artifacts"/><link href="https://jaas.projects.metio.wtf/api/externalartifact/?utm_source=atom_feed" rel="related" type="text/html" title="ExternalArtifact output contract"/><link href="https://jaas.projects.metio.wtf/usage/jsonnet-libraries/?utm_source=atom_feed" rel="related" type="text/html" title="Jsonnet libraries"/><link href="https://jaas.projects.metio.wtf/api/jsonnetlibrary/?utm_source=atom_feed" rel="related" type="text/html" title="JsonnetLibrary"/><id>https://jaas.projects.metio.wtf/usage/admission-webhook/</id><published>0001-01-01T00:00:00+00:00</published><updated>2026-06-16T20:48:39+02:00</updated><content type="html"><![CDATA[<blockquote>The opt-in validating webhook for JsonnetSnippet, what it rejects, the failure-policy trade-off, and the two TLS provisioning modes.</blockquote><p>JaaS ships an optional validating admission webhook for <code>JsonnetSnippet</code>. It is
independent of, but layered on top of, <a href="/usage/operator-mode/">operator mode</a>
.</p>
<h2 id="enabling-it">Enabling it</h2>
<p>Set <code>--enable-webhook</code> to boot the webhook server. It requires
<code>--enable-flux-integration</code> (the webhook is wired only inside the operator boot
path; enabling it alone is rejected as a flag error) and TLS material — <code>tls.crt</code>
and <code>tls.key</code> — under <code>--webhook-cert-dir</code> (default
<code>/tmp/k8s-webhook-server/serving-certs</code>). The server binds <code>--webhook-port</code>
(default <code>9443</code>).</p>
<h2 id="what-it-validates">What it validates</h2>
<p>The webhook rejects a <code>JsonnetSnippet</code> whose <code>spec.externalVariables</code> declares a
key that collides with an operator-level <code>--ext-var</code>. An operator-supplied
external variable always wins, so a snippet that tries to redeclare one would
render against a value it does not control; the webhook refuses the snippet at
admission time instead.</p>
<p>The reconciler enforces the same invariant as a fallback, so a snippet that
bypasses admission — for example when the webhook is unreachable under a
<code>failurePolicy: Ignore</code> — still fails at reconcile rather than rendering with the
wrong value.</p>
<h2 id="failure-policy-trade-off">Failure policy trade-off</h2>
<p>The Helm chart defaults <code>operator.webhook.failurePolicy: Fail</code>. With <code>Fail</code>, a
webhook outage blocks every <code>JsonnetSnippet</code> create and update cluster-wide until
the operator is back. During a rolling update that window is typically under five
seconds, because leader election releases the lease on context-cancel and the next
replica takes over immediately.</p>
<p>If your CI or GitOps tooling cannot tolerate even that window, narrow or relax the
webhook:</p>
<ul>
<li><code>operator.webhook.objectSelector</code> — match only snippets carrying a label, e.g.
require <code>jaas.metio.wtf/managed: &quot;true&quot;</code>.</li>
<li><code>operator.webhook.namespaceSelector</code> — opt in per namespace.</li>
<li><code>failurePolicy: Ignore</code> — let create/update through when the webhook is
unreachable, relying on the reconciler-side fallback to catch the colliding-key
case.</li>
</ul>
<h2 id="tls-provisioning">TLS provisioning</h2>
<p><code>--webhook-cert-mode</code> selects how the serving certificate is provisioned.</p>
<h3 id="cert-manager-default">cert-manager (default)</h3>
<p><code>--webhook-cert-mode=cert-manager</code> expects external tooling to provision
<code>tls.crt</code>/<code>tls.key</code>. The Helm chart renders a <code>cert-manager.io/v1</code> Certificate and
mounts the issued Secret into the pod at <code>--webhook-cert-dir</code>. cert-manager
handles renewal; the webhook server hot-reloads TLS when the mounted files change.</p>
<h3 id="self-signed">self-signed</h3>
<p><code>--webhook-cert-mode=self-signed</code> makes the operator generate a CA and serving
certificate in-pod, write them to <code>--webhook-cert-dir</code>, and patch the named
<code>ValidatingWebhookConfiguration</code>&rsquo;s <code>caBundle</code> so the apiserver trusts the chain. A
background renewer regenerates and re-writes the files before expiry, and the
webhook server hot-reloads without a restart. The relevant flags:</p>
<table>
	<thead>
			<tr>
					<th>Flag</th>
					<th>Purpose</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td><code>--webhook-validating-config-name</code></td>
					<td>Name of the <code>ValidatingWebhookConfiguration</code> whose <code>caBundle</code> is patched. Required in this mode.</td>
			</tr>
			<tr>
					<td><code>--webhook-service-name</code></td>
					<td>Service name the webhook is reachable through; used to build the certificate SANs (default <code>jaas-webhook</code>).</td>
			</tr>
			<tr>
					<td><code>--webhook-service-namespace</code></td>
					<td>Namespace the webhook Service lives in. Empty falls back to <code>--leader-election-namespace</code>, then to the in-cluster downward API.</td>
			</tr>
			<tr>
					<td><code>--webhook-cert-validity</code></td>
					<td>Validity of the self-signed serving certificate (default <code>8760h</code>, one year).</td>
			</tr>
			<tr>
					<td><code>--webhook-port</code></td>
					<td>Port the webhook server binds to (default <code>9443</code>).</td>
			</tr>
	</tbody>
</table>
<p>In self-signed mode the operator needs <code>get</code>/<code>update</code> on the named
<code>ValidatingWebhookConfiguration</code>. Multiple replicas bootstrapping at once during a
rolling update converge on a combined <code>caBundle</code> rather than clobbering each
other, so each replica&rsquo;s CA stays trusted across the rollout.</p>
<p>The full flag list with defaults is on the
<a href="/installation/configuration/">configuration page</a>
.</p>
]]></content><category scheme="https://jaas.projects.metio.wtf/tags/operator" term="operator" label="operator"/><category scheme="https://jaas.projects.metio.wtf/tags/webhook" term="webhook" label="webhook"/><category scheme="https://jaas.projects.metio.wtf/tags/tls" term="tls" label="tls"/></entry><entry><title type="html">Alerting</title><link href="https://jaas.projects.metio.wtf/usage/alerting/?utm_source=atom_feed" rel="alternate" type="text/html"/><link href="https://jaas.projects.metio.wtf/usage/metrics/?utm_source=atom_feed" rel="related" type="text/html" title="Metrics"/><link href="https://jaas.projects.metio.wtf/usage/logging/?utm_source=atom_feed" rel="related" type="text/html" title="Logging"/><link href="https://jaas.projects.metio.wtf/usage/observability/?utm_source=atom_feed" rel="related" type="text/html" title="Observability"/><link href="https://jaas.projects.metio.wtf/usage/tracing/?utm_source=atom_feed" rel="related" type="text/html" title="Tracing"/><link href="https://jaas.projects.metio.wtf/usage/admission-webhook/?utm_source=atom_feed" rel="related" type="text/html" title="Admission webhook"/><id>https://jaas.projects.metio.wtf/usage/alerting/</id><published>0001-01-01T00:00:00+00:00</published><updated>2026-06-17T22:19:37+02:00</updated><content type="html"><![CDATA[<blockquote>The opt-in PrometheusRule alert catalog with tunable thresholds and runbook links, plus Kubernetes Events routed through Flux&rsquo;s notification-controller.</blockquote><p>JaaS turns a sustained problem into a notification two ways: a Prometheus
<code>PrometheusRule</code> that pages on its <a href="/usage/metrics/">metrics</a>
, and Kubernetes
Events that Flux&rsquo;s notification-controller can route to chat or e-mail.</p>
<h2 id="the-binary">The binary</h2>
<p>The operator emits a standard Kubernetes <code>Event</code> on every Ready-condition
transition — <code>Normal</code> for <code>Synced</code>, <code>Warning</code> for every other reason. The reason
string fills both the event <code>reason</code> and <code>action</code>. These Events need no flag to
enable; they are written whenever the operator reconciles.</p>
<p>The operator also threads runbook links into its own status automatically: every
actionable Ready-condition Message gains a
<code>(runbook: https://jaas.projects.metio.wtf/runbooks/&lt;reason&gt;/)</code> suffix, so
<code>kubectl describe jsonnetsnippet</code> points straight at the matching page. Healthy
or intentional states (<code>Synced</code>, <code>Suspended</code>, <code>Pending</code>) get no suffix.</p>
<h3 id="routing-events-through-flux">Routing Events through Flux</h3>
<p>Routing the Events is Flux&rsquo;s <code>notification-controller</code>: target an <code>Alert</code> CR at
<code>kind: JsonnetSnippet</code> and JaaS needs no <code>Provider</code>/<code>Alert</code> plumbing of its own.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nt">apiVersion</span><span class="p">:</span><span class="w"> </span><span class="l">notification.toolkit.fluxcd.io/v1beta3</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">kind</span><span class="p">:</span><span class="w"> </span><span class="l">Alert</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">metadata</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">jaas-snippets</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">namespace</span><span class="p">:</span><span class="w"> </span><span class="l">flux-system</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">spec</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">providerRef</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">slack</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">eventSeverity</span><span class="p">:</span><span class="w"> </span><span class="l">warn  </span><span class="w"> </span><span class="c"># &#39;info&#39; to include success events</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">eventSources</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span>- <span class="nt">kind</span><span class="p">:</span><span class="w"> </span><span class="l">JsonnetSnippet</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="s1">&#39;*&#39;</span><span class="w">
</span></span></span></code></pre></div><p>Wire whatever <code>Provider</code> you already use for Flux source CRs; see the
<a href="https://fluxcd.io/">Flux notification-controller documentation</a>
 for provider
configuration.</p>
<h3 id="the-alert-catalog">The alert catalog</h3>
<p>The chart ships a starter alert set on the custom metrics plus a handful of
controller-runtime signals. Each alert carries its remediation page as a
<code>runbook_url</code> annotation so Alertmanager renders a direct link:</p>
<table>
	<thead>
			<tr>
					<th>Alert</th>
					<th>Severity</th>
					<th>Fires when</th>
					<th>Threshold knobs (default)</th>
					<th>Runbook</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td><code>JaaSSnippetReconcileErrorsHigh</code></td>
					<td>warning</td>
					<td>A snippet keeps flipping to Ready=False (excluding <code>Synced</code>/<code>Suspended</code>/<code>Pending</code>).</td>
					<td><code>reconcileErrorRate</code> (0.1/s), <code>reconcileErrorDuration</code> (10m)</td>
					<td>per-reason page under <a href="/runbooks/"><code>/runbooks/</code></a>
</td>
			</tr>
			<tr>
					<td><code>JaaSSnippetArtifactGrowing</code></td>
					<td>warning</td>
					<td>p99 <code>jaas_snippet_rendered_bytes</code> exceeds the size ceiling.</td>
					<td><code>artifactSizeBytes</code> (16 MiB), <code>artifactSizeDuration</code> (30m)</td>
					<td><a href="/runbooks/artifacttoolarge/">artifacttoolarge</a>
</td>
			</tr>
			<tr>
					<td><code>JaaSControllerWorkqueueDepthHigh</code></td>
					<td>warning</td>
					<td>The <code>jsonnetsnippet</code> workqueue can&rsquo;t drain.</td>
					<td><code>workqueueDepth</code> (50), <code>workqueueDuration</code> (15m)</td>
					<td><a href="/runbooks/workqueue-saturation/">workqueue-saturation</a>
</td>
			</tr>
			<tr>
					<td><code>JaaSReconcileLatencyHigh</code></td>
					<td>warning</td>
					<td>p99 reconcile time crosses the ceiling.</td>
					<td><code>reconcileLatencySeconds</code> (30), <code>reconcileLatencyDuration</code> (15m)</td>
					<td><a href="/runbooks/reconcile-latency/">reconcile-latency</a>
</td>
			</tr>
			<tr>
					<td><code>JaaSOperatorPodDown</code></td>
					<td>critical</td>
					<td>A jaas pod stays NotReady.</td>
					<td><code>podDownDuration</code> (5m)</td>
					<td><a href="/runbooks/operator-pod-down/">operator-pod-down</a>
</td>
			</tr>
			<tr>
					<td><code>JaaSStorageSweepFailures</code></td>
					<td>warning</td>
					<td>Background sweeps fail per hour above the floor.</td>
					<td><code>sweepFailuresPerHour</code> (3), <code>sweepFailuresDuration</code> (30m)</td>
					<td><a href="/runbooks/storage-recovery/">storage-recovery</a>
</td>
			</tr>
			<tr>
					<td><code>JaaSWebhookCertRenewalFailing</code></td>
					<td>critical</td>
					<td>Self-signed cert renewal fails per hour above the floor.</td>
					<td><code>webhookCertRenewalFailuresPerHour</code> (1), <code>webhookCertRenewalFailuresDuration</code> (30m)</td>
					<td><a href="/runbooks/webhook-cert-renewal/">webhook-cert-renewal</a>
</td>
			</tr>
			<tr>
					<td><code>JaaSTenantTokenMintFailing</code></td>
					<td>warning</td>
					<td>Token mints fail for a <code>(namespace, serviceAccount)</code> pair.</td>
					<td><code>tenantTokenMintFailureRate</code> (0.01/s), <code>tenantTokenMintFailureDuration</code> (10m)</td>
					<td><a href="/runbooks/rbacdenied/">rbacdenied</a>
</td>
			</tr>
			<tr>
					<td><code>JaaSForceDropsAccumulating</code></td>
					<td>warning</td>
					<td>Snippet finalizers are force-dropped per hour above the floor.</td>
					<td><code>forceDropsPerHour</code> (0), <code>forceDropsDuration</code> (5m)</td>
					<td><a href="/runbooks/storage-recovery/">storage-recovery</a>
</td>
			</tr>
			<tr>
					<td><code>JaaSCRDWatchEngagementFailing</code></td>
					<td>warning</td>
					<td>A Flux source watch won&rsquo;t engage for a GVK.</td>
					<td><code>crdWatchEngagementFailuresPerHour</code> (1), <code>crdWatchEngagementFailuresDuration</code> (30m)</td>
					<td><a href="/runbooks/crd-watch-engagement/">crd-watch-engagement</a>
</td>
			</tr>
			<tr>
					<td><code>JaaSEvalSaturation</code></td>
					<td>warning</td>
					<td>In-flight evals exceed the saturation ratio of the cap (guarded on the cap being non-zero).</td>
					<td><code>evalSaturationRatio</code> (0.9), <code>evalSaturationDuration</code> (10m)</td>
					<td><a href="/runbooks/eval-saturation/">eval-saturation</a>
</td>
			</tr>
			<tr>
					<td><code>JaaSEvalRejected</code></td>
					<td>warning</td>
					<td>The semaphore turns evals away per second above the floor.</td>
					<td><code>evalRejectedRate</code> (0.05/s), <code>evalRejectedDuration</code> (10m)</td>
					<td><a href="/runbooks/eval-saturation/">eval-saturation</a>
</td>
			</tr>
			<tr>
					<td><code>JaaSEvalLeakedGoroutines</code></td>
					<td>warning</td>
					<td>Orphan eval goroutines persist above the floor — a runaway snippet.</td>
					<td><code>evalLeakedFloor</code> (0), <code>evalLeakedDuration</code> (5m)</td>
					<td><a href="/runbooks/eval-saturation/">eval-saturation</a>
</td>
			</tr>
	</tbody>
</table>
<p><code>JaaSSnippetReconcileErrorsHigh</code> templates its runbook URL on the failing reason,
so it lands on the matching per-reason page under <a href="/runbooks/"><code>/runbooks/</code></a>
. Each
Ready-condition reason and each alert maps to a remediation page there.</p>
<h2 id="the-helm-chart">The Helm chart</h2>
<p>The <code>PrometheusRule</code> is opt-in under <code>operator.metrics.prometheusRule</code> and needs
the Prometheus Operator&rsquo;s <code>monitoring.coreos.com/v1</code> API in the cluster:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nt">operator</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">enabled</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">metrics</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">enabled</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">prometheusRule</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">enabled</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">interval</span><span class="p">:</span><span class="w"> </span><span class="l">30s</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="c"># Labels your Prometheus instance selects PrometheusRules on.</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">labels</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">release</span><span class="p">:</span><span class="w"> </span><span class="l">kube-prometheus</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="c"># Merged onto every rendered alert — route all jaas alerts</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="c"># through one Alertmanager receiver.</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">extraAlertLabels</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">team</span><span class="p">:</span><span class="w"> </span><span class="l">platform</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="c"># Annotation key the runbook URL lands under (Prometheus-operator</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="c"># convention is runbook_url).</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">runbookAnnotationKey</span><span class="p">:</span><span class="w"> </span><span class="l">runbook_url</span><span class="w">
</span></span></span></code></pre></div><p>Every threshold is a knob under <code>operator.metrics.prometheusRule.thresholds</code>, so
the noise floor is tunable without copy-pasting rule bodies. To silence a
built-in alert, raise its threshold to an impossibly high value — there is no
per-alert disable toggle, and the threshold pattern keeps &ldquo;this alert is
intentionally inert&rdquo; visible in the chart values. Cluster-specific rules append
under a separate group via <code>operator.metrics.prometheusRule.extraRules</code>.</p>
]]></content><category scheme="https://jaas.projects.metio.wtf/tags/operator" term="operator" label="operator"/><category scheme="https://jaas.projects.metio.wtf/tags/alerts" term="alerts" label="alerts"/><category scheme="https://jaas.projects.metio.wtf/tags/prometheus" term="prometheus" label="prometheus"/><category scheme="https://jaas.projects.metio.wtf/tags/observability" term="observability" label="observability"/></entry><entry><title type="html">ArtifactTooLarge</title><link href="https://jaas.projects.metio.wtf/runbooks/artifacttoolarge/?utm_source=atom_feed" rel="alternate" type="text/html"/><link href="https://jaas.projects.metio.wtf/runbooks/storage-recovery/?utm_source=atom_feed" rel="related" type="text/html" title="Storage backend recovery"/><link href="https://jaas.projects.metio.wtf/runbooks/crd-watch-engagement/?utm_source=atom_feed" rel="related" type="text/html" title="CRD watch engagement failing"/><link href="https://jaas.projects.metio.wtf/runbooks/crossnamespacerefrejected/?utm_source=atom_feed" rel="related" type="text/html" title="CrossNamespaceRefRejected"/><link href="https://jaas.projects.metio.wtf/runbooks/dependencycycle/?utm_source=atom_feed" rel="related" type="text/html" title="DependencyCycle"/><link href="https://jaas.projects.metio.wtf/runbooks/eval-saturation/?utm_source=atom_feed" rel="related" type="text/html" title="Eval-concurrency saturation"/><id>https://jaas.projects.metio.wtf/runbooks/artifacttoolarge/</id><published>0001-01-01T00:00:00+00:00</published><updated>2026-06-16T20:48:39+02:00</updated><content type="html"><![CDATA[<blockquote>The snippet&rsquo;s rendered output exceeds the operator&rsquo;s per-artifact byte cap</blockquote><h2 id="symptom">Symptom</h2>
<p><code>READY=False</code>, <code>REASON=ArtifactTooLarge</code>. The Message states the rendered byte count and the configured cap.</p>
<h2 id="cause">Cause</h2>
<p>The snippet&rsquo;s rendered output exceeds the operator&rsquo;s <code>--max-artifact-bytes</code> (Helm: <code>operator.storage.maxArtifactBytes</code>). The cap is a defense-in-depth control — one runaway snippet shouldn&rsquo;t fill a shared storage volume.</p>
<p>Common triggers:</p>
<ul>
<li>a snippet generating massive arrays via <code>std.range(n)</code> with a much larger <code>n</code> than intended</li>
<li>accidentally inlining a large data fixture via <code>importstr</code></li>
<li>forgetting to project / filter when fanning out per-tenant configs</li>
</ul>
<h2 id="diagnosis">Diagnosis</h2>
<p>Check the rendered size locally:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">jsonnet /tmp/snippet/&lt;entry-file&gt; <span class="p">|</span> wc -c
</span></span></code></pre></div><h2 id="remediation">Remediation</h2>
<p>Two paths:</p>
<ol>
<li><strong>Shrink the output.</strong> Inspect the snippet for unintended fan-out; project only the fields downstream consumers actually need.</li>
<li><strong>Raise the cap.</strong> <code>--max-artifact-bytes=10485760</code> (10 MiB) gives more headroom. Pair with PVC sizing in the chart so the volume can hold N rev&rsquo;s worth of the new max.</li>
</ol>
<p>If many snippets are bumping against the cap, the cap itself may be too low for the workload — review the cluster-wide ratio of total storage to per-snippet rev count.</p>
]]></content><category scheme="https://jaas.projects.metio.wtf/tags/runbooks" term="runbooks" label="runbooks"/><category scheme="https://jaas.projects.metio.wtf/tags/troubleshooting" term="troubleshooting" label="troubleshooting"/><category scheme="https://jaas.projects.metio.wtf/tags/storage" term="storage" label="storage"/></entry><entry><title type="html">Building and Testing</title><link href="https://jaas.projects.metio.wtf/contributing/building/?utm_source=atom_feed" rel="alternate" type="text/html"/><link href="https://jaas.projects.metio.wtf/contributing/ci-and-release/?utm_source=atom_feed" rel="related" type="text/html" title="CI and Releases"/><id>https://jaas.projects.metio.wtf/contributing/building/</id><published>0001-01-01T00:00:00+00:00</published><updated>2026-06-16T20:48:39+02:00</updated><content type="html"><![CDATA[<blockquote>Build JaaS and run its full test suite inside the containerized dev shell.</blockquote><p>The host needs no Go toolchain. All build and test commands run inside a containerized dev shell defined by <code>dev/Containerfile</code> and driven by <code>ilo</code>. The <code>.ilo.rc</code> at the repo root supplies the shell arguments, so the short form is always available:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">ilo bash -c <span class="s1">&#39;&lt;command&gt;&#39;</span>
</span></span></code></pre></div><p>The dev shell pre-installs the Go toolchain, all static-analysis tools, <code>controller-gen</code>, and the envtest asset bundle. The Go module cache is mounted from <code>${XDG_CACHE_HOME}/go</code> so repeated builds do not re-download dependencies.</p>
<h2 id="building">Building</h2>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">ilo bash -c <span class="s1">&#39;go build -o jaas .&#39;</span>
</span></span></code></pre></div><p>The <code>Dockerfile</code> builds the production image. It accepts <code>VERSION</code> and <code>COMMIT</code> build args:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">docker build -t ghcr.io/metio/jaas:dev .
</span></span><span class="line"><span class="cl">docker build --build-arg <span class="nv">VERSION</span><span class="o">=</span>v1 --build-arg <span class="nv">COMMIT</span><span class="o">=</span>abc123 -t ghcr.io/metio/jaas:dev .
</span></span></code></pre></div><h2 id="regenerating-generated-code">Regenerating generated code</h2>
<p>CRD manifests and the <code>zz_generated.deepcopy.go</code> file are generated by <code>controller-gen</code>. Run this after touching <code>api/v1/</code> types:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl"><span class="c1"># Regenerate deep-copy functions</span>
</span></span><span class="line"><span class="cl">ilo bash -c <span class="s1">&#39;controller-gen object paths=./api/v1/...&#39;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Regenerate CRD manifests under config/crd/bases/</span>
</span></span><span class="line"><span class="cl">ilo bash -c <span class="s1">&#39;controller-gen crd paths=./api/v1/... output:crd:dir=./config/crd/bases&#39;</span>
</span></span></code></pre></div><h2 id="static-analysis">Static analysis</h2>
<p>golangci-lint is not used. The standalone tools below run directly, both in CI and locally inside the dev shell:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">ilo bash -c <span class="s1">&#39;go vet ./...&#39;</span>
</span></span><span class="line"><span class="cl">ilo bash -c <span class="s1">&#39;staticcheck ./...&#39;</span>       <span class="c1"># config: staticcheck.conf, checks = [&#34;all&#34;]</span>
</span></span><span class="line"><span class="cl">ilo bash -c <span class="s1">&#39;gofumpt -l .&#39;</span>           <span class="c1"># empty output means clean; any output is a failure</span>
</span></span><span class="line"><span class="cl">ilo bash -c <span class="s1">&#39;gosec ./...&#39;</span>            <span class="c1"># inline #nosec justifications silence false positives</span>
</span></span><span class="line"><span class="cl">ilo bash -c <span class="s1">&#39;govulncheck ./...&#39;</span>      <span class="c1"># reachable-from-code advisories only</span>
</span></span><span class="line"><span class="cl">ilo bash -c <span class="s1">&#39;arch-go&#39;</span>                <span class="c1"># architecture rules; config: arch-go.yml</span>
</span></span></code></pre></div><h2 id="test-layers">Test layers</h2>
<h3 id="pure-unit-tests">Pure unit tests</h3>
<p>Table-driven tests with no external state. They live next to the code they cover across <code>internal/...</code> and <code>api/v1/</code>. Several act as drift gates: <code>conditions_test.go</code> verifies that every <code>Reason*</code> constant has a matching <code>docs/runbooks/&lt;reason&gt;.md</code>, and <code>TestErrorResponse_StableCodeValues</code> pins the wire-level <code>ErrCode*</code> strings against accidental rename.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">ilo bash -c <span class="s1">&#39;go test -count=1 -race -cover ./...&#39;</span>
</span></span></code></pre></div><p>To run a single test by name:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">ilo bash -c <span class="s1">&#39;go test -count=1 -v -run TestName ./internal/handler/&#39;</span>
</span></span></code></pre></div><h3 id="envtest-backed-operator-tests">Envtest-backed operator tests</h3>
<p>Files named <code>envtest_*_test.go</code> (in <code>internal/operator/</code>, <code>internal/webhook/selfsigned/</code>, and <code>main_envtest_test.go</code> at the repo root) boot a real <code>kube-apiserver</code> and <code>etcd</code> via controller-runtime&rsquo;s <code>envtest</code> package and run the reconciler, webhook, and full <code>run(...)</code> function against them.</p>
<p>The tests share one apiserver instance per test binary, guarded by a <code>sync.Once</code>, so the startup cost is paid once. Each test <code>t.Skip</code>s when <code>KUBEBUILDER_ASSETS</code> is unset — there is no build tag. The dev shell pre-stages the envtest asset bundle via <code>setup-envtest</code> (pinned <code>ENVTEST_K8S_VERSION</code>) and exports <code>KUBEBUILDER_ASSETS</code>, so these tests run by default inside <code>ilo bash</code>. On a host without the bundle they silently skip.</p>
<p>The envtest harness sets <code>Config.SkipImpersonation</code> (the only place that setting is allowed) and defaults <code>MetricsBindAddress</code> to <code>&quot;0&quot;</code> so parallel test cases do not fight over the metrics port.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">ilo bash -c <span class="s1">&#39;go test -count=1 -race -cover ./...&#39;</span>
</span></span></code></pre></div><h3 id="golden--example-end-to-end-tests">Golden / example end-to-end tests</h3>
<p><code>examples_test.go</code> boots the full binary via <code>runInBackground</code> and asserts HTTP responses against golden files under <code>testdata/golden/</code>. Comparison is semantic — both sides are parsed as JSON and compared on the parsed values, so whitespace and key ordering are irrelevant.</p>
<p>After changing an example or adding a new one, regenerate the golden files:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">ilo bash -c <span class="s1">&#39;go test -update ./...&#39;</span>
</span></span></code></pre></div><p>Inspect and commit the diff in <code>testdata/golden/</code>.</p>
<h3 id="fuzz-tests">Fuzz tests</h3>
<p>Fuzz targets in <code>internal/handler/</code>, <code>internal/sources/</code>, and <code>internal/urlguard/</code> harden the request path, the tar/gzip artifact unpacker, and the SSRF URL/IP parser against adversarial input. CI exercises their seed corpus as ordinary unit tests. To fuzz interactively:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">ilo bash -c <span class="s1">&#39;go test -fuzz=FuzzName -fuzztime=30s ./internal/urlguard/&#39;</span>
</span></span></code></pre></div><h3 id="benchmarks">Benchmarks</h3>
<p>Throughput benchmarks in <code>internal/eval/</code>, <code>internal/storage/</code>, and <code>internal/operator/</code> cover reconcile throughput, watch mapping, and the tenant-client cache. They are baselines, not merge gates. The reconcile benchmark is envtest-backed and skips without <code>KUBEBUILDER_ASSETS</code>.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">ilo bash -c <span class="s1">&#39;go test -bench=. -benchmem -run=^$ ./internal/operator/&#39;</span>
</span></span></code></pre></div><h3 id="kind-operator-smoke-tests">Kind operator smoke tests</h3>
<p>The cluster-level layer runs outside <code>go test</code>. Pure-<code>kubectl</code> bash scenarios in <code>hack/smoke/</code> run against a real kind cluster via <code>.github/workflows/kind-smoke.yml</code>. To run a scenario locally against any reachable cluster, deploy JaaS and invoke the scenario scripts directly:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">hack/smoke/scenario-inline-files.sh
</span></span></code></pre></div><p>See <a href="/contributing/ci-and-release/">CI and releases</a>
 for how the smoke layer fits into the two-angle end-to-end strategy.</p>
]]></content><category scheme="https://jaas.projects.metio.wtf/tags/contributing" term="contributing" label="contributing"/><category scheme="https://jaas.projects.metio.wtf/tags/building" term="building" label="building"/><category scheme="https://jaas.projects.metio.wtf/tags/testing" term="testing" label="testing"/></entry><entry><title type="html">CI and Releases</title><link href="https://jaas.projects.metio.wtf/contributing/ci-and-release/?utm_source=atom_feed" rel="alternate" type="text/html"/><link href="https://jaas.projects.metio.wtf/contributing/building/?utm_source=atom_feed" rel="related" type="text/html" title="Building and Testing"/><id>https://jaas.projects.metio.wtf/contributing/ci-and-release/</id><published>0001-01-01T00:00:00+00:00</published><updated>2026-06-16T20:48:39+02:00</updated><content type="html"><![CDATA[<blockquote>The verify.yml PR gate, the static-analysis tool set, and the calendar-based hand-rolled release pipeline.</blockquote><h2 id="static-analysis">Static analysis</h2>
<p>golangci-lint is not used. The tools below run directly, both in CI and locally inside the dev shell (see <a href="/contributing/building/">Building and Testing</a>
). Every tool is a separate, auditable binary with its own config file.</p>
<table>
	<thead>
			<tr>
					<th>Tool</th>
					<th>Scope</th>
					<th>Config</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td><code>go vet</code> (all analyzers)</td>
					<td>Go correctness</td>
					<td>—</td>
			</tr>
			<tr>
					<td><a href="https://staticcheck.dev">staticcheck</a>
</td>
					<td>Bugs, simplifications, style</td>
					<td><code>staticcheck.conf</code> (<code>checks = [&quot;all&quot;]</code>)</td>
			</tr>
			<tr>
					<td><a href="https://github.com/securego/gosec">gosec</a>
</td>
					<td>Security patterns</td>
					<td>inline <code>#nosec</code> justifications</td>
			</tr>
			<tr>
					<td><a href="https://go.dev/security/vuln/">govulncheck</a>
</td>
					<td>Known vulnerabilities in the dependency graph</td>
					<td>—</td>
			</tr>
			<tr>
					<td><a href="https://github.com/arch-go/arch-go">arch-go</a>
</td>
					<td>Architecture rules</td>
					<td><code>arch-go.yml</code></td>
			</tr>
			<tr>
					<td><a href="https://github.com/mvdan/gofumpt">gofumpt</a>
</td>
					<td>Strict formatting</td>
					<td>—</td>
			</tr>
			<tr>
					<td><a href="https://reuse.software">REUSE</a>
</td>
					<td>License / copyright metadata on every file</td>
					<td><code>REUSE.toml</code></td>
			</tr>
			<tr>
					<td><a href="https://yamllint.readthedocs.io">yamllint</a>
</td>
					<td>YAML</td>
					<td><code>.yamllint.yaml</code></td>
			</tr>
			<tr>
					<td><a href="https://github.com/rhysd/actionlint">actionlint</a>
</td>
					<td>GitHub Actions workflows</td>
					<td>—</td>
			</tr>
			<tr>
					<td><a href="https://github.com/DavidAnson/markdownlint-cli2">markdownlint</a>
</td>
					<td>Markdown</td>
					<td><code>.markdownlint.yaml</code></td>
			</tr>
			<tr>
					<td><a href="https://github.com/crate-ci/typos">typos</a>
</td>
					<td>Spelling</td>
					<td><code>.typos.toml</code></td>
			</tr>
			<tr>
					<td><a href="https://github.com/aquasecurity/trivy">Trivy</a>
</td>
					<td>Container image CVEs</td>
					<td>—</td>
			</tr>
	</tbody>
</table>
<h3 id="architecture-rules">Architecture rules</h3>
<p><code>arch-go.yml</code> pins two invariants enforced with 100% compliance:</p>
<ul>
<li><code>api/v1</code> depends on neither the operator internals nor <code>sigs.k8s.io/controller-runtime</code>. The CRD types stay importable by external consumers without dragging the manager in. Scheme registration uses apimachinery&rsquo;s <code>runtime.NewSchemeBuilder</code> for exactly this reason.</li>
<li><code>internal/urlguard</code> — the SSRF-defence layer — depends on the standard library only, with no internal and no external imports. This keeps the IP/URL validation logic self-contained and straightforward to fuzz in isolation.</li>
</ul>
<h2 id="the-verifyyml-pr-gate">The verify.yml PR gate</h2>
<p><code>.github/workflows/verify.yml</code> fans out into one job per concern. A failure points straight at the offending gate. CI installs each tool fresh via <code>go run &lt;tool&gt;@latest</code>; the dev shell pre-installs the same tools at the same versions, so local and CI runs agree.</p>
<table>
	<thead>
			<tr>
					<th>Job</th>
					<th>What it runs</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td><code>test</code></td>
					<td><code>go build ./...</code> then <code>go test -v -cover ./...</code></td>
			</tr>
			<tr>
					<td><code>lint-go</code></td>
					<td><code>go vet ./...</code>, <code>staticcheck ./...</code>, <code>gosec ./...</code>, <code>gofumpt -l .</code> (fails on any output)</td>
			</tr>
			<tr>
					<td><code>vulnerabilities</code></td>
					<td><code>govulncheck ./...</code> — reachable-from-code advisories are a hard merge gate</td>
			</tr>
			<tr>
					<td><code>architecture</code></td>
					<td><code>arch-go</code> against <code>arch-go.yml</code></td>
			</tr>
			<tr>
					<td><code>reuse</code></td>
					<td><code>fsfe/reuse-action</code> — every file must carry SPDX headers</td>
			</tr>
			<tr>
					<td><code>yaml</code></td>
					<td><code>yamllint</code> against <code>.yamllint.yaml</code></td>
			</tr>
			<tr>
					<td><code>github-actions</code></td>
					<td><code>actionlint</code></td>
			</tr>
			<tr>
					<td><code>markdown</code></td>
					<td><code>markdownlint-cli2</code> against <code>.markdownlint.yaml</code></td>
			</tr>
			<tr>
					<td><code>typos</code></td>
					<td><code>typos</code> against <code>.typos.toml</code></td>
			</tr>
			<tr>
					<td><code>prose</code></td>
					<td>Vale against the shared metio/vale-config style; error-level findings (naming/branding) fail the gate</td>
			</tr>
			<tr>
					<td><code>container-image</code></td>
					<td><code>docker buildx</code> build (load, no push) followed by Trivy scan; hard-fails on any fixable <code>CRITICAL</code>/<code>HIGH</code></td>
			</tr>
	</tbody>
</table>
<h3 id="all-green-aggregate">All-green aggregate</h3>
<p>The workflow ends with a single <code>all-green</code> job:</p>
<ul>
<li><code>needs</code> every other job</li>
<li>runs <code>if: always()</code></li>
<li>fails unless each dependency <code>result</code> is <code>success</code> or <code>skipped</code></li>
</ul>
<p>That one job is the <strong>only</strong> check marked required in branch protection. Adding a new job to the <code>needs</code> list of <code>all-green</code> covers it automatically; no new required check needs to be registered.</p>
<p>The <code>govulncheck</code> gate is a hard blocker. A reachable-from-code advisory that cannot be fixed by bumping a dependency blocks the PR until resolved. Resolution is usually a <code>toolchain</code> bump in <code>go.mod</code> (for stdlib advisories) or <code>go get -u</code> (for module advisories).</p>
<h2 id="the-release-pipeline">The release pipeline</h2>
<p>Releases are calendar-based and automated. <code>.github/workflows/release.yml</code> runs on a Monday cron (<code>47 7 * * MON</code>) plus manual <code>workflow_dispatch</code>. The version is computed from the run date:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">date +<span class="s1">&#39;%Y.%-m.%-d&#39;</span>
</span></span></code></pre></div><p>For a Monday run on 2026-06-22 that produces <code>2026.6.22</code>.</p>
<p>goreleaser is not used. GPG is not used. The pipeline is hand-rolled across three jobs.</p>
<h3 id="prepare">prepare</h3>
<p>Counts commits since the last release touching the build-relevant paths (<code>go.mod main.go internal api config Dockerfile</code>). Every downstream job gates on that count being non-zero (or there being no prior release at all), so an empty week publishes nothing.</p>
<h3 id="build">build</h3>
<p>A cross-compile matrix over ten platform/arch combinations:</p>
<ul>
<li><code>linux/amd64</code>, <code>linux/arm</code> (v7), <code>linux/arm64</code>, <code>linux/ppc64le</code>, <code>linux/riscv64</code>, <code>linux/s390x</code></li>
<li><code>windows/amd64</code>, <code>windows/arm64</code></li>
<li><code>darwin/amd64</code>, <code>darwin/arm64</code></li>
</ul>
<p>Each platform compiles with:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl"><span class="nv">CGO_ENABLED</span><span class="o">=</span><span class="m">0</span> go build -trimpath <span class="se">\
</span></span></span><span class="line"><span class="cl">  -ldflags<span class="o">=</span><span class="s2">&#34;-s -w -X main.version=&lt;ver&gt; -X main.commit=&lt;sha&gt;&#34;</span> .
</span></span></code></pre></div><p>Archives are <code>tar.gz</code> on linux/darwin and <code>zip</code> on windows (with a <code>.exe</code> binary), each bundling <code>LICENSE</code> and <code>README.md</code>.</p>
<h3 id="container">container</h3>
<p>A single <code>docker buildx</code> multi-arch push to <code>ghcr.io/metio/jaas:{latest,&lt;version&gt;}</code> over the six linux arches. The <code>Dockerfile</code> builder is pinned to <code>$BUILDPLATFORM</code> and cross-compiles via Go&rsquo;s <code>GOARCH</code>, so the multi-arch build needs no QEMU.</p>
<p>SBOM and provenance are attached. The image is signed with cosign keyless immediately after push:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">cosign sign <span class="se">\
</span></span></span><span class="line"><span class="cl">  --yes <span class="se">\
</span></span></span><span class="line"><span class="cl">  --annotations <span class="s2">&#34;repo=metio/jaas&#34;</span> <span class="se">\
</span></span></span><span class="line"><span class="cl">  --annotations <span class="s2">&#34;workflow=Automated Release&#34;</span> <span class="se">\
</span></span></span><span class="line"><span class="cl">  ghcr.io/metio/jaas@&lt;digest&gt;
</span></span></code></pre></div><p>Identity is proven by the workflow&rsquo;s OIDC certificate issued by Fulcio; there is no key to distribute.</p>
<h3 id="github">github</h3>
<p>Gates on both <code>build</code> and <code>container</code> succeeding. Downloads all platform archives, computes a single <code>SHA256SUMS</code> over them, signs it with cosign keyless (Sigstore bundle format), and publishes the GitHub Release with all archives, the checksum file, and the bundle attached.</p>
<p>To verify a release download:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">cosign verify-blob jaas_&lt;version&gt;_SHA256SUMS <span class="se">\
</span></span></span><span class="line"><span class="cl">  --bundle jaas_&lt;version&gt;_SHA256SUMS.bundle <span class="se">\
</span></span></span><span class="line"><span class="cl">  --certificate-identity-regexp <span class="s1">&#39;^https://github.com/metio/jaas/\.github/workflows/release\.yml@refs/&#39;</span> <span class="se">\
</span></span></span><span class="line"><span class="cl">  --certificate-oidc-issuer https://token.actions.githubusercontent.com
</span></span><span class="line"><span class="cl">sha256sum -c jaas_&lt;version&gt;_SHA256SUMS
</span></span></code></pre></div><p>To verify the container image:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">cosign verify ghcr.io/metio/jaas:&lt;version&gt; <span class="se">\
</span></span></span><span class="line"><span class="cl">  --certificate-identity-regexp <span class="s1">&#39;^https://github.com/metio/jaas/\.github/workflows/release\.yml@refs/&#39;</span> <span class="se">\
</span></span></span><span class="line"><span class="cl">  --certificate-oidc-issuer https://token.actions.githubusercontent.com
</span></span></code></pre></div>]]></content><category scheme="https://jaas.projects.metio.wtf/tags/contributing" term="contributing" label="contributing"/><category scheme="https://jaas.projects.metio.wtf/tags/ci" term="ci" label="ci"/><category scheme="https://jaas.projects.metio.wtf/tags/release" term="release" label="release"/></entry><entry><title type="html">Configuration reference</title><link href="https://jaas.projects.metio.wtf/installation/configuration/?utm_source=atom_feed" rel="alternate" type="text/html"/><link href="https://jaas.projects.metio.wtf/installation/helm-values/?utm_source=atom_feed" rel="related" type="text/html" title="Helm chart values"/><link href="https://jaas.projects.metio.wtf/usage/joi-images/?utm_source=atom_feed" rel="related" type="text/html" title="JOI images"/><link href="https://jaas.projects.metio.wtf/installation/kubernetes/?utm_source=atom_feed" rel="related" type="text/html" title="Kubernetes"/><link href="https://jaas.projects.metio.wtf/installation/operations/?utm_source=atom_feed" rel="related" type="text/html" title="Operations"/><link href="https://jaas.projects.metio.wtf/installation/production/?utm_source=atom_feed" rel="related" type="text/html" title="Production"/><id>https://jaas.projects.metio.wtf/installation/configuration/</id><published>0001-01-01T00:00:00+00:00</published><updated>2026-06-16T20:48:39+02:00</updated><content type="html"><![CDATA[<blockquote>Complete reference for every JaaS command-line flag, organized by subsystem, with defaults and chart value equivalents.</blockquote><p>Every JaaS flag is listed here with its default and a one-line description. Run
<code>jaas --help</code> to see the same list at runtime. The tables on this page are
generated from the binary&rsquo;s own flag definitions, so they never drift from the
runtime contract.</p>
<p>The Helm chart exposes most flags under <code>arguments.*</code>; operator-specific flags
are under <code>operator.*</code>. The full set of chart values is in the
<a href="/installation/helm-values/">Helm chart values</a>
 reference.</p>
<h2 id="jsonnet-server">Jsonnet server</h2>
<p>The Jsonnet server evaluates snippets and returns JSON. It binds on
<code>--listen-address:--port</code> by default.</p>
<table>
  <thead>
    <tr><th>Flag</th><th>Type</th><th>Default</th><th>Description</th></tr>
  </thead>
  <tbody>
    <tr>
      <td><code>--jsonnet-endpoint-path</code></td>
      <td><code>string</code></td>
      <td><code>jsonnet</code></td>
      <td>The path to the jsonnet endpoint</td>
    </tr>
    <tr>
      <td><code>--listen-address</code></td>
      <td><code>string</code></td>
      <td><code>127.0.0.1</code></td>
      <td>The listen address to bind to for the Jsonnet server</td>
    </tr>
    <tr>
      <td><code>--port</code></td>
      <td><code>string</code></td>
      <td><code>8080</code></td>
      <td>The port to bind to for the Jsonnet server</td>
    </tr>
    <tr>
      <td><code>--read-timeout</code></td>
      <td><code>duration</code></td>
      <td><code>10s</code></td>
      <td>maximum duration for reading the entire request, including the body in the Jsonnet server</td>
    </tr>
    <tr>
      <td><code>--write-timeout</code></td>
      <td><code>duration</code></td>
      <td><code>10s</code></td>
      <td>The maximum duration before timing out writes of the response in the Jsonnet server</td>
    </tr>
  </tbody>
</table>
<h2 id="management-server">Management server</h2>
<p>The management server exposes the three Kubernetes probe endpoints. It binds on
<code>--management-listen-address:--management-port</code>.</p>
<table>
  <thead>
    <tr><th>Flag</th><th>Type</th><th>Default</th><th>Description</th></tr>
  </thead>
  <tbody>
    <tr>
      <td><code>--management-listen-address</code></td>
      <td><code>string</code></td>
      <td><code>127.0.0.1</code></td>
      <td>The listen address to bind to for the management server</td>
    </tr>
    <tr>
      <td><code>--management-port</code></td>
      <td><code>string</code></td>
      <td><code>8081</code></td>
      <td>The port to bind to for the management server</td>
    </tr>
    <tr>
      <td><code>--management-read-timeout</code></td>
      <td><code>duration</code></td>
      <td><code>10s</code></td>
      <td>maximum duration for reading the entire request, including the body in the management server</td>
    </tr>
    <tr>
      <td><code>--management-write-timeout</code></td>
      <td><code>duration</code></td>
      <td><code>10s</code></td>
      <td>The maximum duration before timing out writes of the response in the management server</td>
    </tr>
  </tbody>
</table>
<p>Endpoints: <code>GET /start</code> (startup probe), <code>GET /ready</code> (readiness probe),
<code>GET /live</code> (liveness probe). Startup and readiness return <code>503</code> with a
<code>{&quot;status&quot;:&quot;…&quot;}</code> JSON body when the server is not yet ready. Liveness is an
unconditional <code>200</code>.</p>
<h2 id="snippets-and-libraries">Snippets and libraries</h2>
<p>Flags for declaring the Jsonnet files the server serves.</p>
<table>
  <thead>
    <tr><th>Flag</th><th>Type</th><th>Default</th><th>Description</th></tr>
  </thead>
  <tbody>
    <tr>
      <td><code>--library-path</code></td>
      <td><code>stringArray</code></td>
      <td><code>[]</code></td>
      <td>The path of a directory containing jsonnet libraries (can be specified multiple times). Rightmost matching library will be used.</td>
    </tr>
    <tr>
      <td><code>--snippet</code></td>
      <td><code>stringArray</code></td>
      <td><code>[]</code></td>
      <td>The path of a jsonnet file or directory containing snippets (can be specified multiple times). Snippets will be loaded from the given path, where the file name is the snippet name.</td>
    </tr>
    <tr>
      <td><code>--snippet-directory</code></td>
      <td><code>stringArray</code></td>
      <td><code>[]</code></td>
      <td>The path of a directory containing snippets as subdirectories (can be specified multiple times). Snippets will be loaded from subdirectories of the given path, where the directory name is the snippet name.</td>
    </tr>
  </tbody>
</table>
<p>Snippet name resolution uses Go&rsquo;s <code>os.OpenRoot</code>, which rejects <code>..</code> traversal
and symlinks that escape the configured directory. This is security-critical;
see <a href="/usage/evaluation-and-security/">Evaluation and security</a>
.</p>
<h2 id="external-variables">External variables</h2>
<table>
  <thead>
    <tr><th>Flag</th><th>Type</th><th>Default</th><th>Description</th></tr>
  </thead>
  <tbody>
    <tr>
      <td><code>--ext-var</code></td>
      <td><code>stringArray</code></td>
      <td><code>[]</code></td>
      <td>External variable as KEY=VALUE for std.extVar lookups (can be specified multiple times). Takes precedence over JAAS_EXT_VAR_* env vars on conflict.</td>
    </tr>
  </tbody>
</table>
<p><strong>Environment variable alternative:</strong> set <code>JAAS_EXT_VAR_&lt;NAME&gt;=&lt;VALUE&gt;</code> to
expose <code>&lt;NAME&gt;</code> as an external variable. The <code>--ext-var</code> flag overrides the env
mechanism on key conflict. See
<a href="/usage/external-variables-and-tlas/">External variables and TLAs</a>
 for usage
examples.</p>
<h2 id="evaluation-limits">Evaluation limits</h2>
<table>
  <thead>
    <tr><th>Flag</th><th>Type</th><th>Default</th><th>Description</th></tr>
  </thead>
  <tbody>
    <tr>
      <td><code>--evaluation-timeout</code></td>
      <td><code>duration</code></td>
      <td><code>5s</code></td>
      <td>Maximum duration a single Jsonnet evaluation is allowed to take. Set to 0 to disable.</td>
    </tr>
    <tr>
      <td><code>--max-concurrent-evals</code></td>
      <td><code>int</code></td>
      <td><code>max(GOMAXPROCS×4, 16)</code></td>
      <td>Maximum number of in-flight Jsonnet evaluations. Excess requests return 503 (HTTP) or RequeueAfter (operator). Set to 0 to disable. Defaults to max(GOMAXPROCS*4, 16).</td>
    </tr>
    <tr>
      <td><code>--max-stack</code></td>
      <td><code>int</code></td>
      <td><code>500</code></td>
      <td>Maximum Jsonnet call-stack depth. Set to 0 to use go-jsonnet&#39;s default.</td>
    </tr>
  </tbody>
</table>
<p><code>--evaluation-timeout</code> fires the HTTP response but does not terminate the
underlying go-jsonnet call — the evaluation continues consuming CPU until it
finishes naturally. Size container resources accordingly and use
<code>--max-concurrent-evals</code> to bound worst-case goroutine pile-up. See
<a href="/usage/evaluation-and-security/">Evaluation and security</a>
 for the full
discussion.</p>
<h2 id="lifecycle">Lifecycle</h2>
<table>
  <thead>
    <tr><th>Flag</th><th>Type</th><th>Default</th><th>Description</th></tr>
  </thead>
  <tbody>
    <tr>
      <td><code>--shutdown-delay</code></td>
      <td><code>duration</code></td>
      <td><code>5s</code></td>
      <td>Time to wait after readiness flips to false before initiating graceful shutdown; gives Kubernetes time to propagate the not-ready status to endpoint controllers. Set to 0 to disable.</td>
    </tr>
  </tbody>
</table>
<h2 id="operator-flux-integration">Operator (Flux integration)</h2>
<p>The following flags are only active when <code>--enable-flux-integration</code> is set.</p>
<table>
  <thead>
    <tr><th>Flag</th><th>Type</th><th>Default</th><th>Description</th></tr>
  </thead>
  <tbody>
    <tr>
      <td><code>--artifact-gc-grace</code></td>
      <td><code>duration</code></td>
      <td><code>5m0s</code></td>
      <td>Minimum time a superseded artifact revision is retained after being evicted from the keep-set. Closes the pin→fetch race in which a Flux consumer reads status.artifact a moment before the operator garbage-collects the superseded revision. Zero disables and restores eager pruning. The deletion path (snippet teardown) is unaffected.</td>
    </tr>
    <tr>
      <td><code>--default-service-account</code></td>
      <td><code>string</code></td>
      <td><em>(empty)</em></td>
      <td>ServiceAccount the operator impersonates when a JsonnetSnippet has no spec.serviceAccountName. Empty rejects such snippets at reconcile time.</td>
    </tr>
    <tr>
      <td><code>--enable-flux-integration</code></td>
      <td><code>bool</code></td>
      <td><code>false</code></td>
      <td>Boot the Kubernetes operator that watches JsonnetSnippet / JsonnetLibrary CRs and publishes evaluated results as Flux ExternalArtifacts.</td>
    </tr>
    <tr>
      <td><code>--kubeconfig</code></td>
      <td><code>string</code></td>
      <td><em>(empty)</em></td>
      <td>Path to a kubeconfig file for the operator. Empty falls back to KUBECONFIG env, then to in-cluster service-account credentials.</td>
    </tr>
    <tr>
      <td><code>--label-selector</code></td>
      <td><code>string</code></td>
      <td><em>(empty)</em></td>
      <td>Narrow the operator&#39;s watch to CRs matching this label selector. Empty selects every CR in the watched scope.</td>
    </tr>
    <tr>
      <td><code>--max-artifact-bytes</code></td>
      <td><code>int64</code></td>
      <td><code>0</code></td>
      <td>Cap the rendered artifact size in bytes. Snippets whose rendered output exceeds this fail with ReasonArtifactTooLarge. Zero disables.</td>
    </tr>
    <tr>
      <td><code>--max-withdraw-wait</code></td>
      <td><code>duration</code></td>
      <td><code>1h0m0s</code></td>
      <td>Bound the time a deleted JsonnetSnippet&#39;s finalizer can hold while Publisher.Withdraw keeps failing. Past this, the operator emits a Warning WithdrawForced event, drops the finalizer, and lets the snippet be garbage-collected — possibly leaving an orphan tarball in storage. Required so a permanently-broken backend doesn&#39;t block namespace teardown.</td>
    </tr>
    <tr>
      <td><code>--no-cross-namespace-refs</code></td>
      <td><code>bool</code></td>
      <td><code>true</code></td>
      <td>When true (default), reject JsonnetSnippet / library CRs that reference a SourceRef in a different namespace.</td>
    </tr>
    <tr>
      <td><code>--rerender-burst</code></td>
      <td><code>int</code></td>
      <td><code>120</code></td>
      <td>Per-snippet token-bucket depth for re-render rate limiting.</td>
    </tr>
    <tr>
      <td><code>--rerender-rate</code></td>
      <td><code>string</code></td>
      <td><code>60/min</code></td>
      <td>Per-snippet steady-state re-render budget, as N/period (sec|min|hour). Token-bucket combined with --rerender-burst.</td>
    </tr>
    <tr>
      <td><code>--watch-namespaces</code></td>
      <td><code>string</code></td>
      <td><em>(empty)</em></td>
      <td>Comma-separated list of namespaces this operator watches. Empty (the default) means cluster-wide. When set, the manager&#39;s cache only observes CRs in these namespaces — multi-tenant operator-instances pattern. Falls back to JAAS_WATCH_NAMESPACES env var when the flag is empty.</td>
    </tr>
  </tbody>
</table>
<p><strong>Environment variable:</strong> <code>JAAS_WATCH_NAMESPACES</code> — comma-separated namespace
list. Superseded by <code>--watch-namespaces</code> when both are set.</p>
<h2 id="storage-server-local-and-s3">Storage server (local and S3)</h2>
<p>The storage server is the HTTP file server that downstream Flux consumers fetch
artifacts from. It is started only when <code>--enable-flux-integration</code> is set.</p>
<table>
  <thead>
    <tr><th>Flag</th><th>Type</th><th>Default</th><th>Description</th></tr>
  </thead>
  <tbody>
    <tr>
      <td><code>--storage-backend</code></td>
      <td><code>string</code></td>
      <td><code>local</code></td>
      <td>Artifact backend the operator publishes ExternalArtifact tarballs through. local (default; emptyDir/PVC) or s3 (any S3-compatible object store; pairs with leader election for HA across replicas).</td>
    </tr>
    <tr>
      <td><code>--storage-base-url</code></td>
      <td><code>string</code></td>
      <td><em>(empty)</em></td>
      <td>Public URL prefix the operator&#39;s storage HTTP server serves tarballs at. Required when --enable-flux-integration is set.</td>
    </tr>
    <tr>
      <td><code>--storage-listen-address</code></td>
      <td><code>string</code></td>
      <td><code>0.0.0.0</code></td>
      <td>The listen address to bind to for the storage HTTP server</td>
    </tr>
    <tr>
      <td><code>--storage-path</code></td>
      <td><code>string</code></td>
      <td><em>(empty)</em></td>
      <td>Directory the operator writes ExternalArtifact tarballs to. Required when --enable-flux-integration is set.</td>
    </tr>
    <tr>
      <td><code>--storage-port</code></td>
      <td><code>string</code></td>
      <td><code>8082</code></td>
      <td>The port to bind to for the storage HTTP server</td>
    </tr>
    <tr>
      <td><code>--storage-read-timeout</code></td>
      <td><code>duration</code></td>
      <td><code>30s</code></td>
      <td>Maximum duration for reading the entire request on the storage server.</td>
    </tr>
    <tr>
      <td><code>--storage-sweep-interval</code></td>
      <td><code>duration</code></td>
      <td><code>10m0s</code></td>
      <td>How often the operator sweeps orphaned &lt;rev&gt;.tar.gz.tmp residue left by Puts whose process died mid-rename. Zero disables.</td>
    </tr>
    <tr>
      <td><code>--storage-sweep-max-tmp-age</code></td>
      <td><code>duration</code></td>
      <td><code>30m0s</code></td>
      <td>Minimum age before an orphaned .tmp file is eligible for sweep. Wider than the longest plausible in-flight Put to avoid racing live writers.</td>
    </tr>
    <tr>
      <td><code>--storage-write-timeout</code></td>
      <td><code>duration</code></td>
      <td><code>5m0s</code></td>
      <td>Maximum duration before timing out writes of the response on the storage server. Tarballs can be MBs, so this is generous by default.</td>
    </tr>
  </tbody>
</table>
<h3 id="s3-flags">S3 flags</h3>
<p>Active only when <code>--storage-backend=s3</code>.</p>
<table>
  <thead>
    <tr><th>Flag</th><th>Type</th><th>Default</th><th>Description</th></tr>
  </thead>
  <tbody>
    <tr>
      <td><code>--s3-access-key</code></td>
      <td><code>string</code></td>
      <td><em>(empty)</em></td>
      <td>Static AWS_ACCESS_KEY_ID. Empty triggers the IAM/IRSA discovery chain (AWS_*, web-identity, EC2 metadata).</td>
    </tr>
    <tr>
      <td><code>--s3-anonymous</code></td>
      <td><code>bool</code></td>
      <td><code>false</code></td>
      <td>Skip request signing entirely. Only useful against a public bucket — test/dev only.</td>
    </tr>
    <tr>
      <td><code>--s3-bucket</code></td>
      <td><code>string</code></td>
      <td><em>(empty)</em></td>
      <td>S3 bucket the artifacts live in. Must already exist. Required when --storage-backend=s3.</td>
    </tr>
    <tr>
      <td><code>--s3-endpoint</code></td>
      <td><code>string</code></td>
      <td><em>(empty)</em></td>
      <td>S3 service host:port (e.g. s3.amazonaws.com or minio.minio.svc:9000). Required when --storage-backend=s3.</td>
    </tr>
    <tr>
      <td><code>--s3-prefix</code></td>
      <td><code>string</code></td>
      <td><em>(empty)</em></td>
      <td>Optional object-key prefix prepended under the bucket, so jaas can coexist with other tenants in one bucket.</td>
    </tr>
    <tr>
      <td><code>--s3-region</code></td>
      <td><code>string</code></td>
      <td><em>(empty)</em></td>
      <td>S3 region the bucket lives in. Required for AWS multi-region setups; ignored by most S3-compatible servers.</td>
    </tr>
    <tr>
      <td><code>--s3-secret-key</code></td>
      <td><code>string</code></td>
      <td><em>(empty)</em></td>
      <td>Static AWS_SECRET_ACCESS_KEY. Pairs with --s3-access-key.</td>
    </tr>
    <tr>
      <td><code>--s3-session-token</code></td>
      <td><code>string</code></td>
      <td><em>(empty)</em></td>
      <td>Optional AWS_SESSION_TOKEN, paired with --s3-access-key/--s3-secret-key for temporary credentials.</td>
    </tr>
    <tr>
      <td><code>--s3-use-ssl</code></td>
      <td><code>bool</code></td>
      <td><code>true</code></td>
      <td>Talk HTTPS to the S3 endpoint. Set to false only for local MinIO over HTTP.</td>
    </tr>
  </tbody>
</table>
<h2 id="webhook-tls-provisioning">Webhook (TLS provisioning)</h2>
<p>Active only when <code>--enable-webhook</code> is set (which also requires
<code>--enable-flux-integration</code>).</p>
<table>
  <thead>
    <tr><th>Flag</th><th>Type</th><th>Default</th><th>Description</th></tr>
  </thead>
  <tbody>
    <tr>
      <td><code>--enable-webhook</code></td>
      <td><code>bool</code></td>
      <td><code>false</code></td>
      <td>Boot the validating admission webhook for JsonnetSnippet. Requires --enable-flux-integration and a TLS cert/key in --webhook-cert-dir.</td>
    </tr>
    <tr>
      <td><code>--webhook-cert-dir</code></td>
      <td><code>string</code></td>
      <td><code>/tmp/k8s-webhook-server/serving-certs</code></td>
      <td>Directory holding the TLS cert (tls.crt) and key (tls.key) the webhook server presents.</td>
    </tr>
    <tr>
      <td><code>--webhook-cert-mode</code></td>
      <td><code>string</code></td>
      <td><code>cert-manager</code></td>
      <td>How the webhook&#39;s TLS material is provisioned: cert-manager (chart renders a Certificate; cert injected via Secret mount), or self-signed (operator generates a CA &#43; serving cert in-pod and patches the ValidatingWebhookConfiguration&#39;s caBundle).</td>
    </tr>
    <tr>
      <td><code>--webhook-cert-validity</code></td>
      <td><code>duration</code></td>
      <td><code>8760h0m0s</code></td>
      <td>Validity of the self-signed serving cert. Operators that want short-lived rotation should use cert-manager instead.</td>
    </tr>
    <tr>
      <td><code>--webhook-port</code></td>
      <td><code>int</code></td>
      <td><code>9443</code></td>
      <td>Port the validating webhook server binds to.</td>
    </tr>
    <tr>
      <td><code>--webhook-service-name</code></td>
      <td><code>string</code></td>
      <td><code>jaas-webhook</code></td>
      <td>Service name the webhook is reachable through. Used to build cert SANs when --webhook-cert-mode=self-signed.</td>
    </tr>
    <tr>
      <td><code>--webhook-service-namespace</code></td>
      <td><code>string</code></td>
      <td><em>(empty)</em></td>
      <td>Namespace the webhook Service lives in. Empty falls back to --leader-election-namespace, then to in-cluster downward API.</td>
    </tr>
    <tr>
      <td><code>--webhook-validating-config-name</code></td>
      <td><code>string</code></td>
      <td><em>(empty)</em></td>
      <td>Name of the ValidatingWebhookConfiguration whose caBundle this operator patches. Required when --webhook-cert-mode=self-signed.</td>
    </tr>
  </tbody>
</table>
<p>See <a href="/usage/admission-webhook/">Admission webhook</a>
 for the full <code>failurePolicy</code>
trade-off and cert rotation details.</p>
<h2 id="leader-election">Leader election</h2>
<table>
  <thead>
    <tr><th>Flag</th><th>Type</th><th>Default</th><th>Description</th></tr>
  </thead>
  <tbody>
    <tr>
      <td><code>--leader-election</code></td>
      <td><code>bool</code></td>
      <td><code>true</code></td>
      <td>Enable controller-runtime leader election so only one operator replica reconciles at a time. Honored only when --enable-flux-integration is set.</td>
    </tr>
    <tr>
      <td><code>--leader-election-id</code></td>
      <td><code>string</code></td>
      <td><code>jaas-operator</code></td>
      <td>Lease object name used for leader election. Must be unique across JaaS installations sharing a namespace.</td>
    </tr>
    <tr>
      <td><code>--leader-election-namespace</code></td>
      <td><code>string</code></td>
      <td><em>(empty)</em></td>
      <td>Namespace holding the leader-election Lease. Empty defaults to the operator pod&#39;s namespace.</td>
    </tr>
  </tbody>
</table>
<h2 id="observability">Observability</h2>
<h3 id="metrics">Metrics</h3>
<table>
  <thead>
    <tr><th>Flag</th><th>Type</th><th>Default</th><th>Description</th></tr>
  </thead>
  <tbody>
    <tr>
      <td><code>--metrics-bind-address</code></td>
      <td><code>string</code></td>
      <td><code>:8083</code></td>
      <td>Bind address for the controller-runtime Prometheus metrics endpoint. Use &#34;0&#34; to disable. The default avoids the conflict between controller-runtime&#39;s built-in :8080 and the jsonnet HTTP server.</td>
    </tr>
  </tbody>
</table>
<h3 id="tracing">Tracing</h3>
<table>
  <thead>
    <tr><th>Flag</th><th>Type</th><th>Default</th><th>Description</th></tr>
  </thead>
  <tbody>
    <tr>
      <td><code>--tracing-endpoint</code></td>
      <td><code>string</code></td>
      <td><em>(empty)</em></td>
      <td>OTLP gRPC collector host:port (e.g. otel-collector.observability.svc:4317). Empty disables tracing entirely.</td>
    </tr>
    <tr>
      <td><code>--tracing-insecure</code></td>
      <td><code>bool</code></td>
      <td><code>false</code></td>
      <td>Skip TLS when dialing the OTLP collector. Use only for in-cluster collectors that don&#39;t terminate TLS themselves.</td>
    </tr>
    <tr>
      <td><code>--tracing-sample-ratio</code></td>
      <td><code>float64</code></td>
      <td><code>1</code></td>
      <td>TraceID-ratio sampling (0.0..1.0). 1.0 samples every trace.</td>
    </tr>
  </tbody>
</table>
<h2 id="logging-and-lifecycle">Logging and lifecycle</h2>
<table>
  <thead>
    <tr><th>Flag</th><th>Type</th><th>Default</th><th>Description</th></tr>
  </thead>
  <tbody>
    <tr>
      <td><code>--log-format</code></td>
      <td><code>string</code></td>
      <td><code>json</code></td>
      <td>The log output format to use (json, text)</td>
    </tr>
    <tr>
      <td><code>--log-level</code></td>
      <td><code>string</code></td>
      <td><code>info</code></td>
      <td>The log level to use (debug, info, warn, error)</td>
    </tr>
    <tr>
      <td><code>--version</code></td>
      <td><code>bool</code></td>
      <td><code>false</code></td>
      <td>Print version and exit</td>
    </tr>
  </tbody>
</table>
]]></content><category scheme="https://jaas.projects.metio.wtf/tags/installation" term="installation" label="installation"/><category scheme="https://jaas.projects.metio.wtf/tags/configuration" term="configuration" label="configuration"/><category scheme="https://jaas.projects.metio.wtf/tags/flags" term="flags" label="flags"/><category scheme="https://jaas.projects.metio.wtf/tags/reference" term="reference" label="reference"/></entry><entry><title type="html">CRD watch engagement failing</title><link href="https://jaas.projects.metio.wtf/runbooks/crd-watch-engagement/?utm_source=atom_feed" rel="alternate" type="text/html"/><link href="https://jaas.projects.metio.wtf/runbooks/invalidspec/?utm_source=atom_feed" rel="related" type="text/html" title="InvalidSpec"/><link href="https://jaas.projects.metio.wtf/runbooks/operator-pod-down/?utm_source=atom_feed" rel="related" type="text/html" title="Operator pod not ready"/><link href="https://jaas.projects.metio.wtf/runbooks/pending/?utm_source=atom_feed" rel="related" type="text/html" title="Pending"/><link href="https://jaas.projects.metio.wtf/runbooks/suspended/?utm_source=atom_feed" rel="related" type="text/html" title="Suspended"/><link href="https://jaas.projects.metio.wtf/runbooks/synced/?utm_source=atom_feed" rel="related" type="text/html" title="Synced"/><id>https://jaas.projects.metio.wtf/runbooks/crd-watch-engagement/</id><published>0001-01-01T00:00:00+00:00</published><updated>2026-06-17T10:56:50+02:00</updated><content type="html"><![CDATA[<blockquote>A runtime watch on a Flux source CRD failed to engage, so snippets referencing that kind no longer re-render on upstream changes</blockquote><p>Fires when <code>jaas_crd_watch_engagement_failures_total{gvk=...}</code> has increased above the per-hour threshold for the alert window. JaaS lazy-watches Flux source CRDs: at boot, only the CRDs already installed get a watch; when a previously-missing CRD becomes <code>Established=True</code> (operator installed source-controller post hoc, say), the <code>crdWatcher</code> engages a runtime watch on it via <code>Controller.Watch</code>. <strong>When that engagement fails, the apiextensions informer fires no further events on a stable CRD</strong> — meaning the watch stays un-engaged forever until either the CRD object&rsquo;s metadata/status is changed by something else, or the operator restarts.</p>
<p>The visible symptom is that snippets with <code>spec.sourceRef.Kind=&lt;the affected kind&gt;</code> stop re-rendering on upstream source updates. There is no per-snippet status signal — they sit at their last-rendered revision, drifting from upstream.</p>
<h2 id="symptom">Symptom</h2>
<ul>
<li><code>JaaSCRDWatchEngagementFailing</code> alert is firing with <code>gvk</code> labelling the affected kind.</li>
<li><code>kubectl describe jsonnetsnippet</code> on snippets referencing that GVK shows a Ready condition that hasn&rsquo;t moved in hours/days.</li>
<li>Upstream Flux source CRs (GitRepository, OCIRepository, Bucket, ExternalArtifact) show recent <code>status.artifact.revision</code> changes that the jaas snippets aren&rsquo;t picking up.</li>
</ul>
<h2 id="diagnosis">Diagnosis</h2>
<h3 id="step-1--confirm-the-crd-is-actually-installed-and-established">Step 1 — confirm the CRD is actually installed and Established</h3>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">kubectl get crd &lt;plural&gt;.source.toolkit.fluxcd.io <span class="se">\
</span></span></span><span class="line"><span class="cl">  --output <span class="nv">jsonpath</span><span class="o">=</span><span class="s1">&#39;{.status.conditions[?(@.type==&#34;Established&#34;)].status}{&#34;\n&#34;}&#39;</span>
</span></span></code></pre></div><p>Expect <code>True</code>. If the CRD is not installed or not yet Established, the watcher is correct to skip; install / wait.</p>
<h3 id="step-2--check-the-operators-rbac-on-the-source-kind">Step 2 — check the operator&rsquo;s RBAC on the source kind</h3>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">kubectl auth can-i list &lt;plural&gt;.source.toolkit.fluxcd.io <span class="se">\
</span></span></span><span class="line"><span class="cl">  --as<span class="o">=</span>system:serviceaccount:&lt;ns&gt;:&lt;operator-sa&gt;
</span></span><span class="line"><span class="cl">kubectl auth can-i watch &lt;plural&gt;.source.toolkit.fluxcd.io <span class="se">\
</span></span></span><span class="line"><span class="cl">  --as<span class="o">=</span>system:serviceaccount:&lt;ns&gt;:&lt;operator-sa&gt;
</span></span></code></pre></div><p>If either is &ldquo;no&rdquo;, the chart&rsquo;s <code>operator-tenants</code> ClusterRole (or per-namespace RoleBinding when <code>watchNamespaces</code> is set) is missing the <code>get/list/watch</code> verbs on this kind. Update the chart&rsquo;s <code>FluxSourceKinds</code> mapping or add the verb manually.</p>
<h3 id="step-3--check-controller-runtime-cache-state">Step 3 — check controller-runtime cache state</h3>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">kubectl --namespace &lt;ns&gt; logs &lt;operator-pod&gt; <span class="p">|</span> grep -E <span class="s1">&#39;engage|Failed to watch|cache&#39;</span> <span class="p">|</span> tail -20
</span></span></code></pre></div><p>Look for <code>cache reconnect</code>, <code>informer failed</code>, or <code>Watch failed: forbidden</code>. A transient cache reconnect during a heavy load period can trip engagement once; the DD7 bounded-retry mechanism re-engages automatically. Sustained failures point at RBAC or a misconfigured <code>MetricsBindAddress</code>.</p>
<h2 id="remediation">Remediation</h2>
<ol>
<li>
<p><strong>Fix the verb / kind / RBAC</strong> issue identified above.</p>
</li>
<li>
<p><strong>Roll the operator pod</strong> to force a fresh <code>SetupWithManager</code> pass, which re-detects every Flux CRD and re-engages watches that succeed on first try:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">kubectl --namespace &lt;ns&gt; rollout restart deployment &lt;operator-deployment&gt;
</span></span></code></pre></div></li>
<li>
<p><strong>Verify</strong> the counter stops increasing and the alert clears.</p>
</li>
</ol>
<h2 id="when-the-alert-is-noisy">When the alert is noisy</h2>
<p>If <code>jaas_crd_watch_engagement_failures_total</code> ticks once at boot but never again, that&rsquo;s the expected DD7 bounded-retry behavior: the first attempt failed (transient race during cache start), the retry succeeded. Raise <code>crdWatchEngagementFailuresPerHour</code> if the boot-time blip is noisy enough to page.</p>
]]></content><category scheme="https://jaas.projects.metio.wtf/tags/runbooks" term="runbooks" label="runbooks"/><category scheme="https://jaas.projects.metio.wtf/tags/troubleshooting" term="troubleshooting" label="troubleshooting"/><category scheme="https://jaas.projects.metio.wtf/tags/lifecycle" term="lifecycle" label="lifecycle"/></entry><entry><title type="html">Creating source artifacts</title><link href="https://jaas.projects.metio.wtf/usage/creating-sources/?utm_source=atom_feed" rel="alternate" type="text/html"/><link href="https://jaas.projects.metio.wtf/api/externalartifact/?utm_source=atom_feed" rel="related" type="text/html" title="ExternalArtifact output contract"/><link href="https://jaas.projects.metio.wtf/api/jsonnetsnippet/?utm_source=atom_feed" rel="related" type="text/html" title="JsonnetSnippet"/><link href="https://jaas.projects.metio.wtf/usage/operator-mode/?utm_source=atom_feed" rel="related" type="text/html" title="Operator mode"/><link href="https://jaas.projects.metio.wtf/usage/snippet-sources/?utm_source=atom_feed" rel="related" type="text/html" title="Snippet sources"/><link href="https://jaas.projects.metio.wtf/usage/admission-webhook/?utm_source=atom_feed" rel="related" type="text/html" title="Admission webhook"/><id>https://jaas.projects.metio.wtf/usage/creating-sources/</id><published>0001-01-01T00:00:00+00:00</published><updated>2026-06-16T20:48:39+02:00</updated><content type="html"><![CDATA[<blockquote>Step-by-step recipes to prepare GitRepository, OCIRepository, and Bucket sources for a JsonnetSnippet — including the single-layer rule for OCI.</blockquote><p>A <code>JsonnetSnippet</code>&rsquo;s <code>spec.sourceRef</code> consumes a Flux source. The operator reads
the referenced source CR&rsquo;s <code>status.artifact.url</code>, downloads the tarball Flux&rsquo;s
source-controller serves there, verifies its <code>status.artifact.digest</code>, and
extracts it into the snippet&rsquo;s file tree. Every supported kind —
<code>GitRepository</code>, <code>OCIRepository</code>, and <code>Bucket</code> — reaches the operator through
that same <code>status.artifact</code> contract, so the operator never talks to a git
remote, an OCI registry, or an object store directly. Flux owns that fetch; the
operator consumes the artifact Flux already produced.</p>
<p>The recipes below show how to produce each source kind so a snippet can reference
it. <a href="/usage/snippet-sources/">Snippet sources</a>
 covers wiring the finished source
into a <code>JsonnetSnippet</code>. For the source CRDs themselves and their full field
reference, see the <a href="https://fluxcd.io/">Flux documentation</a>
 — only what a JaaS
source needs is covered here.</p>
<p>A <code>JsonnetSnippet</code> references the source you create with a <code>spec.sourceRef</code>:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nt">apiVersion</span><span class="p">:</span><span class="w"> </span><span class="l">jaas.metio.wtf/v1</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">kind</span><span class="p">:</span><span class="w"> </span><span class="l">JsonnetSnippet</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">metadata</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">dashboards</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">namespace</span><span class="p">:</span><span class="w"> </span><span class="l">default</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">spec</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">serviceAccountName</span><span class="p">:</span><span class="w"> </span><span class="l">dashboards-tenant</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">entryFile</span><span class="p">:</span><span class="w"> </span><span class="l">dashboards/api-latency.jsonnet</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">sourceRef</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">kind</span><span class="p">:</span><span class="w"> </span><span class="l">GitRepository     </span><span class="w"> </span><span class="c"># or OCIRepository, or Bucket</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">dashboards-source</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">path: dashboards/        # optional</span><span class="p">:</span><span class="w"> </span><span class="l">narrow extraction to a subtree</span><span class="w">
</span></span></span></code></pre></div><p>The tenant ServiceAccount needs <code>get</code> on the referenced source kind. See
<a href="/usage/tenancy-and-rbac/">Tenancy and RBAC</a>
 for the exact verbs.</p>
<h2 id="gitrepository">GitRepository</h2>
<p>A <code>GitRepository</code> source tracks a branch, tag, or commit of a git repository.
source-controller clones the ref and packs the tree into the tarball the
operator fetches. There is no packaging or layer constraint — the operator
extracts whatever files the commit contains.</p>
<ol>
<li>
<p>Lay out your Jsonnet files in a directory. File names and the directory
structure carry over verbatim into the snippet&rsquo;s file tree, so place the
entry file where <code>spec.entryFile</code> expects it:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">dashboards/
</span></span><span class="line"><span class="cl">├── api-latency.jsonnet
</span></span><span class="line"><span class="cl">├── error-budget.jsonnet
</span></span><span class="line"><span class="cl">└── lib/
</span></span><span class="line"><span class="cl">    └── panels.libsonnet
</span></span></code></pre></div></li>
<li>
<p>Commit the files and push them to a git repository:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">git add dashboards/
</span></span><span class="line"><span class="cl">git commit -m <span class="s2">&#34;Add Grafana dashboards&#34;</span>
</span></span><span class="line"><span class="cl">git push origin main
</span></span></code></pre></div></li>
<li>
<p>Create the <code>GitRepository</code> source. With the Flux CLI:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">flux create <span class="nb">source</span> git dashboards-source <span class="se">\
</span></span></span><span class="line"><span class="cl">  --url<span class="o">=</span>https://github.com/example-org/grafana-dashboards <span class="se">\
</span></span></span><span class="line"><span class="cl">  --branch<span class="o">=</span>main <span class="se">\
</span></span></span><span class="line"><span class="cl">  --interval<span class="o">=</span>5m <span class="se">\
</span></span></span><span class="line"><span class="cl">  --namespace<span class="o">=</span>default <span class="se">\
</span></span></span><span class="line"><span class="cl">  --export
</span></span></code></pre></div><p>The equivalent CR YAML, which is authoritative:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nt">apiVersion</span><span class="p">:</span><span class="w"> </span><span class="l">source.toolkit.fluxcd.io/v1</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">kind</span><span class="p">:</span><span class="w"> </span><span class="l">GitRepository</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">metadata</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">dashboards-source</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">namespace</span><span class="p">:</span><span class="w"> </span><span class="l">default</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">spec</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">interval</span><span class="p">:</span><span class="w"> </span><span class="l">5m</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">url</span><span class="p">:</span><span class="w"> </span><span class="l">https://github.com/example-org/grafana-dashboards</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">ref</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">branch</span><span class="p">:</span><span class="w"> </span><span class="l">main</span><span class="w">
</span></span></span></code></pre></div></li>
<li>
<p>Point a snippet&rsquo;s <code>spec.sourceRef</code> at the source. Set <code>kind: GitRepository</code>,
<code>name: dashboards-source</code>, and optionally <code>path:</code> to extract only a subtree:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nt">apiVersion</span><span class="p">:</span><span class="w"> </span><span class="l">jaas.metio.wtf/v1</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">kind</span><span class="p">:</span><span class="w"> </span><span class="l">JsonnetSnippet</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">metadata</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">api-latency-dashboard</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">namespace</span><span class="p">:</span><span class="w"> </span><span class="l">default</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">spec</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">serviceAccountName</span><span class="p">:</span><span class="w"> </span><span class="l">dashboards-tenant</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">entryFile</span><span class="p">:</span><span class="w"> </span><span class="l">dashboards/api-latency.jsonnet</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">sourceRef</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">kind</span><span class="p">:</span><span class="w"> </span><span class="l">GitRepository</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">dashboards-source</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">path</span><span class="p">:</span><span class="w"> </span><span class="l">dashboards/</span><span class="w">
</span></span></span></code></pre></div></li>
</ol>
<p>When a new commit lands on the tracked branch, source-controller republishes the
artifact and the operator&rsquo;s watch re-renders the snippet.</p>
<h2 id="ocirepository">OCIRepository</h2>
<p>An <code>OCIRepository</code> source pulls an OCI artifact from a registry. source-controller
unpacks the artifact&rsquo;s single gzipped-tar layer into the tarball the operator
fetches. Producing the artifact with <code>flux push artifact</code> packs a directory into
exactly that shape.</p>
<ol>
<li>
<p>Lay out your Jsonnet files in a directory, the same as for a git source:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">./
</span></span><span class="line"><span class="cl">├── main.jsonnet
</span></span><span class="line"><span class="cl">└── lib/
</span></span><span class="line"><span class="cl">    └── panels.libsonnet
</span></span></code></pre></div></li>
<li>
<p>Push the directory as an OCI artifact with the Flux CLI. <code>flux push artifact</code>
packs the directory into one gzipped-tar layer and pushes it to the registry:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">flux push artifact oci://ghcr.io/example-org/dashboards:v1 <span class="se">\
</span></span></span><span class="line"><span class="cl">  --path<span class="o">=</span>. <span class="se">\
</span></span></span><span class="line"><span class="cl">  --source<span class="o">=</span><span class="s2">&#34;</span><span class="k">$(</span>git config --get remote.origin.url<span class="k">)</span><span class="s2">&#34;</span> <span class="se">\
</span></span></span><span class="line"><span class="cl">  --revision<span class="o">=</span><span class="s2">&#34;</span><span class="k">$(</span>git rev-parse HEAD<span class="k">)</span><span class="s2">&#34;</span>
</span></span></code></pre></div><p><code>--source</code> and <code>--revision</code> stamp provenance metadata onto the artifact;
set them to a URL and a version identifier of your choosing.</p>
</li>
<li>
<p>Create the <code>OCIRepository</code> source. With the Flux CLI:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">flux create <span class="nb">source</span> oci dashboards-source <span class="se">\
</span></span></span><span class="line"><span class="cl">  --url<span class="o">=</span>oci://ghcr.io/example-org/dashboards <span class="se">\
</span></span></span><span class="line"><span class="cl">  --tag<span class="o">=</span>v1 <span class="se">\
</span></span></span><span class="line"><span class="cl">  --interval<span class="o">=</span>5m <span class="se">\
</span></span></span><span class="line"><span class="cl">  --namespace<span class="o">=</span>default <span class="se">\
</span></span></span><span class="line"><span class="cl">  --export
</span></span></code></pre></div><p>The equivalent CR YAML, which is authoritative:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nt">apiVersion</span><span class="p">:</span><span class="w"> </span><span class="l">source.toolkit.fluxcd.io/v1</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">kind</span><span class="p">:</span><span class="w"> </span><span class="l">OCIRepository</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">metadata</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">dashboards-source</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">namespace</span><span class="p">:</span><span class="w"> </span><span class="l">default</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">spec</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">interval</span><span class="p">:</span><span class="w"> </span><span class="l">5m</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">url</span><span class="p">:</span><span class="w"> </span><span class="l">oci://ghcr.io/example-org/dashboards</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">ref</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">tag</span><span class="p">:</span><span class="w"> </span><span class="l">v1</span><span class="w">
</span></span></span></code></pre></div></li>
<li>
<p>Point a snippet&rsquo;s <code>spec.sourceRef</code> at the source with <code>kind: OCIRepository</code>:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nt">apiVersion</span><span class="p">:</span><span class="w"> </span><span class="l">jaas.metio.wtf/v1</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">kind</span><span class="p">:</span><span class="w"> </span><span class="l">JsonnetSnippet</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">metadata</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">api-latency-dashboard</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">namespace</span><span class="p">:</span><span class="w"> </span><span class="l">default</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">spec</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">serviceAccountName</span><span class="p">:</span><span class="w"> </span><span class="l">dashboards-tenant</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">entryFile</span><span class="p">:</span><span class="w"> </span><span class="l">main.jsonnet</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">sourceRef</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">kind</span><span class="p">:</span><span class="w"> </span><span class="l">OCIRepository</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">dashboards-source</span><span class="w">
</span></span></span></code></pre></div></li>
</ol>
<blockquote>
<p><strong>Single layer is mandatory.</strong> <code>flux push artifact</code> produces an OCI artifact
with exactly one gzipped-tar layer, which is what source-controller expects
and the only shape it unpacks. An artifact built any other way — a hand-rolled
<code>oras push</code> with one file per layer, a <code>Dockerfile</code>/container-image build, or
any tool that splits content across multiple layers — is not consumed
correctly. source-controller cannot reconstruct the file tree, the snippet&rsquo;s
source never resolves, and the snippet reports <code>Ready=False</code>. Always build OCI
sources with <code>flux push artifact</code>.</p>
</blockquote>
<p>Verify the layer count before relying on an artifact. Fetch the manifest and
confirm the <code>layers</code> array has length 1:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">oras manifest fetch oci://ghcr.io/example-org/dashboards:v1 <span class="p">|</span> <span class="se">\
</span></span></span><span class="line"><span class="cl">  jq <span class="s1">&#39;.layers | length&#39;</span>
</span></span></code></pre></div><p>A result of <code>1</code> is required. Any other number means the artifact was not built
with <code>flux push artifact</code> and will not resolve.</p>
<h3 id="private-registries-and-amazon-ecr">Private registries and Amazon ECR</h3>
<p>source-controller performs the pull, so registry credentials belong on the
<code>OCIRepository</code> (or on source-controller itself) — never on the JaaS operator or a
snippet&rsquo;s ServiceAccount. The same applies to a <code>JsonnetLibrary</code> whose <code>sourceRef</code>
points at an <code>OCIRepository</code>.</p>
<p>For a generic private registry, add a <code>spec.secretRef</code> to a <code>docker-registry</code>
Secret. For <strong>Amazon ECR you need no pull Secret at all</strong>: set <code>spec.provider: aws</code>
and source-controller authenticates with its own ambient AWS identity. On EKS that
is an IRSA role bound to <strong>source-controller&rsquo;s</strong> ServiceAccount with ECR read
permissions:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nt">apiVersion</span><span class="p">:</span><span class="w"> </span><span class="l">source.toolkit.fluxcd.io/v1</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">kind</span><span class="p">:</span><span class="w"> </span><span class="l">OCIRepository</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">metadata</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">dashboards-source</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">namespace</span><span class="p">:</span><span class="w"> </span><span class="l">default</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">spec</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">interval</span><span class="p">:</span><span class="w"> </span><span class="l">5m</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">provider</span><span class="p">:</span><span class="w"> </span><span class="l">aws</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">url</span><span class="p">:</span><span class="w"> </span><span class="l">oci://111122223333.dkr.ecr.eu-west-1.amazonaws.com/dashboards</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">ref</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">tag</span><span class="p">:</span><span class="w"> </span><span class="l">v1</span><span class="w">
</span></span></span></code></pre></div><p>The IRSA role needs <code>ecr:GetAuthorizationToken</code> (resource <code>*</code>) plus
<code>ecr:BatchGetImage</code> and <code>ecr:GetDownloadUrlForLayer</code> on the repository. Because the
credential is source-controller&rsquo;s, one role covers every <code>OCIRepository</code> it pulls,
and the JaaS operator stays out of the registry path entirely.</p>
<p>This IRSA role is source-controller&rsquo;s, not the JaaS operator&rsquo;s. The JaaS operator
uses IRSA only for its own <a href="/usage/storage-and-ha/">S3 storage backend</a>
 — a
separate concern from pulling sources.</p>
<p>There is a third way to load OCI content that does not go through a <code>sourceRef</code> at
all: the chart can mount snippets and libraries from <strong>OCI image volumes</strong>
(<code>snippets</code> / <code>additionalLibraries</code>), read straight from a registry into the pod.
Those volumes are pulled by the <strong>kubelet</strong>, exactly like a container image — so
they authenticate the way images do, not through IRSA. On EKS that means the
<strong>node&rsquo;s</strong> IAM role with ECR read (the <code>AmazonEC2ContainerRegistryReadOnly</code>
managed policy the default node role already carries), or an <code>imagePullSecret</code> on
the pod. Pod-level IRSA grants the <em>pod&rsquo;s</em> ServiceAccount AWS API access, which the
kubelet does not use when pulling images, so it is not the mechanism for this path.
With a node role that can read ECR, image-volume snippets and libraries load with
no pull Secret. Static OCI mounts and operator mode are mutually exclusive in one
release, so a given install uses either these mounts or the <code>sourceRef</code> path above,
not both.</p>
<p>The <a href="https://github.com/metio/jsonnet-oci-images">jsonnet-oci-images (JOI)</a>

project enforces this same single-layer rule for every image it publishes, so
its images are ready-made single-layer <code>OCIRepository</code> sources. Reference a JOI
image directly when you need a shared Jsonnet library tree (grafonnet, the
jsonnet-libs catalog) rather than building and maintaining your own OCI source.</p>
<h2 id="bucket">Bucket</h2>
<p>A <code>Bucket</code> source mirrors objects from an S3- or GCS-compatible bucket.
source-controller fetches the matching objects, packs them into the tarball the
operator fetches, and there is no layer constraint — the only requirement is
that the objects laid out under the bucket prefix form the file tree your snippet
expects.</p>
<ol>
<li>
<p>Produce the files to upload. Either upload the individual <code>.jsonnet</code> /
<code>.libsonnet</code> files under a prefix, or pack them into a single archive — both
work, source-controller flattens the mirrored objects into the file tree:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">dashboards/
</span></span><span class="line"><span class="cl">├── main.jsonnet
</span></span><span class="line"><span class="cl">└── lib/
</span></span><span class="line"><span class="cl">    └── panels.libsonnet
</span></span></code></pre></div></li>
<li>
<p>Upload the files to the bucket under a prefix. With the AWS CLI against an
S3-compatible endpoint:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">aws s3 cp dashboards/ s3://example-bucket/dashboards/ <span class="se">\
</span></span></span><span class="line"><span class="cl">  --recursive <span class="se">\
</span></span></span><span class="line"><span class="cl">  --endpoint-url<span class="o">=</span>https://s3.example.com
</span></span></code></pre></div></li>
<li>
<p>Create the <code>Bucket</code> source. With the Flux CLI:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">flux create <span class="nb">source</span> bucket dashboards-source <span class="se">\
</span></span></span><span class="line"><span class="cl">  --bucket-name<span class="o">=</span>example-bucket <span class="se">\
</span></span></span><span class="line"><span class="cl">  --endpoint<span class="o">=</span>s3.example.com <span class="se">\
</span></span></span><span class="line"><span class="cl">  --provider<span class="o">=</span>generic <span class="se">\
</span></span></span><span class="line"><span class="cl">  --secret-ref<span class="o">=</span>bucket-credentials <span class="se">\
</span></span></span><span class="line"><span class="cl">  --interval<span class="o">=</span>5m <span class="se">\
</span></span></span><span class="line"><span class="cl">  --namespace<span class="o">=</span>default <span class="se">\
</span></span></span><span class="line"><span class="cl">  --export
</span></span></code></pre></div><p>The equivalent CR YAML, which is authoritative:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nt">apiVersion</span><span class="p">:</span><span class="w"> </span><span class="l">source.toolkit.fluxcd.io/v1</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">kind</span><span class="p">:</span><span class="w"> </span><span class="l">Bucket</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">metadata</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">dashboards-source</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">namespace</span><span class="p">:</span><span class="w"> </span><span class="l">default</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">spec</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">interval</span><span class="p">:</span><span class="w"> </span><span class="l">5m</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">provider</span><span class="p">:</span><span class="w"> </span><span class="l">generic</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">bucketName</span><span class="p">:</span><span class="w"> </span><span class="l">example-bucket</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">endpoint</span><span class="p">:</span><span class="w"> </span><span class="l">s3.example.com</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">secretRef</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">bucket-credentials</span><span class="w">
</span></span></span></code></pre></div><p>The referenced Secret carries the bucket credentials (<code>accesskey</code> /
<code>secretkey</code>). See the <a href="https://fluxcd.io/">Flux documentation</a>
 for the Secret
layout and provider-specific fields.</p>
</li>
<li>
<p>Point a snippet&rsquo;s <code>spec.sourceRef</code> at the source with <code>kind: Bucket</code>. Use
<code>path:</code> to extract only the prefix that holds your Jsonnet:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nt">apiVersion</span><span class="p">:</span><span class="w"> </span><span class="l">jaas.metio.wtf/v1</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">kind</span><span class="p">:</span><span class="w"> </span><span class="l">JsonnetSnippet</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">metadata</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">api-latency-dashboard</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">namespace</span><span class="p">:</span><span class="w"> </span><span class="l">default</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">spec</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">serviceAccountName</span><span class="p">:</span><span class="w"> </span><span class="l">dashboards-tenant</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">entryFile</span><span class="p">:</span><span class="w"> </span><span class="l">main.jsonnet</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">sourceRef</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">kind</span><span class="p">:</span><span class="w"> </span><span class="l">Bucket</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">dashboards-source</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">path</span><span class="p">:</span><span class="w"> </span><span class="l">dashboards/</span><span class="w">
</span></span></span></code></pre></div></li>
</ol>
<h2 id="which-source-should-i-use">Which source should I use?</h2>
<table>
	<thead>
			<tr>
					<th>Source</th>
					<th>Use when</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td><code>GitRepository</code></td>
					<td>Your Jsonnet is human-authored configuration living in a version-controlled git repository.</td>
			</tr>
			<tr>
					<td><code>OCIRepository</code></td>
					<td>You want an immutable, content-addressed artifact; must be a single layer, and pairs with JOI images.</td>
			</tr>
			<tr>
					<td><code>Bucket</code></td>
					<td>Your artifacts already live in S3- or GCS-compatible object storage.</td>
			</tr>
	</tbody>
</table>
]]></content><category scheme="https://jaas.projects.metio.wtf/tags/operator" term="operator" label="operator"/><category scheme="https://jaas.projects.metio.wtf/tags/sources" term="sources" label="sources"/><category scheme="https://jaas.projects.metio.wtf/tags/oci" term="oci" label="oci"/><category scheme="https://jaas.projects.metio.wtf/tags/flux" term="flux" label="flux"/></entry><entry><title type="html">CrossNamespaceRefRejected</title><link href="https://jaas.projects.metio.wtf/runbooks/crossnamespacerefrejected/?utm_source=atom_feed" rel="alternate" type="text/html"/><link href="https://jaas.projects.metio.wtf/runbooks/librarynotfound/?utm_source=atom_feed" rel="related" type="text/html" title="LibraryNotFound"/><link href="https://jaas.projects.metio.wtf/runbooks/rbacdenied/?utm_source=atom_feed" rel="related" type="text/html" title="RBACDenied"/><link href="https://jaas.projects.metio.wtf/runbooks/serviceaccountmissing/?utm_source=atom_feed" rel="related" type="text/html" title="ServiceAccountMissing"/><link href="https://jaas.projects.metio.wtf/runbooks/operator-watch-silent/?utm_source=atom_feed" rel="related" type="text/html" title="Watch-layer silent failure"/><link href="https://jaas.projects.metio.wtf/runbooks/artifacttoolarge/?utm_source=atom_feed" rel="related" type="text/html" title="ArtifactTooLarge"/><id>https://jaas.projects.metio.wtf/runbooks/crossnamespacerefrejected/</id><published>0001-01-01T00:00:00+00:00</published><updated>2026-06-17T10:56:50+02:00</updated><content type="html"><![CDATA[<blockquote>The snippet references a library or Flux source in a different namespace and cross-namespace refs are disabled</blockquote><h2 id="symptom">Symptom</h2>
<p><code>READY=False</code>, <code>REASON=CrossNamespaceRefRejected</code>. The Message names the offending reference (a library or a sourceRef).</p>
<h2 id="cause">Cause</h2>
<p>The operator is running with <code>--no-cross-namespace-refs=true</code> (the chart default) and the snippet references a library or Flux source in a different namespace.</p>
<p>This is a deliberate isolation control — it mirrors Flux&rsquo;s <code>--no-cross-namespace-refs</code> and stops a tenant in namespace A from reaching libraries / sources in namespace B without an explicit relationship.</p>
<h2 id="diagnosis">Diagnosis</h2>
<p>Inspect the spec and identify which reference points outside the snippet&rsquo;s namespace:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">kubectl --namespace &lt;ns&gt; get jsonnetsnippet &lt;name&gt; --output yaml <span class="p">|</span> grep -E <span class="s2">&#34;namespace:|sourceRef:|libraries:&#34;</span>
</span></span></code></pre></div><h2 id="remediation">Remediation</h2>
<p>Three options, by isolation strength:</p>
<ol>
<li><strong>(recommended)</strong> Duplicate the library / source CR into the snippet&rsquo;s namespace.</li>
<li>Promote the library to an OCI volume — mount via the chart&rsquo;s <code>additionalLibraries</code> map. Becomes part of the operator&rsquo;s filesystem, available to every snippet without a cross-namespace CR ref.</li>
<li><strong>(loosen isolation, cluster-wide)</strong> Set <code>--no-cross-namespace-refs=false</code> on the operator. Affects every tenant in the cluster — only do this when tenants are mutually trusting.</li>
</ol>
]]></content><category scheme="https://jaas.projects.metio.wtf/tags/runbooks" term="runbooks" label="runbooks"/><category scheme="https://jaas.projects.metio.wtf/tags/troubleshooting" term="troubleshooting" label="troubleshooting"/><category scheme="https://jaas.projects.metio.wtf/tags/rbac" term="rbac" label="rbac"/></entry><entry><title type="html">DependencyCycle</title><link href="https://jaas.projects.metio.wtf/runbooks/dependencycycle/?utm_source=atom_feed" rel="alternate" type="text/html"/><link href="https://jaas.projects.metio.wtf/runbooks/sourcefetchfailed/?utm_source=atom_feed" rel="related" type="text/html" title="SourceFetchFailed"/><link href="https://jaas.projects.metio.wtf/runbooks/sourcenotready/?utm_source=atom_feed" rel="related" type="text/html" title="SourceNotReady"/><link href="https://jaas.projects.metio.wtf/runbooks/sourcerefnotyetsupported/?utm_source=atom_feed" rel="related" type="text/html" title="SourceRefNotYetSupported"/><link href="https://jaas.projects.metio.wtf/runbooks/artifacttoolarge/?utm_source=atom_feed" rel="related" type="text/html" title="ArtifactTooLarge"/><link href="https://jaas.projects.metio.wtf/runbooks/crd-watch-engagement/?utm_source=atom_feed" rel="related" type="text/html" title="CRD watch engagement failing"/><id>https://jaas.projects.metio.wtf/runbooks/dependencycycle/</id><published>0001-01-01T00:00:00+00:00</published><updated>2026-06-17T10:56:50+02:00</updated><content type="html"><![CDATA[<blockquote>The snippet&rsquo;s sourceRef or library chain loops back to itself, which would cause infinite re-render</blockquote><h2 id="symptom">Symptom</h2>
<p><code>READY=False</code>, <code>REASON=DependencyCycle</code>. The Message names the snippet that closes the cycle.</p>
<h2 id="cause">Cause</h2>
<p>The snippet&rsquo;s <code>spec.sourceRef</code> chain transitively points back at the snippet itself. The reconciler detects this and refuses to publish so chained snippets don&rsquo;t loop forever (each republish would trigger every downstream snippet to re-render, which would re-trigger the upstream, and so on).</p>
<p>Two cycle shapes:</p>
<ol>
<li><strong>Direct sourceRef cycle:</strong> <code>A.spec.sourceRef → ExternalArtifact/A</code>. A snippet sourcing from its own published artifact.</li>
<li><strong>Library-mediated cycle:</strong> <code>A.spec.libraries → JsonnetLibrary/L</code>, where <code>L.spec.sourceRef → ExternalArtifact/A</code> (or a longer chain back to A).</li>
</ol>
<p>The validating webhook (<code>--enable-webhook</code>) rejects new CRs that introduce a cycle at admission; the reconciler check is a fallback for when admission is bypassed or the cycle is introduced retroactively (e.g., adding a new library that closes a loop with existing snippets).</p>
<h2 id="diagnosis">Diagnosis</h2>
<p>Walk the chain manually:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">kubectl get jsonnetsnippet &lt;name&gt; --output <span class="nv">jsonpath</span><span class="o">=</span><span class="s1">&#39;{.spec.sourceRef}&#39;</span> <span class="o">&amp;&amp;</span> <span class="nb">echo</span>
</span></span><span class="line"><span class="cl"><span class="c1"># Then inspect what that sourceRef points at, and what it sources from in turn.</span>
</span></span></code></pre></div><p>For library-mediated cycles, the chain is:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">snippet A.spec.libraries[i] → JsonnetLibrary L → L.spec.sourceRef → ExternalArtifact X → snippet that publishes X
</span></span></code></pre></div><p>If the publishing snippet at the end is A, you have a cycle.</p>
<h2 id="remediation">Remediation</h2>
<p>Break the cycle by removing the back-edge. Common fixes:</p>
<ul>
<li>detach a library from its sourceRef (inline its files instead, if small)</li>
<li>have the upstream snippet publish a smaller artifact the downstream doesn&rsquo;t need to re-consume</li>
<li>restructure so the shared data lives in a static ConfigMap referenced as a sourceRef-equivalent, not in a snippet output</li>
</ul>
]]></content><category scheme="https://jaas.projects.metio.wtf/tags/runbooks" term="runbooks" label="runbooks"/><category scheme="https://jaas.projects.metio.wtf/tags/troubleshooting" term="troubleshooting" label="troubleshooting"/><category scheme="https://jaas.projects.metio.wtf/tags/sources" term="sources" label="sources"/></entry><entry><title type="html">Deploying manifests with StageSet</title><link href="https://jaas.projects.metio.wtf/tutorials/deploying-manifests/?utm_source=atom_feed" rel="alternate" type="text/html"/><link href="https://jaas.projects.metio.wtf/tutorials/grafana-dashboards/?utm_source=atom_feed" rel="related" type="text/html" title="Grafana dashboards"/><link href="https://jaas.projects.metio.wtf/tutorials/quickstart/?utm_source=atom_feed" rel="related" type="text/html" title="Quickstart"/><id>https://jaas.projects.metio.wtf/tutorials/deploying-manifests/</id><published>0001-01-01T00:00:00+00:00</published><updated>2026-06-17T16:04:27+02:00</updated><content type="html"><![CDATA[<blockquote>Render Kubernetes manifests in Jsonnet, publish them as an ExternalArtifact, and hand them to stageset-controller for a gated rollout.</blockquote><p>JaaS pairs with <a href="https://stageset.projects.metio.wtf/">stageset-controller</a>
 to
deploy Kubernetes manifests as code: you author the manifests in Jsonnet, the
JaaS operator renders and publishes them as a Flux <code>ExternalArtifact</code>, and
stageset-controller rolls that artifact out across ordered, gated stages.</p>
<p>This tutorial covers the JaaS side — authoring the manifests with top-level
arguments and external variables, and publishing the rendered JSON. The rollout
side (the <code>StageSet</code> resource, its stages, gates, and actions) lives on the
stageset-controller site and is linked at the end.</p>
<h2 id="prerequisites">Prerequisites</h2>
<ul>
<li>The JaaS operator installed and a tenant ServiceAccount granted the
<code>externalartifacts</code> write verbs. The <a href="/tutorials/quickstart/">Quickstart</a>

covers both.</li>
<li>stageset-controller installed, if you intend to follow the handoff section and
roll the manifests out.</li>
</ul>
<p>This tutorial uses the namespace <code>default</code> and the tenant ServiceAccount
<code>manifests-tenant</code>.</p>
<h2 id="step-1--grant-the-tenant-serviceaccount-its-verbs">Step 1 — Grant the tenant ServiceAccount its verbs</h2>
<p>The snippet publishes an <code>ExternalArtifact</code>, so the tenant needs the
<code>externalartifacts</code> write verbs:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">cat <span class="s">&lt;&lt;EOF | kubectl apply -f -
</span></span></span><span class="line"><span class="cl"><span class="s">apiVersion: v1
</span></span></span><span class="line"><span class="cl"><span class="s">kind: ServiceAccount
</span></span></span><span class="line"><span class="cl"><span class="s">metadata:
</span></span></span><span class="line"><span class="cl"><span class="s">  name: manifests-tenant
</span></span></span><span class="line"><span class="cl"><span class="s">  namespace: default
</span></span></span><span class="line"><span class="cl"><span class="s">---
</span></span></span><span class="line"><span class="cl"><span class="s">apiVersion: rbac.authorization.k8s.io/v1
</span></span></span><span class="line"><span class="cl"><span class="s">kind: Role
</span></span></span><span class="line"><span class="cl"><span class="s">metadata:
</span></span></span><span class="line"><span class="cl"><span class="s">  namespace: default
</span></span></span><span class="line"><span class="cl"><span class="s">  name: manifests-tenant
</span></span></span><span class="line"><span class="cl"><span class="s">rules:
</span></span></span><span class="line"><span class="cl"><span class="s">  - apiGroups: [source.toolkit.fluxcd.io]
</span></span></span><span class="line"><span class="cl"><span class="s">    resources: [externalartifacts]
</span></span></span><span class="line"><span class="cl"><span class="s">    verbs: [get, create, update, patch]
</span></span></span><span class="line"><span class="cl"><span class="s">---
</span></span></span><span class="line"><span class="cl"><span class="s">apiVersion: rbac.authorization.k8s.io/v1
</span></span></span><span class="line"><span class="cl"><span class="s">kind: RoleBinding
</span></span></span><span class="line"><span class="cl"><span class="s">metadata:
</span></span></span><span class="line"><span class="cl"><span class="s">  namespace: default
</span></span></span><span class="line"><span class="cl"><span class="s">  name: manifests-tenant
</span></span></span><span class="line"><span class="cl"><span class="s">subjects:
</span></span></span><span class="line"><span class="cl"><span class="s">  - kind: ServiceAccount
</span></span></span><span class="line"><span class="cl"><span class="s">    name: manifests-tenant
</span></span></span><span class="line"><span class="cl"><span class="s">    namespace: default
</span></span></span><span class="line"><span class="cl"><span class="s">roleRef:
</span></span></span><span class="line"><span class="cl"><span class="s">  apiGroup: rbac.authorization.k8s.io
</span></span></span><span class="line"><span class="cl"><span class="s">  kind: Role
</span></span></span><span class="line"><span class="cl"><span class="s">  name: manifests-tenant
</span></span></span><span class="line"><span class="cl"><span class="s">EOF</span>
</span></span></code></pre></div><p>Verify the ServiceAccount and binding:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">kubectl --namespace default get serviceaccount manifests-tenant
</span></span><span class="line"><span class="cl">kubectl --namespace default get rolebinding manifests-tenant
</span></span></code></pre></div><h2 id="step-2--author-and-apply-the-manifest-snippet">Step 2 — Author and apply the manifest snippet</h2>
<p>The snippet renders a <code>List</code> of a <code>Deployment</code> and a <code>Service</code> from top-level
arguments (<code>spec.tlas</code>) and an external variable (<code>spec.externalVariables</code>). A
single-value TLA arrives as a string; the snippet parses the replica count with
<code>std.parseInt</code>. <code>spec.output</code> stays at its default <code>rendered</code> so the artifact
carries the evaluated manifest JSON:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">cat <span class="s">&lt;&lt;EOF | kubectl apply -f -
</span></span></span><span class="line"><span class="cl"><span class="s">apiVersion: jaas.metio.wtf/v1
</span></span></span><span class="line"><span class="cl"><span class="s">kind: JsonnetSnippet
</span></span></span><span class="line"><span class="cl"><span class="s">metadata:
</span></span></span><span class="line"><span class="cl"><span class="s">  name: web-app
</span></span></span><span class="line"><span class="cl"><span class="s">  namespace: default
</span></span></span><span class="line"><span class="cl"><span class="s">spec:
</span></span></span><span class="line"><span class="cl"><span class="s">  serviceAccountName: manifests-tenant
</span></span></span><span class="line"><span class="cl"><span class="s">  output: rendered
</span></span></span><span class="line"><span class="cl"><span class="s">  files:
</span></span></span><span class="line"><span class="cl"><span class="s">    main.jsonnet: |
</span></span></span><span class="line"><span class="cl"><span class="s">      function(name=&#39;web&#39;, replicas=&#39;2&#39;)
</span></span></span><span class="line"><span class="cl"><span class="s">        local image = std.extVar(&#39;image&#39;);
</span></span></span><span class="line"><span class="cl"><span class="s">        local labels = { &#39;app.kubernetes.io/name&#39;: name };
</span></span></span><span class="line"><span class="cl"><span class="s">        {
</span></span></span><span class="line"><span class="cl"><span class="s">          apiVersion: &#39;v1&#39;,
</span></span></span><span class="line"><span class="cl"><span class="s">          kind: &#39;List&#39;,
</span></span></span><span class="line"><span class="cl"><span class="s">          items: [
</span></span></span><span class="line"><span class="cl"><span class="s">            {
</span></span></span><span class="line"><span class="cl"><span class="s">              apiVersion: &#39;apps/v1&#39;,
</span></span></span><span class="line"><span class="cl"><span class="s">              kind: &#39;Deployment&#39;,
</span></span></span><span class="line"><span class="cl"><span class="s">              metadata: { name: name, labels: labels },
</span></span></span><span class="line"><span class="cl"><span class="s">              spec: {
</span></span></span><span class="line"><span class="cl"><span class="s">                replicas: std.parseInt(replicas),
</span></span></span><span class="line"><span class="cl"><span class="s">                selector: { matchLabels: labels },
</span></span></span><span class="line"><span class="cl"><span class="s">                template: {
</span></span></span><span class="line"><span class="cl"><span class="s">                  metadata: { labels: labels },
</span></span></span><span class="line"><span class="cl"><span class="s">                  spec: {
</span></span></span><span class="line"><span class="cl"><span class="s">                    containers: [{
</span></span></span><span class="line"><span class="cl"><span class="s">                      name: name,
</span></span></span><span class="line"><span class="cl"><span class="s">                      image: image,
</span></span></span><span class="line"><span class="cl"><span class="s">                      ports: [{ containerPort: 8080 }],
</span></span></span><span class="line"><span class="cl"><span class="s">                    }],
</span></span></span><span class="line"><span class="cl"><span class="s">                  },
</span></span></span><span class="line"><span class="cl"><span class="s">                },
</span></span></span><span class="line"><span class="cl"><span class="s">              },
</span></span></span><span class="line"><span class="cl"><span class="s">            },
</span></span></span><span class="line"><span class="cl"><span class="s">            {
</span></span></span><span class="line"><span class="cl"><span class="s">              apiVersion: &#39;v1&#39;,
</span></span></span><span class="line"><span class="cl"><span class="s">              kind: &#39;Service&#39;,
</span></span></span><span class="line"><span class="cl"><span class="s">              metadata: { name: name, labels: labels },
</span></span></span><span class="line"><span class="cl"><span class="s">              spec: {
</span></span></span><span class="line"><span class="cl"><span class="s">                selector: labels,
</span></span></span><span class="line"><span class="cl"><span class="s">                ports: [{ port: 80, targetPort: 8080 }],
</span></span></span><span class="line"><span class="cl"><span class="s">              },
</span></span></span><span class="line"><span class="cl"><span class="s">            },
</span></span></span><span class="line"><span class="cl"><span class="s">          ],
</span></span></span><span class="line"><span class="cl"><span class="s">        }
</span></span></span><span class="line"><span class="cl"><span class="s">  tlas:
</span></span></span><span class="line"><span class="cl"><span class="s">    name: [web]
</span></span></span><span class="line"><span class="cl"><span class="s">    replicas: [&#34;3&#34;]
</span></span></span><span class="line"><span class="cl"><span class="s">  externalVariables:
</span></span></span><span class="line"><span class="cl"><span class="s">    image: &#34;ghcr.io/example/web:1.4.0&#34;
</span></span></span><span class="line"><span class="cl"><span class="s">EOF</span>
</span></span></code></pre></div><p>Each <code>spec.tlas</code> value is a list, matching the HTTP query-parameter convention:
a single element becomes a string TLA, multiple elements a JSON-encoded array.
External variables seed <code>std.extVar</code> lookups.</p>
<h2 id="step-3--confirm-the-manifests-rendered">Step 3 — Confirm the manifests rendered</h2>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">kubectl --namespace default get jsonnetsnippet web-app
</span></span><span class="line"><span class="cl"><span class="c1"># NAME      READY   URL                                                                                     AGE</span>
</span></span><span class="line"><span class="cl"><span class="c1"># web-app   True    http://jaas-storage.jaas-system.svc.cluster.local:8082/default/web-app/&lt;sha256&gt;.tar.gz  5s</span>
</span></span></code></pre></div><p>If <code>READY</code> is <code>False</code>, describe the snippet — the Ready condition&rsquo;s <code>Reason</code> and
<code>Message</code> name the cause:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">kubectl --namespace default describe jsonnetsnippet web-app
</span></span></code></pre></div><h2 id="step-4--inspect-the-published-manifests">Step 4 — Inspect the published manifests</h2>
<p>Fetch the artifact from a one-shot pod to see the rendered manifests:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl"><span class="nv">URL</span><span class="o">=</span><span class="k">$(</span>kubectl --namespace default get jsonnetsnippet web-app -o <span class="nv">jsonpath</span><span class="o">=</span><span class="s1">&#39;{.status.artifactURL}&#39;</span><span class="k">)</span>
</span></span><span class="line"><span class="cl">kubectl run --rm -i --restart<span class="o">=</span>Never --image<span class="o">=</span>docker.io/curlimages/curl:8.10.1 fetch -- <span class="se">\
</span></span></span><span class="line"><span class="cl">    sh -c <span class="s2">&#34;curl -fsSL &#39;</span><span class="nv">$URL</span><span class="s2">&#39; | tar -xzO rendered.json&#34;</span>
</span></span><span class="line"><span class="cl"><span class="c1"># {</span>
</span></span><span class="line"><span class="cl"><span class="c1">#    &#34;apiVersion&#34;: &#34;v1&#34;,</span>
</span></span><span class="line"><span class="cl"><span class="c1">#    &#34;kind&#34;: &#34;List&#34;,</span>
</span></span><span class="line"><span class="cl"><span class="c1">#    &#34;items&#34;: [ { &#34;kind&#34;: &#34;Deployment&#34;, ... }, { &#34;kind&#34;: &#34;Service&#34;, ... } ]</span>
</span></span><span class="line"><span class="cl"><span class="c1"># }</span>
</span></span></code></pre></div><p><code>rendered.json</code> is the manifest set a Flux consumer applies to the cluster.</p>
<h2 id="handoff-roll-the-manifests-out-with-stageset">Handoff: roll the manifests out with StageSet</h2>
<p>The published <code>ExternalArtifact</code> is now ready for a Flux consumer. A consumer
references it in one of two ways:</p>
<ul>
<li><strong>Directly</strong> — name the <code>ExternalArtifact</code> (which shares the snippet&rsquo;s name and
namespace) in a <code>sourceRef</code>.</li>
<li><strong>Producer-aware</strong> — name the producing <code>JsonnetSnippet</code> and let the consumer
resolve it to the <code>ExternalArtifact</code>. JaaS writes a three-field back-pointer
(<code>apiVersion</code>, <code>kind</code>, <code>name</code>) under the artifact&rsquo;s <code>spec.sourceRef</code> for this,
which is the contract producer-aware resolvers match on.</li>
</ul>
<p>stageset-controller consumes the published artifact the producer-aware way and
rolls it out across ordered, gated stages. The <code>StageSet</code> resource, its stages,
gates, and actions live on the stageset-controller documentation:</p>
<ul>
<li><strong>stageset-controller producer-aware sources guide:</strong>
<a href="https://stageset.projects.metio.wtf/usage/producer-aware-sources/">https://stageset.projects.metio.wtf/usage/producer-aware-sources/</a>
</li>
<li><strong>stageset-controller project:</strong> <a href="https://stageset.projects.metio.wtf/">https://stageset.projects.metio.wtf/</a>
</li>
</ul>
<p>Follow that guide for the rollout side; it picks up exactly where this tutorial
leaves off — at the published <code>ExternalArtifact</code>.</p>
<h2 id="where-to-go-next">Where to go next</h2>
<ul>
<li><a href="/usage/operator-mode/">Operator mode</a>
 — the full operator reference,
including the <code>ExternalArtifact</code> <code>spec.sourceRef</code> back-pointer contract that
producer-aware consumers match on.</li>
<li><a href="/usage/snippet-sources/">Snippet sources</a>
 — back the manifests with a
<code>GitRepository</code> or OCIRepository instead of inline <code>spec.files</code>.</li>
</ul>
]]></content><category scheme="https://jaas.projects.metio.wtf/tags/stageset" term="stageset" label="stageset"/><category scheme="https://jaas.projects.metio.wtf/tags/manifests" term="manifests" label="manifests"/><category scheme="https://jaas.projects.metio.wtf/tags/externalartifact" term="externalartifact" label="externalartifact"/></entry><entry><title type="html">Eval-concurrency saturation</title><link href="https://jaas.projects.metio.wtf/runbooks/eval-saturation/?utm_source=atom_feed" rel="alternate" type="text/html"/><link href="https://jaas.projects.metio.wtf/runbooks/evaluationfailed/?utm_source=atom_feed" rel="related" type="text/html" title="EvaluationFailed"/><link href="https://jaas.projects.metio.wtf/runbooks/evaluationtimeout/?utm_source=atom_feed" rel="related" type="text/html" title="EvaluationTimeout"/><link href="https://jaas.projects.metio.wtf/runbooks/externalvariableconflict/?utm_source=atom_feed" rel="related" type="text/html" title="ExternalVariableConflict"/><link href="https://jaas.projects.metio.wtf/runbooks/artifacttoolarge/?utm_source=atom_feed" rel="related" type="text/html" title="ArtifactTooLarge"/><link href="https://jaas.projects.metio.wtf/runbooks/crd-watch-engagement/?utm_source=atom_feed" rel="related" type="text/html" title="CRD watch engagement failing"/><id>https://jaas.projects.metio.wtf/runbooks/eval-saturation/</id><published>0001-01-01T00:00:00+00:00</published><updated>2026-06-17T10:56:50+02:00</updated><content type="html"><![CDATA[<blockquote>The global concurrent-eval cap is full and the operator is shedding new evaluations, typically because a runaway snippet is holding slots past its deadline</blockquote><p>Not tied to a single <code>Reason</code> — this page covers what to do when the global concurrent-eval cap (<code>--max-concurrent-evals</code>) is full and JaaS is shedding new evaluations. The cap exists because the synchronous go-jsonnet API has no context-aware cancellation: once an eval starts it runs to natural completion, so an unbounded queue lets a runaway snippet pile up goroutines that outlive every caller&rsquo;s deadline.</p>
<h2 id="symptom">Symptom</h2>
<p>One or more of:</p>
<ul>
<li><code>JaaSEvalSaturation</code> is firing — <code>jaas_eval_in_flight / jaas_eval_max_concurrent</code> has been above the threshold (default <code>0.9</code>) for the alert window.</li>
<li><code>JaaSEvalRejected</code> is firing — <code>rate(jaas_eval_unavailable_total[5m])</code> has been above the threshold.</li>
<li>HTTP clients see <code>503 Service Unavailable</code> with body <code>{&quot;error&quot;: &quot;evaluation_unavailable&quot;, &quot;message&quot;: &quot;concurrent-eval cap is full; retry after backoff&quot;}</code>.</li>
<li><code>kubectl describe jsonnetsnippet</code> shows recurring <code>Warning EvalUnavailable</code> events with message <code>reconcile deferred for 1s by --max-concurrent-evals</code>. Ready condition stays untouched (backpressure is not failure).</li>
<li><code>jaas_eval_outstanding_timed_out</code> is also elevated — confirms the runaway-snippet diagnosis: orphaned evals are pinning slots while their parents have already given up.</li>
</ul>
<h2 id="diagnosis-why-is-the-cap-full">Diagnosis: why is the cap full?</h2>
<p>The cap fills for two distinct reasons. The right remediation depends on which.</p>
<h3 id="path-a--runaway-snippet-goroutines-outliving-their-ctx">Path A — runaway snippet (goroutines outliving their ctx)</h3>
<p>Read the leak gauge. If it&rsquo;s non-zero and trending up, evaluations are starting but not finishing — almost always a single snippet whose work dwarfs <code>--evaluation-timeout</code>.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl"><span class="c1"># Live count of evals whose parent reconcile already timed out:</span>
</span></span><span class="line"><span class="cl">kubectl --namespace &lt;jaas-ns&gt; <span class="nb">exec</span> deploy/jaas -- <span class="se">\
</span></span></span><span class="line"><span class="cl">  wget -qO- http://localhost:8083/metrics <span class="p">|</span> grep jaas_eval_outstanding_timed_out
</span></span></code></pre></div><p>To find the culprit, scan recent reconcile logs for <code>Jsonnet evaluation timed out</code> followed by repeated <code>EvalUnavailable</code> warnings on the same snippet:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">kubectl --namespace &lt;jaas-ns&gt; logs deploy/jaas --since<span class="o">=</span>15m <span class="se">\
</span></span></span><span class="line"><span class="cl">  <span class="p">|</span> grep -E <span class="s1">&#39;EvaluationTimeout|EvalUnavailable&#39;</span> <span class="se">\
</span></span></span><span class="line"><span class="cl">  <span class="p">|</span> sort <span class="p">|</span> uniq -c <span class="p">|</span> sort -rn <span class="p">|</span> head
</span></span></code></pre></div><p>The snippet whose name dominates that list is the culprit. Common causes:</p>
<ul>
<li>Deep recursion that takes seconds-to-minutes to complete naturally even after the parent deadline fires.</li>
<li>Pathological library import that triggers go-jsonnet&rsquo;s worst-case eval order.</li>
<li>A <code>std.foldl</code> over a generated array of millions of entries.</li>
</ul>
<h3 id="path-b--genuine-load-above-the-cap">Path B — genuine load above the cap</h3>
<p>Leak gauge is at zero (or steady, not growing), <code>jaas_eval_in_flight</code> is pegged near the cap, and many distinct snippets show <code>EvalUnavailable</code> events. The cap is sized too small for the workload.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl"><span class="c1"># Distribution of which snippets are seeing rejections — a flat</span>
</span></span><span class="line"><span class="cl"><span class="c1"># distribution across many snippets is path B; a single dominant</span>
</span></span><span class="line"><span class="cl"><span class="c1"># snippet is path A.</span>
</span></span><span class="line"><span class="cl">kubectl --namespace &lt;jaas-ns&gt; <span class="nb">exec</span> deploy/jaas -- <span class="se">\
</span></span></span><span class="line"><span class="cl">  wget -qO- http://localhost:8083/metrics <span class="se">\
</span></span></span><span class="line"><span class="cl">  <span class="p">|</span> grep jaas_snippet_eval_unavailable_total
</span></span></code></pre></div><h2 id="remediation">Remediation</h2>
<h3 id="path-a--runaway-snippet">Path A — runaway snippet</h3>
<ol>
<li>
<p><strong>Suspend the offender</strong> to stop new evals while you fix the snippet:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">kubectl --namespace &lt;ns&gt; patch jsonnetsnippet &lt;name&gt; --type merge <span class="se">\
</span></span></span><span class="line"><span class="cl">  --patch <span class="s1">&#39;{&#34;spec&#34;:{&#34;suspend&#34;:true}}&#39;</span>
</span></span></code></pre></div></li>
<li>
<p><strong>Inspect the snippet</strong> to understand the cost. Lower <code>--max-stack</code> is a blunt clamp that rejects pathological recursion before it can leak. The chart&rsquo;s <code>operator.maxStack</code> defaults to 500; pull it down to ~200 if the snippet doesn&rsquo;t legitimately need deeper recursion.</p>
</li>
<li>
<p><strong>Tighten <code>--evaluation-timeout</code></strong> if the snippet&rsquo;s natural completion time is the load-bearing factor. A 5s default lets a 60s pathological eval leak for nearly a minute; dropping to 1s shrinks the worst-case leak window.</p>
</li>
<li>
<p><strong>Re-enable</strong> after the snippet spec is fixed:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">kubectl --namespace &lt;ns&gt; patch jsonnetsnippet &lt;name&gt; --type merge <span class="se">\
</span></span></span><span class="line"><span class="cl">  --patch <span class="s1">&#39;{&#34;spec&#34;:{&#34;suspend&#34;:false}}&#39;</span>
</span></span></code></pre></div></li>
</ol>
<h3 id="path-b--genuine-load">Path B — genuine load</h3>
<ol>
<li>
<p><strong>Raise the cap</strong> if the operator has CPU headroom. The default is <code>max(GOMAXPROCS*4, 16)</code>; double it via the chart:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">helm upgrade &lt;release&gt; &lt;chart&gt; --reuse-values <span class="se">\
</span></span></span><span class="line"><span class="cl">  --set arguments.maxConcurrentEvals<span class="o">=</span><span class="m">64</span>
</span></span></code></pre></div><p>Each in-flight eval pins roughly one CPU when actively running, so the practical ceiling is bounded by node CPU. Past 2-3× GOMAXPROCS the gains drop sharply — more contention, same throughput.</p>
</li>
<li>
<p><strong>Tune the per-snippet rate limiter</strong> if a small number of snippets dominate the request rate. <code>--rerender-rate</code> + <code>--rerender-burst</code> cap each snippet&rsquo;s reconcile frequency independent of the global eval cap.</p>
</li>
<li>
<p><strong>Scale horizontally</strong> if a single replica can&rsquo;t keep up even at the raised cap. The chart&rsquo;s <code>replicas.max</code> controls the HPA ceiling; combined with the storage layer&rsquo;s leader election (S3 backend) you get multi-replica HA where every replica reads but only the lease-holder writes.</p>
</li>
</ol>
<h2 id="when-not-to-raise-the-cap">When NOT to raise the cap</h2>
<p>If the leak gauge is non-zero AND growing, raising the cap lets more goroutines pile up before the next saturation event. Diagnose path A first. The cap is a backpressure boundary, not a throughput knob.</p>
<h2 id="disable-the-gate-not-recommended">Disable the gate (not recommended)</h2>
<p><code>--max-concurrent-evals=0</code> disables the gate entirely. The leak gauge keeps working, but rejections never fire — a single runaway snippet can OOM the pod. Use only if you&rsquo;ve sized the workload precisely and want to surface saturation purely via the leak gauge.</p>
]]></content><category scheme="https://jaas.projects.metio.wtf/tags/runbooks" term="runbooks" label="runbooks"/><category scheme="https://jaas.projects.metio.wtf/tags/troubleshooting" term="troubleshooting" label="troubleshooting"/><category scheme="https://jaas.projects.metio.wtf/tags/evaluation" term="evaluation" label="evaluation"/></entry><entry><title type="html">Evaluation and security</title><link href="https://jaas.projects.metio.wtf/usage/evaluation-and-security/?utm_source=atom_feed" rel="alternate" type="text/html"/><link href="https://jaas.projects.metio.wtf/runbooks/eval-saturation/?utm_source=atom_feed" rel="related" type="text/html" title="Eval-concurrency saturation"/><link href="https://jaas.projects.metio.wtf/runbooks/evaluationfailed/?utm_source=atom_feed" rel="related" type="text/html" title="EvaluationFailed"/><link href="https://jaas.projects.metio.wtf/runbooks/evaluationtimeout/?utm_source=atom_feed" rel="related" type="text/html" title="EvaluationTimeout"/><link href="https://jaas.projects.metio.wtf/runbooks/externalvariableconflict/?utm_source=atom_feed" rel="related" type="text/html" title="ExternalVariableConflict"/><link href="https://jaas.projects.metio.wtf/usage/network-policy/?utm_source=atom_feed" rel="related" type="text/html" title="Network policy"/><id>https://jaas.projects.metio.wtf/usage/evaluation-and-security/</id><published>0001-01-01T00:00:00+00:00</published><updated>2026-06-16T20:48:39+02:00</updated><content type="html"><![CDATA[<blockquote>Timeout, stack, and concurrency caps on evaluation, and the security model to lock down before exposing the service.</blockquote><p>JaaS runs Jsonnet on the server and returns the result over HTTP. Three caps
bound each evaluation, and a small security model governs what a snippet and its
callers can reach. Review and tune both sections before exposing the service to
a wider audience.</p>
<h2 id="evaluation-caps">Evaluation caps</h2>
<table>
	<thead>
			<tr>
					<th>Flag</th>
					<th>Default</th>
					<th>Effect</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td><code>--evaluation-timeout</code></td>
					<td><code>5s</code></td>
					<td>Wall-clock budget per evaluation. Exceeding it returns <code>504 evaluation_timeout</code>. <code>0</code> disables the timeout.</td>
			</tr>
			<tr>
					<td><code>--max-stack</code></td>
					<td><code>500</code></td>
					<td>Maximum Jsonnet call-stack depth. <code>0</code> uses go-jsonnet&rsquo;s own default.</td>
			</tr>
			<tr>
					<td><code>--max-concurrent-evals</code></td>
					<td><code>max(GOMAXPROCS*4, 16)</code></td>
					<td>In-flight evaluations allowed at once. Excess requests return <code>503 evaluation_unavailable</code>. <code>0</code> disables the cap.</td>
			</tr>
	</tbody>
</table>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">./jaas <span class="se">\
</span></span></span><span class="line"><span class="cl">  --snippet-directory examples/snippets/dashboards <span class="se">\
</span></span></span><span class="line"><span class="cl">  --evaluation-timeout 2s <span class="se">\
</span></span></span><span class="line"><span class="cl">  --max-stack <span class="m">1000</span> <span class="se">\
</span></span></span><span class="line"><span class="cl">  --max-concurrent-evals <span class="m">32</span>
</span></span></code></pre></div><p>The default for <code>--max-concurrent-evals</code> bounds worst-case goroutine pile-up
under a runaway snippet. Each in-flight evaluation pins roughly one CPU for its
working set, so raising the cap far above the available parallelism queues work
without adding throughput.</p>
<h2 id="security-model">Security model</h2>
<p><strong>Library paths are an unrestricted read scope.</strong> Any file reachable under a
configured <code>--library-path</code>, or under a snippet&rsquo;s own directory, can be
<code>import</code>-ed or <code>importstr</code>-ed by any snippet — go-jsonnet&rsquo;s importer does not
sandbox per snippet. Scope these directories tightly. Never point them at <code>/</code>,
<code>/etc</code>, or anywhere holding credentials.</p>
<p><strong>Snippets are operator-controlled, not caller-controlled.</strong> Callers supply only
top-level arguments through the query string. Jsonnet&rsquo;s <code>import</code> and <code>importstr</code>
require string-literal paths, so a TLA or external variable cannot construct an
import path. Deploying a snippet authored by someone you do not trust is
equivalent to running their code on the server.</p>
<p><strong>Snippet name resolution is sandboxed.</strong> The URL&rsquo;s snippet segment resolves
through Go&rsquo;s <code>os.Root</code>, which rejects <code>..</code> traversal and symlinks that escape the
configured snippet directory. A URL like <code>/jsonnet/../etc/passwd</code> returns <code>404</code>,
even though the OS would otherwise resolve the path.</p>
<p><strong>Evaluation has caps but no mid-flight cancellation.</strong> <code>--evaluation-timeout</code>
bounds wall-clock time and <code>--max-stack</code> bounds call-stack depth, but go-jsonnet
cannot abort an evaluation already running. A slow snippet keeps consuming CPU
until it finishes naturally or the timeout fires the HTTP response. Size
container CPU and memory limits to absorb that worst case.</p>
<p>The Prometheus metrics <code>jaas_eval_in_flight</code> (gauge: live in-flight count),
<code>jaas_eval_unavailable_total</code> (counter: cumulative cap rejections), and
<code>jaas_eval_outstanding_timed_out</code> (gauge: evals still running after their
request timed out) surface how close evaluation runs to these caps. See
<a href="/usage/observability/">Observability</a>
 for detail.</p>
<p>The HTTP status codes these caps produce are documented in the
<a href="/usage/rendering-endpoint/">rendering endpoint</a>
 error contract.</p>
]]></content><category scheme="https://jaas.projects.metio.wtf/tags/security" term="security" label="security"/><category scheme="https://jaas.projects.metio.wtf/tags/limits" term="limits" label="limits"/><category scheme="https://jaas.projects.metio.wtf/tags/evaluation" term="evaluation" label="evaluation"/></entry><entry><title type="html">EvaluationFailed</title><link href="https://jaas.projects.metio.wtf/runbooks/evaluationfailed/?utm_source=atom_feed" rel="alternate" type="text/html"/><link href="https://jaas.projects.metio.wtf/runbooks/eval-saturation/?utm_source=atom_feed" rel="related" type="text/html" title="Eval-concurrency saturation"/><link href="https://jaas.projects.metio.wtf/runbooks/evaluationtimeout/?utm_source=atom_feed" rel="related" type="text/html" title="EvaluationTimeout"/><link href="https://jaas.projects.metio.wtf/runbooks/externalvariableconflict/?utm_source=atom_feed" rel="related" type="text/html" title="ExternalVariableConflict"/><link href="https://jaas.projects.metio.wtf/runbooks/artifacttoolarge/?utm_source=atom_feed" rel="related" type="text/html" title="ArtifactTooLarge"/><link href="https://jaas.projects.metio.wtf/runbooks/crd-watch-engagement/?utm_source=atom_feed" rel="related" type="text/html" title="CRD watch engagement failing"/><id>https://jaas.projects.metio.wtf/runbooks/evaluationfailed/</id><published>0001-01-01T00:00:00+00:00</published><updated>2026-06-17T10:56:50+02:00</updated><content type="html"><![CDATA[<blockquote>The Jsonnet snippet failed to evaluate due to a syntax error, runtime error, or unresolved import</blockquote><h2 id="symptom">Symptom</h2>
<p><code>READY=False</code>, <code>REASON=EvaluationFailed</code>. The Message contains the raw go-jsonnet diagnostic — file name, line, column, and the underlying error.</p>
<h2 id="cause">Cause</h2>
<p>The snippet failed to evaluate. Three broad categories:</p>
<ul>
<li><strong>Syntax error</strong> — unclosed brace, missing comma, bad indent.</li>
<li><strong>Runtime error</strong> — <code>std.extVar('missing')</code> for an unset variable, division by zero, <code>error '...'</code> thrown explicitly.</li>
<li><strong>Import error</strong> — <code>import 'missing.libsonnet'</code> resolves to nothing in the snippet&rsquo;s file map or library imports.</li>
</ul>
<h2 id="diagnosis">Diagnosis</h2>
<p>Read the Message — it names the file and line. Reproduce locally:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl"><span class="c1"># Pull the snippet&#39;s files into a tempdir, then evaluate.</span>
</span></span><span class="line"><span class="cl">kubectl get jsonnetsnippet &lt;name&gt; --output json <span class="p">|</span> jq -r <span class="s1">&#39;.spec.files[&#34;main.jsonnet&#34;]&#39;</span> &gt; /tmp/main.jsonnet
</span></span><span class="line"><span class="cl">jsonnet /tmp/main.jsonnet
</span></span></code></pre></div><p>For sourceRef-based snippets, fetch the tarball:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl"><span class="nv">SOURCE_URL</span><span class="o">=</span><span class="k">$(</span>kubectl get gitrepository &lt;name&gt; --output <span class="nv">jsonpath</span><span class="o">=</span><span class="s1">&#39;{.status.artifact.url}&#39;</span><span class="k">)</span>
</span></span><span class="line"><span class="cl">curl -sL <span class="s2">&#34;</span><span class="nv">$SOURCE_URL</span><span class="s2">&#34;</span> <span class="p">|</span> tar -xz -C /tmp/snippet
</span></span><span class="line"><span class="cl">jsonnet /tmp/snippet/&lt;entry-file&gt;
</span></span></code></pre></div><h2 id="remediation">Remediation</h2>
<p>Fix the snippet (or its libraries / source) and re-apply.</p>
<p>The diagnostic message can leak the on-disk path of the snippet — fine in-cluster, worth gating behind a flag if exposed to untrusted callers in the future.</p>
]]></content><category scheme="https://jaas.projects.metio.wtf/tags/runbooks" term="runbooks" label="runbooks"/><category scheme="https://jaas.projects.metio.wtf/tags/troubleshooting" term="troubleshooting" label="troubleshooting"/><category scheme="https://jaas.projects.metio.wtf/tags/evaluation" term="evaluation" label="evaluation"/></entry><entry><title type="html">EvaluationTimeout</title><link href="https://jaas.projects.metio.wtf/runbooks/evaluationtimeout/?utm_source=atom_feed" rel="alternate" type="text/html"/><link href="https://jaas.projects.metio.wtf/runbooks/eval-saturation/?utm_source=atom_feed" rel="related" type="text/html" title="Eval-concurrency saturation"/><link href="https://jaas.projects.metio.wtf/runbooks/evaluationfailed/?utm_source=atom_feed" rel="related" type="text/html" title="EvaluationFailed"/><link href="https://jaas.projects.metio.wtf/runbooks/externalvariableconflict/?utm_source=atom_feed" rel="related" type="text/html" title="ExternalVariableConflict"/><link href="https://jaas.projects.metio.wtf/runbooks/artifacttoolarge/?utm_source=atom_feed" rel="related" type="text/html" title="ArtifactTooLarge"/><link href="https://jaas.projects.metio.wtf/runbooks/crd-watch-engagement/?utm_source=atom_feed" rel="related" type="text/html" title="CRD watch engagement failing"/><id>https://jaas.projects.metio.wtf/runbooks/evaluationtimeout/</id><published>0001-01-01T00:00:00+00:00</published><updated>2026-06-16T20:48:39+02:00</updated><content type="html"><![CDATA[<blockquote>The snippet&rsquo;s evaluation exceeded the operator&rsquo;s wall-clock deadline and was abandoned</blockquote><h2 id="symptom">Symptom</h2>
<p><code>READY=False</code>, <code>REASON=EvaluationTimeout</code>. The snippet&rsquo;s eval ran longer than the operator&rsquo;s <code>--evaluation-timeout</code>.</p>
<h2 id="cause">Cause</h2>
<p>Snippets are evaluated synchronously per reconcile. The deadline is wall-clock, not CPU — but go-jsonnet has no mid-evaluation cancellation, so a snippet that runs over the deadline still keeps consuming CPU on the operator pod until it returns naturally.</p>
<p>Common triggers:</p>
<ul>
<li>a snippet recursing deeper than necessary (try lowering <code>--max-stack</code> to surface this as a stack-limit error instead, then optimize)</li>
<li>a snippet that loads a huge sourceRef tarball and walks it</li>
<li>a snippet that calls <code>std.set</code> / <code>std.uniq</code> over a very large array</li>
</ul>
<h2 id="diagnosis">Diagnosis</h2>
<p>Time it locally:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl"><span class="nb">time</span> jsonnet /tmp/snippet/&lt;entry-file&gt;
</span></span></code></pre></div><p>If it takes seconds locally, the operator&rsquo;s bound is too tight. If it takes minutes locally, the snippet itself is the problem.</p>
<h2 id="remediation">Remediation</h2>
<p>Two paths:</p>
<ol>
<li><strong>Optimize the snippet.</strong> Memoize repeated work into <code>local</code> bindings, narrow the input set, avoid <code>std.flattenDeepArrays</code> over deep trees.</li>
<li><strong>Raise the operator&rsquo;s bound.</strong> <code>--evaluation-timeout=30s</code> (default <code>5s</code>) gives more headroom. Pair with <code>resources.cpu</code> headroom in the chart so the slow snippet doesn&rsquo;t starve other reconciles.</li>
</ol>
<p>For pathological inputs, consider splitting the snippet — render the slow part less often via a separate snippet others source from (see <code>examples/operator/chained-snippets.yaml</code>).</p>
]]></content><category scheme="https://jaas.projects.metio.wtf/tags/runbooks" term="runbooks" label="runbooks"/><category scheme="https://jaas.projects.metio.wtf/tags/troubleshooting" term="troubleshooting" label="troubleshooting"/><category scheme="https://jaas.projects.metio.wtf/tags/evaluation" term="evaluation" label="evaluation"/></entry><entry><title type="html">External variables and TLAs</title><link href="https://jaas.projects.metio.wtf/usage/external-variables-and-tlas/?utm_source=atom_feed" rel="alternate" type="text/html"/><id>https://jaas.projects.metio.wtf/usage/external-variables-and-tlas/</id><published>0001-01-01T00:00:00+00:00</published><updated>2026-06-16T20:48:39+02:00</updated><content type="html"><![CDATA[<blockquote>Passing values into a render through external variables and top-level arguments.</blockquote><p>JaaS feeds two kinds of input into an evaluation: external variables, set by the
process owner at startup, and top-level arguments, supplied per request through
the URL query string.</p>
<h2 id="external-variables">External variables</h2>
<p>External variables come from two sources. The environment mechanism reads every
variable prefixed with <code>JAAS_EXT_VAR_</code> — the suffix is the variable name:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl"><span class="nv">JAAS_EXT_VAR_name</span><span class="o">=</span>Alice <span class="se">\
</span></span></span><span class="line"><span class="cl"><span class="nv">JAAS_EXT_VAR_key</span><span class="o">=</span>secret <span class="se">\
</span></span></span><span class="line"><span class="cl">  ./jaas --snippet-directory examples/snippets/dashboards
</span></span></code></pre></div><p>The <code>--ext-var KEY=VALUE</code> flag does the same and is repeatable. On a key
conflict, the flag takes precedence over the environment value:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">./jaas <span class="se">\
</span></span></span><span class="line"><span class="cl">  --snippet-directory examples/snippets/dashboards <span class="se">\
</span></span></span><span class="line"><span class="cl">  --ext-var <span class="nv">name</span><span class="o">=</span>Alice <span class="se">\
</span></span></span><span class="line"><span class="cl">  --ext-var <span class="nv">key</span><span class="o">=</span>secret
</span></span></code></pre></div><p>A snippet reads a variable with <code>std.extVar</code>:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-jsonnet" data-lang="jsonnet"><span class="line"><span class="cl"><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nv">person1</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nv">name</span><span class="p">:</span><span class="w"> </span><span class="nb">std.extVar</span><span class="p">(</span><span class="s">&#39;name&#39;</span><span class="p">),</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nv">external</span><span class="p">:</span><span class="w"> </span><span class="nb">std.extVar</span><span class="p">(</span><span class="s">&#39;key&#39;</span><span class="p">),</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="p">},</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="p">}</span><span class="w">
</span></span></span></code></pre></div><p>Fetching <code>example1</code> with those variables set produces:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">curl http://127.0.0.1:8080/jsonnet/example1
</span></span></code></pre></div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-json" data-lang="json"><span class="line"><span class="cl"><span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="nt">&#34;person1&#34;</span><span class="p">:</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="nt">&#34;external&#34;</span><span class="p">:</span> <span class="s2">&#34;secret&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="nt">&#34;name&#34;</span><span class="p">:</span> <span class="s2">&#34;Alice&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="nt">&#34;welcome&#34;</span><span class="p">:</span> <span class="s2">&#34;Hello Alice!&#34;</span>
</span></span><span class="line"><span class="cl">  <span class="p">}</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre></div><p>External variables are fixed at startup. Callers cannot set them per request —
that is what top-level arguments are for.</p>
<h2 id="top-level-arguments">Top-level arguments</h2>
<p>A snippet that evaluates to a function receives top-level arguments (TLAs) from
the URL query string. The <code>tla-example</code> snippet is such a function:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-jsonnet" data-lang="jsonnet"><span class="line"><span class="cl"><span class="k">function</span><span class="p">(</span><span class="nv">something</span><span class="o">=</span><span class="s">&#34;value&#34;</span><span class="p">,</span><span class="w"> </span><span class="nv">other</span><span class="o">=</span><span class="s">&#34;more&#34;</span><span class="p">,</span><span class="w"> </span><span class="nv">required</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nv">person1</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nv">welcome</span><span class="p">:</span><span class="w"> </span><span class="s">&#39;Hello &#39;</span><span class="w"> </span><span class="o">+</span><span class="w"> </span><span class="nv">something</span><span class="w"> </span><span class="o">+</span><span class="w"> </span><span class="s">&#39;!&#39;</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nv">key</span><span class="p">:</span><span class="w"> </span><span class="nv">other</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nv">required</span><span class="p">:</span><span class="w"> </span><span class="nb">std.parseJson</span><span class="p">(</span><span class="nv">required</span><span class="p">),</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="p">},</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="p">}</span><span class="w">
</span></span></span></code></pre></div><p>Each query parameter sets a TLA. A single value becomes a string:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">curl <span class="s1">&#39;http://127.0.0.1:8080/jsonnet/tla-example?something=Ada&amp;required=42&#39;</span>
</span></span><span class="line"><span class="cl"><span class="c1"># {&#34;person1&#34;:{&#34;key&#34;:&#34;more&#34;,&#34;required&#34;:42,&#34;welcome&#34;:&#34;Hello Ada!&#34;},...}</span>
</span></span></code></pre></div><p>A repeated parameter becomes a list. The <code>multi-tla</code> snippet joins whatever it
receives:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-jsonnet" data-lang="jsonnet"><span class="line"><span class="cl"><span class="k">function</span><span class="p">(</span><span class="nv">tags</span><span class="o">=</span><span class="p">[</span><span class="s">&#34;default&#34;</span><span class="p">])</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nv">count</span><span class="p">:</span><span class="w"> </span><span class="nb">std.length</span><span class="p">(</span><span class="nv">tags</span><span class="p">),</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nv">list</span><span class="p">:</span><span class="w"> </span><span class="nv">tags</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nv">joined</span><span class="p">:</span><span class="w"> </span><span class="nb">std.join</span><span class="p">(</span><span class="s">&#34;, &#34;</span><span class="p">,</span><span class="w"> </span><span class="nv">tags</span><span class="p">),</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="p">}</span><span class="w">
</span></span></span></code></pre></div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">curl <span class="s1">&#39;http://127.0.0.1:8080/jsonnet/multi-tla?tags=blue&amp;tags=green&#39;</span>
</span></span><span class="line"><span class="cl"><span class="c1"># {&#34;count&#34;:2,&#34;joined&#34;:&#34;blue, green&#34;,&#34;list&#34;:[&#34;blue&#34;,&#34;green&#34;]}</span>
</span></span></code></pre></div><p>A bare parameter with no value sets the TLA to an empty string:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">curl <span class="s1">&#39;http://127.0.0.1:8080/jsonnet/tla-example?something&amp;required=0&#39;</span>
</span></span></code></pre></div><p>For the request and response shape these examples ride on, see the
<a href="/usage/rendering-endpoint/">rendering endpoint</a>
.</p>
]]></content><category scheme="https://jaas.projects.metio.wtf/tags/extvars" term="extvars" label="extvars"/><category scheme="https://jaas.projects.metio.wtf/tags/tlas" term="tlas" label="tlas"/><category scheme="https://jaas.projects.metio.wtf/tags/query" term="query" label="query"/></entry><entry><title type="html">ExternalArtifact output contract</title><link href="https://jaas.projects.metio.wtf/api/externalartifact/?utm_source=atom_feed" rel="alternate" type="text/html"/><link href="https://jaas.projects.metio.wtf/api/jsonnetsnippet/?utm_source=atom_feed" rel="related" type="text/html" title="JsonnetSnippet"/><link href="https://jaas.projects.metio.wtf/usage/creating-sources/?utm_source=atom_feed" rel="related" type="text/html" title="Creating source artifacts"/><link href="https://jaas.projects.metio.wtf/api/jsonnetlibrary/?utm_source=atom_feed" rel="related" type="text/html" title="JsonnetLibrary"/><link href="https://jaas.projects.metio.wtf/usage/operator-mode/?utm_source=atom_feed" rel="related" type="text/html" title="Operator mode"/><link href="https://jaas.projects.metio.wtf/usage/admission-webhook/?utm_source=atom_feed" rel="related" type="text/html" title="Admission webhook"/><id>https://jaas.projects.metio.wtf/api/externalartifact/</id><published>0001-01-01T00:00:00+00:00</published><updated>2026-06-16T20:48:39+02:00</updated><content type="html"><![CDATA[<blockquote>The shape JaaS writes to the Flux ExternalArtifact CR and the contract downstream consumers depend on.</blockquote><p>For every successfully evaluated <code>JsonnetSnippet</code>, the JaaS operator upserts a
Flux <code>ExternalArtifact</code> CR (<code>source.toolkit.fluxcd.io/v1</code>) in the same namespace
as the snippet. JaaS does not own the <code>ExternalArtifact</code> CRD — it is defined and
installed by Flux&rsquo;s source-controller. The full CRD schema is in the
<a href="https://fluxcd.io/flux/components/source/externalartifacts/">Flux ExternalArtifact reference</a>
;
below is the subset JaaS writes and the invariants downstream consumers can rely on.</p>
<p>For task-oriented context, see <a href="/usage/operator-mode/">Operator mode</a>
.</p>
<h2 id="what-jaas-writes">What JaaS writes</h2>
<h3 id="specsourceref"><code>spec.sourceRef</code></h3>
<p>JaaS stamps a back-pointer to the originating snippet on <code>spec.sourceRef</code>:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nt">spec</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">sourceRef</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">apiVersion</span><span class="p">:</span><span class="w"> </span><span class="l">jaas.metio.wtf/v1</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">kind</span><span class="p">:</span><span class="w"> </span><span class="l">JsonnetSnippet</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">&lt;snippet-name&gt;</span><span class="w">
</span></span></span></code></pre></div><p>The namespace is always the snippet&rsquo;s own namespace — JaaS never publishes an
<code>ExternalArtifact</code> to a different namespace. The three fields
(<code>apiVersion</code>, <code>kind</code>, <code>name</code>) are wire-stable: downstream consumers that do
producer-aware reverse lookup (such as stageset-controller&rsquo;s RFC-0012 resolution)
match on this triple. Renaming any field is a breaking change.</p>
<h3 id="statusartifact"><code>status.artifact</code></h3>
<p>After a successful publish, JaaS writes the following fields under <code>status.artifact</code>:</p>
<table>
	<thead>
			<tr>
					<th>Field</th>
					<th>Type</th>
					<th>Description</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td><code>url</code></td>
					<td>string</td>
					<td>HTTP URL of the published tarball. Revision-addressed: <code>&lt;storage-base-url&gt;/&lt;namespace&gt;/&lt;name&gt;/&lt;sha256-hex&gt;.tar.gz</code>. Byte-stable for the lifetime of the revision in the keep-set — re-publishing a different revision does not mutate the bytes at this URL.</td>
			</tr>
			<tr>
					<td><code>path</code></td>
					<td>string</td>
					<td>Storage-backend-relative path of the tarball.</td>
			</tr>
			<tr>
					<td><code>revision</code></td>
					<td>string</td>
					<td><code>sha256:&lt;hex&gt;</code> content hash of the artifact. In <code>rendered</code> output mode this is the sha256 of the evaluated JSON; in <code>source</code> mode it is a deterministic hash over all source files (sorted by filename).</td>
			</tr>
			<tr>
					<td><code>digest</code></td>
					<td>string</td>
					<td><code>sha256:&lt;hex&gt;</code> of the tarball bytes (the <code>.tar.gz</code> itself, not the content). Used by Flux consumers to verify integrity after download.</td>
			</tr>
			<tr>
					<td><code>size</code></td>
					<td>int64</td>
					<td>Tarball size in bytes.</td>
			</tr>
			<tr>
					<td><code>lastUpdateTime</code></td>
					<td>string</td>
					<td>RFC3339 timestamp of the most recent successful publish.</td>
			</tr>
	</tbody>
</table>
<h3 id="statusconditions"><code>status.conditions</code></h3>
<p>JaaS writes a single <code>Ready</code> condition on every successful publish:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nt">status</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">conditions</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span>- <span class="nt">type</span><span class="p">:</span><span class="w"> </span><span class="l">Ready</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">status</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;True&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">reason</span><span class="p">:</span><span class="w"> </span><span class="l">Succeeded</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">message</span><span class="p">:</span><span class="w"> </span><span class="l">artifact published</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">lastTransitionTime</span><span class="p">:</span><span class="w"> </span><span class="l">&lt;RFC3339&gt;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">observedGeneration</span><span class="p">:</span><span class="w"> </span><span class="l">&lt;generation&gt;</span><span class="w">
</span></span></span></code></pre></div><p><code>lastTransitionTime</code> is preserved across steady-state republishes (same
<code>Ready=True</code>, new revision) so the timestamp does not churn. It advances only
when the condition transitions (e.g. from <code>False</code> to <code>True</code> after a failure
clears).</p>
<h2 id="what-downstream-consumers-rely-on">What downstream consumers rely on</h2>
<p><strong>Gate on <code>Ready=True</code> before fetching.</strong> Every Flux consumer — including
<code>kustomize-controller</code>, <code>helm-controller</code>, and JaaS&rsquo;s own chained-snippet
<code>sourceRef</code> resolver — treats an <code>ExternalArtifact</code> as not-yet-consumable until
<code>status.conditions[Ready].status == &quot;True&quot;</code>. A snippet that has not yet
completed its first successful reconcile will have no <code>Ready</code> condition (or
<code>Ready=False</code>) and leaves chained snippets blocked with reason <code>SourceNotReady</code>.</p>
<p><strong>URL is revision-addressed and byte-stable.</strong> The URL published in
<code>status.artifact.url</code> has the form
<code>&lt;storage-base-url&gt;/&lt;namespace&gt;/&lt;name&gt;/&lt;sha256-hex&gt;.tar.gz</code>. The bytes at that
URL are immutable for as long as the revision is in the snippet&rsquo;s keep-set
(<code>spec.history</code>). Consumers can safely re-fetch a pinned URL (e.g. during
rollback) and verify it against the recorded <code>digest</code>. Once a revision leaves
the keep-set it is garbage-collected after the operator&rsquo;s GC grace period; a
fetch after that point returns 404.</p>
<p><strong>Revision identifies content, not time.</strong> Two publishes that produce identical
content (same evaluated JSON or same source files) yield the same <code>revision</code>.
Consumers that cache by revision can skip a re-fetch when the revision has not
changed.</p>
<p><strong>The snippet mirrors <code>status.artifactURL</code>.</strong> To avoid a second lookup, the
originating <code>JsonnetSnippet</code> also carries the URL in its own
<code>status.artifactURL</code>. <code>kubectl describe jsonnetsnippet</code> therefore surfaces the
artifact location directly.</p>
<h2 id="tarball-contents">Tarball contents</h2>
<p>The tarball layout depends on <code>spec.output</code> of the originating <code>JsonnetSnippet</code>:</p>
<table>
	<thead>
			<tr>
					<th><code>spec.output</code></th>
					<th>Tarball contents</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td><code>rendered</code> (default)</td>
					<td>A single <code>rendered.json</code> holding the evaluated JSON output.</td>
			</tr>
			<tr>
					<td><code>source</code></td>
					<td>Every source file from the resolved snippet source (inline <code>spec.files</code> or the files extracted from the <code>spec.sourceRef</code> tarball), with their original relative paths.</td>
			</tr>
	</tbody>
</table>
<p>All tarballs are produced deterministically: entries are sorted by path and
<code>ModTime</code> is zeroed. Two publishes from the same input produce byte-identical
<code>.tar.gz</code> files and therefore the same <code>revision</code> and <code>digest</code>.</p>
]]></content><category scheme="https://jaas.projects.metio.wtf/tags/api" term="api" label="api"/><category scheme="https://jaas.projects.metio.wtf/tags/flux" term="flux" label="flux"/><category scheme="https://jaas.projects.metio.wtf/tags/operator" term="operator" label="operator"/><category scheme="https://jaas.projects.metio.wtf/tags/snippets" term="snippets" label="snippets"/></entry><entry><title type="html">ExternalVariableConflict</title><link href="https://jaas.projects.metio.wtf/runbooks/externalvariableconflict/?utm_source=atom_feed" rel="alternate" type="text/html"/><link href="https://jaas.projects.metio.wtf/runbooks/eval-saturation/?utm_source=atom_feed" rel="related" type="text/html" title="Eval-concurrency saturation"/><link href="https://jaas.projects.metio.wtf/runbooks/evaluationfailed/?utm_source=atom_feed" rel="related" type="text/html" title="EvaluationFailed"/><link href="https://jaas.projects.metio.wtf/runbooks/evaluationtimeout/?utm_source=atom_feed" rel="related" type="text/html" title="EvaluationTimeout"/><link href="https://jaas.projects.metio.wtf/runbooks/artifacttoolarge/?utm_source=atom_feed" rel="related" type="text/html" title="ArtifactTooLarge"/><link href="https://jaas.projects.metio.wtf/runbooks/crd-watch-engagement/?utm_source=atom_feed" rel="related" type="text/html" title="CRD watch engagement failing"/><id>https://jaas.projects.metio.wtf/runbooks/externalvariableconflict/</id><published>0001-01-01T00:00:00+00:00</published><updated>2026-06-17T10:56:50+02:00</updated><content type="html"><![CDATA[<blockquote>The snippet declares an external variable key already claimed by the operator via &ndash;ext-var</blockquote><h2 id="symptom">Symptom</h2>
<p><code>READY=False</code>, <code>REASON=ExternalVariableConflict</code>. The Message names the conflicting key.</p>
<h2 id="cause">Cause</h2>
<p>The snippet&rsquo;s <code>spec.externalVariables</code> declares a key that the operator already owns via <code>--ext-var</code> (cluster operator-level). Operator keys win by design — they&rsquo;re how the cluster admin pins cluster-scoped values like <code>cluster</code>, <code>region</code>, <code>environment</code> so a tenant snippet can&rsquo;t override them.</p>
<h2 id="diagnosis">Diagnosis</h2>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl"><span class="c1"># Which keys does the operator own?</span>
</span></span><span class="line"><span class="cl">kubectl --namespace &lt;jaas-ns&gt; get pod --selector app.kubernetes.io/name<span class="o">=</span>jaas --output yaml <span class="p">|</span> grep -A1 <span class="s2">&#34;\--ext-var=&#34;</span>
</span></span></code></pre></div><p>Cross-reference with the snippet&rsquo;s <code>spec.externalVariables</code>.</p>
<h2 id="remediation">Remediation</h2>
<p>Rename the conflicting key in the snippet, or remove it from the snippet entirely (the operator-level value flows through automatically).</p>
<p>If the snippet legitimately needs a different value, that&rsquo;s a structural problem — the snippet shouldn&rsquo;t ship with an opinion that overrides a cluster-wide invariant. Re-discuss with the cluster admin.</p>
<p>The validating webhook (<code>--enable-webhook</code>) catches this at admission so <code>kubectl apply</code> rejects it before it lands. The reconciler enforcing the same rule is a fallback for when admission is bypassed.</p>
]]></content><category scheme="https://jaas.projects.metio.wtf/tags/runbooks" term="runbooks" label="runbooks"/><category scheme="https://jaas.projects.metio.wtf/tags/troubleshooting" term="troubleshooting" label="troubleshooting"/><category scheme="https://jaas.projects.metio.wtf/tags/evaluation" term="evaluation" label="evaluation"/></entry><entry><title type="html">Grafana dashboards</title><link href="https://jaas.projects.metio.wtf/tutorials/grafana-dashboards/?utm_source=atom_feed" rel="alternate" type="text/html"/><link href="https://jaas.projects.metio.wtf/tutorials/deploying-manifests/?utm_source=atom_feed" rel="related" type="text/html" title="Deploying manifests with StageSet"/><link href="https://jaas.projects.metio.wtf/tutorials/quickstart/?utm_source=atom_feed" rel="related" type="text/html" title="Quickstart"/><id>https://jaas.projects.metio.wtf/tutorials/grafana-dashboards/</id><published>0001-01-01T00:00:00+00:00</published><updated>2026-06-17T16:04:27+02:00</updated><content type="html"><![CDATA[<blockquote>Author a Grafana dashboard in Jsonnet with grafonnet, render it through the JaaS operator, and publish the dashboard JSON as an ExternalArtifact.</blockquote><p>JaaS pairs with the
<a href="https://grafana.github.io/grafana-operator/">grafana-operator</a>
 to manage
Grafana dashboards as code: you author the dashboard in Jsonnet, the JaaS
operator renders it and publishes the dashboard JSON as a Flux
<code>ExternalArtifact</code>, and the grafana-operator reconciles that artifact into a live
Grafana instance.</p>
<p>This tutorial covers the JaaS side — authoring the dashboard, importing
grafonnet as a <code>JsonnetLibrary</code>, and publishing the rendered JSON. The
grafana-operator side (the <code>GrafanaDashboard</code> CR, datasources, folders) lives on
their site and is linked at the end.</p>
<h2 id="prerequisites">Prerequisites</h2>
<ul>
<li>The JaaS operator installed and a tenant ServiceAccount granted the
<code>externalartifacts</code> write verbs. The <a href="/tutorials/quickstart/">Quickstart</a>

covers both.</li>
<li>The grafana-operator installed, if you intend to follow the handoff section
and reconcile the dashboard into Grafana.</li>
</ul>
<p>This tutorial uses the namespace <code>default</code> and the tenant ServiceAccount
<code>dashboards-tenant</code>.</p>
<h2 id="step-1--grant-the-tenant-serviceaccount-its-verbs">Step 1 — Grant the tenant ServiceAccount its verbs</h2>
<p>The snippet imports a <code>JsonnetLibrary</code>, so on top of the <code>externalartifacts</code>
write verbs the tenant needs <code>get</code> on <code>jsonnetlibraries</code>:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">cat <span class="s">&lt;&lt;EOF | kubectl apply -f -
</span></span></span><span class="line"><span class="cl"><span class="s">apiVersion: v1
</span></span></span><span class="line"><span class="cl"><span class="s">kind: ServiceAccount
</span></span></span><span class="line"><span class="cl"><span class="s">metadata:
</span></span></span><span class="line"><span class="cl"><span class="s">  name: dashboards-tenant
</span></span></span><span class="line"><span class="cl"><span class="s">  namespace: default
</span></span></span><span class="line"><span class="cl"><span class="s">---
</span></span></span><span class="line"><span class="cl"><span class="s">apiVersion: rbac.authorization.k8s.io/v1
</span></span></span><span class="line"><span class="cl"><span class="s">kind: Role
</span></span></span><span class="line"><span class="cl"><span class="s">metadata:
</span></span></span><span class="line"><span class="cl"><span class="s">  namespace: default
</span></span></span><span class="line"><span class="cl"><span class="s">  name: dashboards-tenant
</span></span></span><span class="line"><span class="cl"><span class="s">rules:
</span></span></span><span class="line"><span class="cl"><span class="s">  - apiGroups: [source.toolkit.fluxcd.io]
</span></span></span><span class="line"><span class="cl"><span class="s">    resources: [externalartifacts]
</span></span></span><span class="line"><span class="cl"><span class="s">    verbs: [get, create, update, patch]
</span></span></span><span class="line"><span class="cl"><span class="s">  - apiGroups: [jaas.metio.wtf]
</span></span></span><span class="line"><span class="cl"><span class="s">    resources: [jsonnetlibraries]
</span></span></span><span class="line"><span class="cl"><span class="s">    verbs: [get]
</span></span></span><span class="line"><span class="cl"><span class="s">---
</span></span></span><span class="line"><span class="cl"><span class="s">apiVersion: rbac.authorization.k8s.io/v1
</span></span></span><span class="line"><span class="cl"><span class="s">kind: RoleBinding
</span></span></span><span class="line"><span class="cl"><span class="s">metadata:
</span></span></span><span class="line"><span class="cl"><span class="s">  namespace: default
</span></span></span><span class="line"><span class="cl"><span class="s">  name: dashboards-tenant
</span></span></span><span class="line"><span class="cl"><span class="s">subjects:
</span></span></span><span class="line"><span class="cl"><span class="s">  - kind: ServiceAccount
</span></span></span><span class="line"><span class="cl"><span class="s">    name: dashboards-tenant
</span></span></span><span class="line"><span class="cl"><span class="s">    namespace: default
</span></span></span><span class="line"><span class="cl"><span class="s">roleRef:
</span></span></span><span class="line"><span class="cl"><span class="s">  apiGroup: rbac.authorization.k8s.io
</span></span></span><span class="line"><span class="cl"><span class="s">  kind: Role
</span></span></span><span class="line"><span class="cl"><span class="s">  name: dashboards-tenant
</span></span></span><span class="line"><span class="cl"><span class="s">EOF</span>
</span></span></code></pre></div><p>Verify the ServiceAccount and binding:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">kubectl --namespace default get serviceaccount dashboards-tenant
</span></span><span class="line"><span class="cl">kubectl --namespace default get rolebinding dashboards-tenant
</span></span></code></pre></div><h2 id="step-2--publish-the-dashboard-helpers-as-a-jsonnetlibrary">Step 2 — Publish the dashboard helpers as a JsonnetLibrary</h2>
<p>A <code>JsonnetLibrary</code> holds reusable <code>.libsonnet</code> files that snippets in the same
namespace import by alias. The example below carries a minimal set of dashboard
constructors. In a production setup this is where grafonnet lives — see
<a href="/usage/jsonnet-libraries/">Jsonnet libraries</a>
 for serving the full grafonnet
tree from an OCIRepository.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">cat <span class="s">&lt;&lt;EOF | kubectl apply -f -
</span></span></span><span class="line"><span class="cl"><span class="s">apiVersion: jaas.metio.wtf/v1
</span></span></span><span class="line"><span class="cl"><span class="s">kind: JsonnetLibrary
</span></span></span><span class="line"><span class="cl"><span class="s">metadata:
</span></span></span><span class="line"><span class="cl"><span class="s">  name: grafana-helpers
</span></span></span><span class="line"><span class="cl"><span class="s">  namespace: default
</span></span></span><span class="line"><span class="cl"><span class="s">spec:
</span></span></span><span class="line"><span class="cl"><span class="s">  files:
</span></span></span><span class="line"><span class="cl"><span class="s">    dashboard.libsonnet: |
</span></span></span><span class="line"><span class="cl"><span class="s">      {
</span></span></span><span class="line"><span class="cl"><span class="s">        new(title): {
</span></span></span><span class="line"><span class="cl"><span class="s">          title: title,
</span></span></span><span class="line"><span class="cl"><span class="s">          schemaVersion: 38,
</span></span></span><span class="line"><span class="cl"><span class="s">          panels: [],
</span></span></span><span class="line"><span class="cl"><span class="s">        },
</span></span></span><span class="line"><span class="cl"><span class="s">      }
</span></span></span><span class="line"><span class="cl"><span class="s">    panel.libsonnet: |
</span></span></span><span class="line"><span class="cl"><span class="s">      {
</span></span></span><span class="line"><span class="cl"><span class="s">        timeseries(title, expr): {
</span></span></span><span class="line"><span class="cl"><span class="s">          type: &#39;timeseries&#39;,
</span></span></span><span class="line"><span class="cl"><span class="s">          title: title,
</span></span></span><span class="line"><span class="cl"><span class="s">          targets: [{ expr: expr }],
</span></span></span><span class="line"><span class="cl"><span class="s">        },
</span></span></span><span class="line"><span class="cl"><span class="s">        stat(title, expr): {
</span></span></span><span class="line"><span class="cl"><span class="s">          type: &#39;stat&#39;,
</span></span></span><span class="line"><span class="cl"><span class="s">          title: title,
</span></span></span><span class="line"><span class="cl"><span class="s">          targets: [{ expr: expr }],
</span></span></span><span class="line"><span class="cl"><span class="s">        },
</span></span></span><span class="line"><span class="cl"><span class="s">      }
</span></span></span><span class="line"><span class="cl"><span class="s">EOF</span>
</span></span></code></pre></div><p>Verify the library:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">kubectl --namespace default get jsonnetlibrary grafana-helpers
</span></span></code></pre></div><h2 id="step-3--author-and-apply-the-dashboard-snippet">Step 3 — Author and apply the dashboard snippet</h2>
<p>The <code>JsonnetSnippet</code> imports the library by the alias declared in
<code>spec.libraries[*].importPath</code>, composes a dashboard from its constructors, and
leaves <code>spec.output</code> at its default <code>rendered</code> so the published artifact carries
the evaluated dashboard JSON:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">cat <span class="s">&lt;&lt;EOF | kubectl apply -f -
</span></span></span><span class="line"><span class="cl"><span class="s">apiVersion: jaas.metio.wtf/v1
</span></span></span><span class="line"><span class="cl"><span class="s">kind: JsonnetSnippet
</span></span></span><span class="line"><span class="cl"><span class="s">metadata:
</span></span></span><span class="line"><span class="cl"><span class="s">  name: api-latency
</span></span></span><span class="line"><span class="cl"><span class="s">  namespace: default
</span></span></span><span class="line"><span class="cl"><span class="s">spec:
</span></span></span><span class="line"><span class="cl"><span class="s">  serviceAccountName: dashboards-tenant
</span></span></span><span class="line"><span class="cl"><span class="s">  output: rendered
</span></span></span><span class="line"><span class="cl"><span class="s">  files:
</span></span></span><span class="line"><span class="cl"><span class="s">    main.jsonnet: |
</span></span></span><span class="line"><span class="cl"><span class="s">      local dashboard = import &#39;grafana/dashboard.libsonnet&#39;;
</span></span></span><span class="line"><span class="cl"><span class="s">      local panel = import &#39;grafana/panel.libsonnet&#39;;
</span></span></span><span class="line"><span class="cl"><span class="s">      dashboard.new(&#39;API Latency&#39;) + {
</span></span></span><span class="line"><span class="cl"><span class="s">        panels: [
</span></span></span><span class="line"><span class="cl"><span class="s">          panel.timeseries(&#39;p99 by route&#39;, &#39;histogram_quantile(0.99, rate(http_request_duration_seconds_bucket[5m]))&#39;),
</span></span></span><span class="line"><span class="cl"><span class="s">          panel.stat(&#39;error rate&#39;, &#39;sum(rate(http_requests_total{code=~&#34;5..&#34;}[5m]))&#39;),
</span></span></span><span class="line"><span class="cl"><span class="s">        ],
</span></span></span><span class="line"><span class="cl"><span class="s">      }
</span></span></span><span class="line"><span class="cl"><span class="s">  libraries:
</span></span></span><span class="line"><span class="cl"><span class="s">    - kind: JsonnetLibrary
</span></span></span><span class="line"><span class="cl"><span class="s">      name: grafana-helpers
</span></span></span><span class="line"><span class="cl"><span class="s">      importPath: grafana
</span></span></span><span class="line"><span class="cl"><span class="s">EOF</span>
</span></span></code></pre></div><p>The <code>importPath: grafana</code> ties <code>import 'grafana/dashboard.libsonnet'</code> to the
<code>grafana-helpers</code> library. It defaults to the library&rsquo;s <code>metadata.name</code>, so
naming the library <code>grafana</code> would let you drop the field. <code>kind</code> is always
<code>JsonnetLibrary</code>.</p>
<h2 id="step-4--confirm-the-dashboard-rendered">Step 4 — Confirm the dashboard rendered</h2>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">kubectl --namespace default get jsonnetsnippet api-latency
</span></span><span class="line"><span class="cl"><span class="c1"># NAME          READY   URL                                                                                         AGE</span>
</span></span><span class="line"><span class="cl"><span class="c1"># api-latency   True    http://jaas-storage.jaas-system.svc.cluster.local:8082/default/api-latency/&lt;sha256&gt;.tar.gz  5s</span>
</span></span></code></pre></div><p>If <code>READY</code> is <code>False</code>, describe the snippet — the Ready condition&rsquo;s <code>Reason</code> and
<code>Message</code> name the cause (an RBAC gap on the library, an import alias collision,
or a Jsonnet error):</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">kubectl --namespace default describe jsonnetsnippet api-latency
</span></span></code></pre></div><h2 id="step-5--inspect-the-published-dashboard-json">Step 5 — Inspect the published dashboard JSON</h2>
<p>Fetch the artifact from a one-shot pod to see the rendered dashboard:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl"><span class="nv">URL</span><span class="o">=</span><span class="k">$(</span>kubectl --namespace default get jsonnetsnippet api-latency -o <span class="nv">jsonpath</span><span class="o">=</span><span class="s1">&#39;{.status.artifactURL}&#39;</span><span class="k">)</span>
</span></span><span class="line"><span class="cl">kubectl run --rm -i --restart<span class="o">=</span>Never --image<span class="o">=</span>docker.io/curlimages/curl:8.10.1 fetch -- <span class="se">\
</span></span></span><span class="line"><span class="cl">    sh -c <span class="s2">&#34;curl -fsSL &#39;</span><span class="nv">$URL</span><span class="s2">&#39; | tar -xzO rendered.json&#34;</span>
</span></span><span class="line"><span class="cl"><span class="c1"># {</span>
</span></span><span class="line"><span class="cl"><span class="c1">#    &#34;panels&#34;: [ ... ],</span>
</span></span><span class="line"><span class="cl"><span class="c1">#    &#34;schemaVersion&#34;: 38,</span>
</span></span><span class="line"><span class="cl"><span class="c1">#    &#34;title&#34;: &#34;API Latency&#34;</span>
</span></span><span class="line"><span class="cl"><span class="c1"># }</span>
</span></span></code></pre></div><p><code>rendered.json</code> is the Grafana dashboard model — the exact JSON the
grafana-operator hands to Grafana&rsquo;s dashboard API.</p>
<h2 id="use-real-grafonnet-instead-of-the-toy-helpers">Use real grafonnet instead of the toy helpers</h2>
<p><code>grafana-helpers</code> kept this tutorial self-contained, but in production you import
the real <a href="https://github.com/grafana/grafonnet">grafonnet</a>
 library from a JOI
image rather than hand-rolling constructors. Install it as a <code>JsonnetLibrary</code>
with the <a href="https://github.com/metio/helm-charts/tree/main/charts/joi"><code>joi</code> Helm chart</a>
:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">helm upgrade --install joi oci://ghcr.io/metio/helm-charts/joi <span class="se">\
</span></span></span><span class="line"><span class="cl">  --namespace default <span class="se">\
</span></span></span><span class="line"><span class="cl">  --set libraries.grafonnet.enabled<span class="o">=</span><span class="nb">true</span>
</span></span></code></pre></div><p>That renders an <code>OCIRepository</code> plus a <code>JsonnetLibrary</code> named <code>grafonnet</code>,
sourcing <code>ghcr.io/metio/joi-grafana-grafonnet</code>. The snippet then references that
library in place of <code>grafana-helpers</code> and imports the real grafonnet API by its
full jb-vendor path:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nt">apiVersion</span><span class="p">:</span><span class="w"> </span><span class="l">jaas.metio.wtf/v1</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">kind</span><span class="p">:</span><span class="w"> </span><span class="l">JsonnetSnippet</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">metadata</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">api-latency</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">namespace</span><span class="p">:</span><span class="w"> </span><span class="l">default</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">spec</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">serviceAccountName</span><span class="p">:</span><span class="w"> </span><span class="l">dashboard-renderer</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">libraries</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span>- <span class="nt">kind</span><span class="p">:</span><span class="w"> </span><span class="l">JsonnetLibrary</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">grafonnet</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">files</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">main.jsonnet</span><span class="p">:</span><span class="w"> </span><span class="p">|</span><span class="sd">
</span></span></span><span class="line"><span class="cl"><span class="sd">      local g = import &#39;github.com/grafana/grafonnet/gen/grafonnet-latest/main.libsonnet&#39;;
</span></span></span><span class="line"><span class="cl"><span class="sd">      g.dashboard.new(&#39;API Latency&#39;)
</span></span></span><span class="line"><span class="cl"><span class="sd">      + g.dashboard.withUid(&#39;api-latency&#39;)</span><span class="w">
</span></span></span></code></pre></div><p>Everything downstream is unchanged — it reconciles and publishes an
<code>ExternalArtifact</code> exactly as in Steps 4–5; only the source of the library
differs.</p>
<h2 id="handoff-reconcile-the-dashboard-into-grafana">Handoff: reconcile the dashboard into Grafana</h2>
<p>The published <code>ExternalArtifact</code> is now ready for the grafana-operator to
consume. The grafana-operator reconciles a JaaS-published dashboard into Grafana
through a <code>GrafanaDashboard</code> CR that references the artifact. That configuration
— the <code>GrafanaDashboard</code> resource, the datasource and folder wiring, and the
<code>Grafana</code> instance — lives on the grafana-operator&rsquo;s own documentation:</p>
<ul>
<li><strong>grafana-operator JaaS example:</strong>
<a href="https://grafana.github.io/grafana-operator/docs/examples/dashboard/jaas/readme/">https://grafana.github.io/grafana-operator/docs/examples/dashboard/jaas/readme/</a>
</li>
<li><strong>grafana-operator project:</strong> <a href="https://grafana.github.io/grafana-operator/">https://grafana.github.io/grafana-operator/</a>
</li>
</ul>
<p>Follow that example for the Grafana side; it picks up exactly where this
tutorial leaves off — at the published <code>ExternalArtifact</code>.</p>
<h2 id="where-to-go-next">Where to go next</h2>
<ul>
<li><a href="/usage/jsonnet-libraries/">Jsonnet libraries</a>
 — serve the full grafonnet
tree as a <code>JsonnetLibrary</code> backed by an OCIRepository, with the empty-<code>path</code>
whole-vendor-tree pattern.</li>
<li><a href="/usage/snippet-sources/">Snippet sources</a>
 — back the dashboard with a
<code>GitRepository</code> or OCIRepository instead of inline <code>spec.files</code>, and point
<code>spec.entryFile</code> at one dashboard in a multi-dashboard tree.</li>
</ul>
]]></content><category scheme="https://jaas.projects.metio.wtf/tags/grafana" term="grafana" label="grafana"/><category scheme="https://jaas.projects.metio.wtf/tags/grafonnet" term="grafonnet" label="grafonnet"/><category scheme="https://jaas.projects.metio.wtf/tags/externalartifact" term="externalartifact" label="externalartifact"/></entry><entry><title type="html">Helm chart values</title><link href="https://jaas.projects.metio.wtf/installation/helm-values/?utm_source=atom_feed" rel="alternate" type="text/html"/><link href="https://jaas.projects.metio.wtf/installation/configuration/?utm_source=atom_feed" rel="related" type="text/html" title="Configuration reference"/><link href="https://jaas.projects.metio.wtf/installation/kubernetes/?utm_source=atom_feed" rel="related" type="text/html" title="Kubernetes"/><link href="https://jaas.projects.metio.wtf/usage/joi-images/?utm_source=atom_feed" rel="related" type="text/html" title="JOI images"/><link href="https://jaas.projects.metio.wtf/installation/operations/?utm_source=atom_feed" rel="related" type="text/html" title="Operations"/><link href="https://jaas.projects.metio.wtf/installation/production/?utm_source=atom_feed" rel="related" type="text/html" title="Production"/><id>https://jaas.projects.metio.wtf/installation/helm-values/</id><published>0001-01-01T00:00:00+00:00</published><updated>2026-06-17T16:04:27+02:00</updated><content type="html"><![CDATA[<blockquote>Complete reference for every value the jaas and joi Helm charts expose, generated from each chart&rsquo;s values.yaml.</blockquote><p>The jaas Helm chart lives in the
<a href="https://github.com/metio/helm-charts/tree/main/charts/jaas">metio/helm-charts</a>

monorepo and is published at <code>oci://ghcr.io/metio/helm-charts/jaas</code>. The tables
below are generated from each chart&rsquo;s <code>values.yaml</code>, so they track the chart&rsquo;s
current values rather than a hand-maintained copy.</p>
<p>For how the values map onto the binary&rsquo;s runtime behaviour, see the
<a href="/installation/configuration/">Configuration reference</a>
 — every <code>arguments.*</code>
value drives the corresponding <code>--flag</code>.</p>
<h2 id="jaas-chart">jaas chart</h2>
<table>
  <thead>
    <tr><th>Value</th><th>Type</th><th>Default</th><th>Description</th></tr>
  </thead>
  <tbody>
    <tr>
      <td><code>additionalLibraries</code></td>
      <td><code>object|null</code></td>
      <td><em>(empty)</em></td>
      <td>Additional library OCI volumes to mount. The key is the name of the directory and the value is an OCI volume that will be mounted beneath .Values.paths.libraries, e.g., the commented example below would result in all files from &#39;ghcr.io/metio/something:latest&#39; to be mounted at &#39;/srv/libraries/something/&#39; (using the default configuration)  OCI mounts suit large, independently-released library bundles pinned by digest. For shared libraries you want to manage as ordinary manifests — published and updated via kubectl/GitOps without an operator restart — apply a namespaced JsonnetLibrary instead and reference it from a snippet&#39;s spec.libraries.</td>
    </tr>
    <tr>
      <td><code>arguments</code></td>
      <td><code>object</code></td>
      <td><em>(empty)</em></td>
      <td>CLI flags for the jaas binary. These arguments map directly to their corresponding CLI flag.</td>
    </tr>
    <tr>
      <td><code>arguments.evaluationTimeout</code></td>
      <td><code>string</code></td>
      <td><code>5s</code></td>
      <td>Per-request go-jsonnet evaluation timeout. 0 disables.</td>
    </tr>
    <tr>
      <td><code>arguments.jsonnetEndpointPath</code></td>
      <td><code>string</code></td>
      <td><code>jsonnet</code></td>
      <td>URL path segment the jsonnet handler is mounted under (GET /&lt;path&gt;/...).</td>
    </tr>
    <tr>
      <td><code>arguments.listenAddress</code></td>
      <td><code>string</code></td>
      <td><code>::</code></td>
      <td>Bind address for the jsonnet HTTP and management servers. `::` listens on every IPv6 interface and — because Go opens IPV6_V6ONLY=0 by default on Linux — also accepts IPv4 connections via v4-mapped addresses. Use `0.0.0.0` to constrain to IPv4 on clusters that don&#39;t support dual-stack pods.</td>
    </tr>
    <tr>
      <td><code>arguments.logFormat</code></td>
      <td><code>string</code></td>
      <td><code>json</code></td>
      <td>Log output format for the jaas binary (json, text).</td>
    </tr>
    <tr>
      <td><code>arguments.logLevel</code></td>
      <td><code>string</code></td>
      <td><code>info</code></td>
      <td>Log verbosity for the jaas binary (debug, info, warn, error).</td>
    </tr>
    <tr>
      <td><code>arguments.managementListenAddress</code></td>
      <td><code>string</code></td>
      <td><code>::</code></td>
      <td>Bind address for the management (probes) HTTP server. Same dual-stack semantics as listenAddress.</td>
    </tr>
    <tr>
      <td><code>arguments.managementReadTimeout</code></td>
      <td><code>string</code></td>
      <td><code>10s</code></td>
      <td>Read timeout on the management (probes) HTTP server.</td>
    </tr>
    <tr>
      <td><code>arguments.managementWriteTimeout</code></td>
      <td><code>string</code></td>
      <td><code>10s</code></td>
      <td>Write timeout on the management (probes) HTTP server.</td>
    </tr>
    <tr>
      <td><code>arguments.maxConcurrentEvals</code></td>
      <td><code>integer</code></td>
      <td><code>32</code></td>
      <td>Maximum in-flight Jsonnet evaluations across both HTTP and operator paths. Excess requests return 503 (HTTP) or RequeueAfter (operator) with EvalUnavailable backpressure events. Sized to bound worst-case goroutine pile-up when a runaway snippet&#39;s synchronous eval outlives every caller&#39;s ctx — the cap turns &#34;unbounded leak&#34; into a tunable backpressure boundary. 0 disables the gate; the operator default (process-side) is max(GOMAXPROCS*4, 16).</td>
    </tr>
    <tr>
      <td><code>arguments.maxStack</code></td>
      <td><code>integer</code></td>
      <td><code>500</code></td>
      <td>go-jsonnet stack-depth ceiling. Guards against unbounded recursion.</td>
    </tr>
    <tr>
      <td><code>arguments.readTimeout</code></td>
      <td><code>string</code></td>
      <td><code>10s</code></td>
      <td>Read timeout on the jsonnet HTTP server.</td>
    </tr>
    <tr>
      <td><code>arguments.shutdownDelay</code></td>
      <td><code>string</code></td>
      <td><code>5s</code></td>
      <td>Grace period readiness stays false before in-flight requests are aborted on SIGTERM, so endpoint controllers can drain the pod first.</td>
    </tr>
    <tr>
      <td><code>arguments.writeTimeout</code></td>
      <td><code>string</code></td>
      <td><code>10s</code></td>
      <td>Write timeout on the jsonnet HTTP server.</td>
    </tr>
    <tr>
      <td><code>crds</code></td>
      <td><code>object</code></td>
      <td><em>(empty)</em></td>
      <td>CRD lifecycle. The chart ships the controller&#39;s CRDs under templates/ so a `helm upgrade` applies schema changes automatically. Set create=false to manage them out-of-band — e.g. pre-installed cluster-wide, or in CI where the same chart is installed once per values file and a kept cluster-scoped CRD owned by the first release can&#39;t be re-adopted by the next.</td>
    </tr>
    <tr>
      <td><code>crds.create</code></td>
      <td><code>boolean</code></td>
      <td><code>true</code></td>
      <td></td>
    </tr>
    <tr>
      <td><code>deployment</code></td>
      <td><code>object</code></td>
      <td><em>(empty)</em></td>
      <td>Fine tune the Deployment resource.</td>
    </tr>
    <tr>
      <td><code>deployment.additionalLabels</code></td>
      <td><code>object</code></td>
      <td><em>(empty)</em></td>
      <td>Extra labels merged onto the Deployment metadata.</td>
    </tr>
    <tr>
      <td><code>deployment.revisionHistoryLimit</code></td>
      <td><code>integer</code></td>
      <td><code>1</code></td>
      <td>spec.revisionHistoryLimit on the Deployment.</td>
    </tr>
    <tr>
      <td><code>externalVariables</code></td>
      <td><code>object|null</code></td>
      <td><em>(empty)</em></td>
      <td>External variables (std.extVar). They will be added as JAAS_EXT_VAR_your_name and are available as your_name in your Jsonnet snippets.</td>
    </tr>
    <tr>
      <td><code>externalVariablesFrom</code></td>
      <td><code>object</code></td>
      <td><em>(empty)</em></td>
      <td>External variables loaded from ConfigMaps or Secrets. Each array expects the name of either ConfigMaps or Secrets. Only those values in ConfigMaps and Secrets that start with JAAS_EXT_VAR will be available as external variable in your Jsonnet snippets.</td>
    </tr>
    <tr>
      <td><code>externalVariablesFrom.configMaps</code></td>
      <td><code>array</code></td>
      <td><em>(empty)</em></td>
      <td>ConfigMap names to source JAAS_EXT_VAR_* env vars from.</td>
    </tr>
    <tr>
      <td><code>externalVariablesFrom.secrets</code></td>
      <td><code>array</code></td>
      <td><em>(empty)</em></td>
      <td>Secret names to source JAAS_EXT_VAR_* env vars from.</td>
    </tr>
    <tr>
      <td><code>global</code></td>
      <td><code>object</code></td>
      <td><em>(empty)</em></td>
      <td>Global values are values that can be accessed from any chart or subchart by exactly the same name.</td>
    </tr>
    <tr>
      <td><code>image</code></td>
      <td><code>object</code></td>
      <td><em>(empty)</em></td>
      <td>Container image for the jaas workload. Overwrite these in case you want to use an internal registry mirror.</td>
    </tr>
    <tr>
      <td><code>image.pullPolicy</code></td>
      <td><code>string</code></td>
      <td><code>IfNotPresent</code></td>
      <td>Pull policy for the jaas container. IfNotPresent is the default — safe for tagged release images. Switch to Always when running off a mutable tag (latest, edge, dev) so each restart re-checks the registry. Never is useful when the image is preloaded into the node (kind, k3d).</td>
    </tr>
    <tr>
      <td><code>image.registry</code></td>
      <td><code>string</code></td>
      <td><code>ghcr.io</code></td>
      <td></td>
    </tr>
    <tr>
      <td><code>image.repository</code></td>
      <td><code>string</code></td>
      <td><code>metio/jaas</code></td>
      <td></td>
    </tr>
    <tr>
      <td><code>image.tag</code></td>
      <td><code>string</code></td>
      <td><em>(empty)</em></td>
      <td>Optional explicit tag override. Defaults to .Chart.AppVersion (the chart&#39;s released version). Useful when running a custom build:   --set-string image.tag=dev-abc123</td>
    </tr>
    <tr>
      <td><code>imagePullSecrets</code></td>
      <td><code>array</code></td>
      <td><em>(empty)</em></td>
      <td>Optional pull secrets in case you use a private mirror/image</td>
    </tr>
    <tr>
      <td><code>libraries</code></td>
      <td><code>object</code></td>
      <td><em>(empty)</em></td>
      <td>Well-known jsonnet libraries. All OFF by default — a default install mounts no OCI volumes. Enable per renderer-mode need (the standalone HTTP renderer imports these from pre-baked image bundles). Each enabled library renders a `-library-path` arg, a read-only volumeMount, and an OCI `image:` volume. Static OCI mounts are mutually exclusive with operator.enabled (Flux mode): in Flux mode libraries come from JsonnetLibrary CRs, not these image bundles.</td>
    </tr>
    <tr>
      <td><code>libraries.docsonnet</code></td>
      <td><code>object</code></td>
      <td><em>(empty)</em></td>
      <td>docsonnet library. Mounts a JOI OCI bundle when enabled.</td>
    </tr>
    <tr>
      <td><code>libraries.docsonnet.enabled</code></td>
      <td><code>boolean</code></td>
      <td><code>false</code></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.docsonnet.registry</code></td>
      <td><code>string</code></td>
      <td><code>ghcr.io</code></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.docsonnet.repository</code></td>
      <td><code>string</code></td>
      <td><code>metio/joi-jsonnet-libs-docsonnet</code></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.docsonnet.version</code></td>
      <td><code>string</code></td>
      <td><code>latest</code></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.grafonnet</code></td>
      <td><code>object</code></td>
      <td><em>(empty)</em></td>
      <td>grafonnet library (Grafana dashboards). Mounts a JOI OCI bundle when enabled.</td>
    </tr>
    <tr>
      <td><code>libraries.grafonnet.enabled</code></td>
      <td><code>boolean</code></td>
      <td><code>false</code></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.grafonnet.registry</code></td>
      <td><code>string</code></td>
      <td><code>ghcr.io</code></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.grafonnet.repository</code></td>
      <td><code>string</code></td>
      <td><code>metio/joi-grafana-grafonnet</code></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.grafonnet.version</code></td>
      <td><code>string</code></td>
      <td><code>latest</code></td>
      <td>The JOI images are calendar-tagged (plus a moving `latest`), not tagged by the upstream grafonnet version — there is no `v11.4.0` tag. Match the other libraries and track `latest`; pin to a dated tag (e.g. 2026.6.14) to freeze.</td>
    </tr>
    <tr>
      <td><code>libraries.xtd</code></td>
      <td><code>object</code></td>
      <td><em>(empty)</em></td>
      <td>xtd library. Mounts a JOI OCI bundle when enabled.</td>
    </tr>
    <tr>
      <td><code>libraries.xtd.enabled</code></td>
      <td><code>boolean</code></td>
      <td><code>false</code></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.xtd.registry</code></td>
      <td><code>string</code></td>
      <td><code>ghcr.io</code></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.xtd.repository</code></td>
      <td><code>string</code></td>
      <td><code>metio/joi-jsonnet-libs-xtd</code></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.xtd.version</code></td>
      <td><code>string</code></td>
      <td><code>latest</code></td>
      <td></td>
    </tr>
    <tr>
      <td><code>namespace</code></td>
      <td><code>object</code></td>
      <td><em>(empty)</em></td>
      <td>Opt-in install-namespace render with Pod Security Standards labels. Default-off so the chart doesn&#39;t claim ownership of a pre-existing namespace operators created via kubectl / Argo. Enable for fresh installs that want the namespace&#39;s PSS posture baked into the chart.</td>
    </tr>
    <tr>
      <td><code>namespace.additionalLabels</code></td>
      <td><code>object</code></td>
      <td><em>(empty)</em></td>
      <td>Extra labels on the rendered Namespace.</td>
    </tr>
    <tr>
      <td><code>namespace.annotations</code></td>
      <td><code>object</code></td>
      <td><em>(empty)</em></td>
      <td>Extra annotations on the rendered Namespace.</td>
    </tr>
    <tr>
      <td><code>namespace.create</code></td>
      <td><code>boolean</code></td>
      <td><code>false</code></td>
      <td>Whether the chart renders (and owns) its install Namespace.</td>
    </tr>
    <tr>
      <td><code>namespace.podSecurity</code></td>
      <td><code>object</code></td>
      <td><em>(empty)</em></td>
      <td>Pod Security Standards admission labels stamped on the namespace.</td>
    </tr>
    <tr>
      <td><code>namespace.podSecurity.audit</code></td>
      <td><code>string</code></td>
      <td><code>restricted</code></td>
      <td>PSS audit level.</td>
    </tr>
    <tr>
      <td><code>namespace.podSecurity.auditVersion</code></td>
      <td><code>string</code></td>
      <td><code>latest</code></td>
      <td></td>
    </tr>
    <tr>
      <td><code>namespace.podSecurity.enforce</code></td>
      <td><code>string</code></td>
      <td><code>restricted</code></td>
      <td>`restricted` is the strictest PSS level. Image volumes (used by the chart for snippet/library mounts) are restricted-compliant from Kubernetes 1.33 onward; clusters on 1.32 or older must drop this to `baseline`.</td>
    </tr>
    <tr>
      <td><code>namespace.podSecurity.enforceVersion</code></td>
      <td><code>string</code></td>
      <td><code>latest</code></td>
      <td></td>
    </tr>
    <tr>
      <td><code>namespace.podSecurity.warn</code></td>
      <td><code>string</code></td>
      <td><code>restricted</code></td>
      <td>PSS warn level.</td>
    </tr>
    <tr>
      <td><code>namespace.podSecurity.warnVersion</code></td>
      <td><code>string</code></td>
      <td><code>latest</code></td>
      <td></td>
    </tr>
    <tr>
      <td><code>networkPolicy</code></td>
      <td><code>object</code></td>
      <td><em>(empty)</em></td>
      <td>Opt-in NetworkPolicy for the jaas pod (storage port lockdown &#43; per-port ingress controls).</td>
    </tr>
    <tr>
      <td><code>networkPolicy.additionalIngress</code></td>
      <td><code>array</code></td>
      <td><em>(empty)</em></td>
      <td>Free-form additional ingress rules merged into the policy verbatim.</td>
    </tr>
    <tr>
      <td><code>networkPolicy.calico</code></td>
      <td><code>object</code></td>
      <td><em>(empty)</em></td>
      <td>Calico engine (engine=calico) raw passthrough. Entries are merged verbatim into the projectcalico.org NetworkPolicy&#39;s spec.ingress / spec.egress.</td>
    </tr>
    <tr>
      <td><code>networkPolicy.calico.egress</code></td>
      <td><code>array</code></td>
      <td><em>(empty)</em></td>
      <td>Extra spec.egress rules merged verbatim (Calico NetworkPolicy schema).</td>
    </tr>
    <tr>
      <td><code>networkPolicy.calico.ingress</code></td>
      <td><code>array</code></td>
      <td><em>(empty)</em></td>
      <td>Extra spec.ingress rules merged verbatim (Calico NetworkPolicy schema).</td>
    </tr>
    <tr>
      <td><code>networkPolicy.cilium</code></td>
      <td><code>object</code></td>
      <td><em>(empty)</em></td>
      <td>Cilium engine (engine=cilium) raw passthrough. Entries are merged verbatim into the CiliumNetworkPolicy&#39;s spec.ingress / spec.egress. This is how you tighten the cilium policy — e.g. add fromEndpoints to the ingress rules, or additional toEndpoints / toFQDNs egress.</td>
    </tr>
    <tr>
      <td><code>networkPolicy.cilium.egress</code></td>
      <td><code>array</code></td>
      <td><em>(empty)</em></td>
      <td>Extra spec.egress rules merged verbatim (CiliumNetworkPolicy schema).</td>
    </tr>
    <tr>
      <td><code>networkPolicy.cilium.ingress</code></td>
      <td><code>array</code></td>
      <td><em>(empty)</em></td>
      <td>Extra spec.ingress rules merged verbatim (CiliumNetworkPolicy schema).</td>
    </tr>
    <tr>
      <td><code>networkPolicy.clusterNetworkPolicy</code></td>
      <td><code>object</code></td>
      <td><em>(empty)</em></td>
      <td>ClusterNetworkPolicy engine (engine=clusterNetworkPolicy) raw passthrough. Entries are merged verbatim into the policy.networking.k8s.io ClusterNetworkPolicy&#39;s spec.ingress / spec.egress.</td>
    </tr>
    <tr>
      <td><code>networkPolicy.clusterNetworkPolicy.egress</code></td>
      <td><code>array</code></td>
      <td><em>(empty)</em></td>
      <td>Extra spec.egress rules merged verbatim (ClusterNetworkPolicy schema).</td>
    </tr>
    <tr>
      <td><code>networkPolicy.clusterNetworkPolicy.ingress</code></td>
      <td><code>array</code></td>
      <td><em>(empty)</em></td>
      <td>Extra spec.ingress rules merged verbatim (ClusterNetworkPolicy schema).</td>
    </tr>
    <tr>
      <td><code>networkPolicy.clusterNetworkPolicy.priority</code></td>
      <td><code>integer</code></td>
      <td><code>1000</code></td>
      <td>spec.priority on the ClusterNetworkPolicy (lower wins within the tier).</td>
    </tr>
    <tr>
      <td><code>networkPolicy.defaultDeny</code></td>
      <td><code>object</code></td>
      <td><em>(empty)</em></td>
      <td>Render a namespace-wide default-deny so EVERY pod in the namespace is denied by default and the per-workload allowlists are the only exceptions (zero-trust namespace). OFF by default because it also denies co-located workloads — enable only when JaaS has its own namespace. Off keeps a pod-scoped setup: only JaaS&#39;s pods are locked down, neighbours untouched.</td>
    </tr>
    <tr>
      <td><code>networkPolicy.defaultDeny.enabled</code></td>
      <td><code>boolean</code></td>
      <td><code>false</code></td>
      <td></td>
    </tr>
    <tr>
      <td><code>networkPolicy.defaultDeny.order</code></td>
      <td><code>integer</code></td>
      <td><code>2000</code></td>
      <td>Calico/ClusterNetworkPolicy sort key for the deny-all. It must rank the deny-all AFTER the per-workload allowlists so the allowlists win: Calico evaluates lower order first (the allowlist policies carry no order = lowest precedence, so this defaults high), and ClusterNetworkPolicy gives lower priority numbers higher precedence (the allowlist priority defaults to networkPolicy.clusterNetworkPolicy.priority, so this defaults higher). The kubernetes and cilium engines have no precedence knob — deny &#43; allow simply combine additively, allow wins.</td>
    </tr>
    <tr>
      <td><code>networkPolicy.egress</code></td>
      <td><code>object</code></td>
      <td><em>(empty)</em></td>
      <td></td>
    </tr>
    <tr>
      <td><code>networkPolicy.egress.dns</code></td>
      <td><code>boolean</code></td>
      <td><code>true</code></td>
      <td>When egress is enabled, allow DNS (UDP&#43;TCP 53) to the cluster DNS namespace.</td>
    </tr>
    <tr>
      <td><code>networkPolicy.egress.dnsNamespace</code></td>
      <td><code>string</code></td>
      <td><code>kube-system</code></td>
      <td>Namespace of the cluster DNS service (matched by kubernetes.io/metadata.name).</td>
    </tr>
    <tr>
      <td><code>networkPolicy.egress.enabled</code></td>
      <td><code>boolean</code></td>
      <td><code>false</code></td>
      <td>Render egress rules. Adds Egress to policyTypes, so everything not explicitly allowed is denied. Off by default: enable only after allowing everything the pod needs (DNS, the kube-apiserver, source-controller, S3, the OTLP collector) via egress.to — otherwise the operator loses cluster access.</td>
    </tr>
    <tr>
      <td><code>networkPolicy.egress.to</code></td>
      <td><code>array</code></td>
      <td><em>(empty)</em></td>
      <td>Additional egress peers merged verbatim into spec.egress. This is where you allow the kube-apiserver (an ipBlock CIDR), the flux-system namespace (source-controller), and external endpoints (S3, OTLP). NetworkPolicy cannot select the apiserver by label, so it must be an ipBlock here.</td>
    </tr>
    <tr>
      <td><code>networkPolicy.enabled</code></td>
      <td><code>boolean</code></td>
      <td><code>false</code></td>
      <td>Opt-in. When enabled, a NetworkPolicy targets the jaas pod and locks the storage HTTP port down to the configured consumer selector (defaults to any pod in flux-system — kustomize-controller / helm-controller). Other ports stay open so kubelet probes, the jsonnet HTTP path, and the webhook traffic from kube-apiserver continue to work. Leave OFF on initial install if the cluster has no NetworkPolicy controller (Calico/Cilium/etc.) — without one, the policy is silently inert; with one, default-deny semantics apply to everything not listed here.</td>
    </tr>
    <tr>
      <td><code>networkPolicy.engine</code></td>
      <td><code>string</code></td>
      <td><code>kubernetes</code></td>
      <td>Policy engine to render for. kubernetes (default) emits a vanilla networking.k8s.io NetworkPolicy. cilium / calico / clusterNetworkPolicy emit the engine-native equivalent instead (CiliumNetworkPolicy, projectcalico.org NetworkPolicy, or policy.networking.k8s.io ClusterNetworkPolicy). The per-port .from knobs below apply to the kubernetes engine only; alternative engines are pod-scoped allow-all on the required ports and tighten via their native passthrough lists (cilium/calico/clusterNetworkPolicy .ingress / .egress).</td>
    </tr>
    <tr>
      <td><code>networkPolicy.http</code></td>
      <td><code>object</code></td>
      <td><em>(empty)</em></td>
      <td>Restrict sources allowed to hit the jsonnet HTTP endpoint. Empty list allows everything (typical when an Ingress fronts the service).</td>
    </tr>
    <tr>
      <td><code>networkPolicy.http.from</code></td>
      <td><code>array</code></td>
      <td><em>(empty)</em></td>
      <td></td>
    </tr>
    <tr>
      <td><code>networkPolicy.management</code></td>
      <td><code>object</code></td>
      <td><em>(empty)</em></td>
      <td>Restrict sources allowed to hit the management probes. Empty list allows everything — kubelet probes source from the node IP.</td>
    </tr>
    <tr>
      <td><code>networkPolicy.management.from</code></td>
      <td><code>array</code></td>
      <td><em>(empty)</em></td>
      <td></td>
    </tr>
    <tr>
      <td><code>networkPolicy.metrics</code></td>
      <td><code>object</code></td>
      <td><em>(empty)</em></td>
      <td>Sources allowed to scrape the operator metrics port. Empty = all (typically your Prometheus). Only rendered in operator mode with metrics enabled.</td>
    </tr>
    <tr>
      <td><code>networkPolicy.metrics.from</code></td>
      <td><code>array</code></td>
      <td><em>(empty)</em></td>
      <td></td>
    </tr>
    <tr>
      <td><code>networkPolicy.storage</code></td>
      <td><code>object</code></td>
      <td><em>(empty)</em></td>
      <td>Sources allowed to read published ExternalArtifact tarballs from the storage HTTP port. The consumers are the Flux controllers that dereference ExternalArtifact.status.artifact.url — kustomize-controller and helm-controller (both in flux-system) — NOT source-controller, which only produces its own typed artifacts. Defaults to any pod in flux-system so the stock Flux consumers work out of the box. Add an entry per extra consumer namespace (e.g. stageset-controller in stageset-system):   - namespaceSelector:       matchLabels:         kubernetes.io/metadata.name: stageset-system</td>
    </tr>
    <tr>
      <td><code>networkPolicy.storage.from</code></td>
      <td><code>array</code></td>
      <td><em>(empty)</em></td>
      <td></td>
    </tr>
    <tr>
      <td><code>networkPolicy.webhook</code></td>
      <td><code>object</code></td>
      <td><em>(empty)</em></td>
      <td>Restrict sources allowed to dial the webhook. Empty list allows everything — kube-apiserver cannot be expressed as a podSelector.</td>
    </tr>
    <tr>
      <td><code>networkPolicy.webhook.from</code></td>
      <td><code>array</code></td>
      <td><em>(empty)</em></td>
      <td></td>
    </tr>
    <tr>
      <td><code>operator</code></td>
      <td><code>object</code></td>
      <td><em>(empty)</em></td>
      <td>Operator mode — opt-in. When enabled, jaas runs alongside its HTTP path as a Kubernetes operator that watches JsonnetSnippet / JsonnetLibrary CRs and publishes evaluated results as Flux ExternalArtifact resources.</td>
    </tr>
    <tr>
      <td><code>operator.cleanupOnDelete</code></td>
      <td><code>object</code></td>
      <td><em>(empty)</em></td>
      <td>On `helm uninstall`, run a pre-delete Job that bulk-deletes every JsonnetSnippet so the operator&#39;s finalizer drops the published ExternalArtifact &#43; tarball BEFORE the operator pod itself is removed. Without this, the operator goes away first and downstream Flux consumers are left referencing orphaned ExternalArtifacts.  Disable when uninstall is driven by something other than Helm (e.g., ArgoCD with hooks off) — the manual cleanup pattern is:    kubectl delete jsonnetsnippet --all -A --wait=true --timeout=2m   helm uninstall jaas -n &lt;ns&gt;</td>
    </tr>
    <tr>
      <td><code>operator.cleanupOnDelete.activeDeadlineSeconds</code></td>
      <td><code>integer</code></td>
      <td><code>600</code></td>
      <td>Job-level safety net. activeDeadlineSeconds caps the Job&#39;s total runtime; if the operator is wedged, Helm won&#39;t be blocked forever.</td>
    </tr>
    <tr>
      <td><code>operator.cleanupOnDelete.backoffLimit</code></td>
      <td><code>integer</code></td>
      <td><code>2</code></td>
      <td>backoffLimit on the pre-delete Job.</td>
    </tr>
    <tr>
      <td><code>operator.cleanupOnDelete.enabled</code></td>
      <td><code>boolean</code></td>
      <td><code>true</code></td>
      <td></td>
    </tr>
    <tr>
      <td><code>operator.cleanupOnDelete.image</code></td>
      <td><code>object</code></td>
      <td><em>(empty)</em></td>
      <td>Image for the pre-delete cleanup Job (a kubectl image).</td>
    </tr>
    <tr>
      <td><code>operator.cleanupOnDelete.image.pullPolicy</code></td>
      <td><code>string</code></td>
      <td><code>IfNotPresent</code></td>
      <td></td>
    </tr>
    <tr>
      <td><code>operator.cleanupOnDelete.image.registry</code></td>
      <td><code>string</code></td>
      <td><code>registry.k8s.io</code></td>
      <td></td>
    </tr>
    <tr>
      <td><code>operator.cleanupOnDelete.image.repository</code></td>
      <td><code>string</code></td>
      <td><code>kubectl</code></td>
      <td></td>
    </tr>
    <tr>
      <td><code>operator.cleanupOnDelete.image.tag</code></td>
      <td><code>string</code></td>
      <td><code>v1.34.0</code></td>
      <td></td>
    </tr>
    <tr>
      <td><code>operator.cleanupOnDelete.kubectlTimeout</code></td>
      <td><code>string</code></td>
      <td><code>2m</code></td>
      <td>Maximum wall-clock time the pre-delete Job waits for snippet finalizers to run before it strips them forcibly and proceeds. Bigger when snippets publish to slow S3 endpoints.</td>
    </tr>
    <tr>
      <td><code>operator.cleanupOnDelete.resources</code></td>
      <td><code>object</code></td>
      <td><em>(empty)</em></td>
      <td>Resource requests/limits for the cleanup Job container.</td>
    </tr>
    <tr>
      <td><code>operator.cleanupOnDelete.resources.cpu</code></td>
      <td><code>string</code></td>
      <td><code>100m</code></td>
      <td></td>
    </tr>
    <tr>
      <td><code>operator.cleanupOnDelete.resources.ephemeralStorage</code></td>
      <td><code>string</code></td>
      <td><code>16Mi</code></td>
      <td>Ephemeral-storage requests/limits. The cleanup Job runs a few `kubectl` calls then exits — disk usage is negligible. Sized small to satisfy kube-score&#39;s resource-completeness check without crowding cluster ephemeral-storage budgets.</td>
    </tr>
    <tr>
      <td><code>operator.cleanupOnDelete.resources.memory</code></td>
      <td><code>string</code></td>
      <td><code>64Mi</code></td>
      <td></td>
    </tr>
    <tr>
      <td><code>operator.defaultServiceAccount</code></td>
      <td><code>string</code></td>
      <td><em>(empty)</em></td>
      <td>The default ServiceAccount the operator impersonates when a snippet does not specify spec.serviceAccountName. Snippets without an effective SA are rejected at reconcile time.</td>
    </tr>
    <tr>
      <td><code>operator.enabled</code></td>
      <td><code>boolean</code></td>
      <td><code>false</code></td>
      <td></td>
    </tr>
    <tr>
      <td><code>operator.extVars</code></td>
      <td><code>object</code></td>
      <td><em>(empty)</em></td>
      <td>Operator-level external variables. Keys here are non-overridable: a JsonnetSnippet whose spec.externalVariables names any of these keys is rejected at admission (and as a fallback at reconcile time).</td>
    </tr>
    <tr>
      <td><code>operator.labelSelector</code></td>
      <td><code>string</code></td>
      <td><em>(empty)</em></td>
      <td>Optional label selector that narrows which CRs the operator watches. Leave empty to watch every CR in the cluster.</td>
    </tr>
    <tr>
      <td><code>operator.leaderElection</code></td>
      <td><code>object</code></td>
      <td><em>(empty)</em></td>
      <td>Leader election guards against two operator replicas double-reconciling the same JsonnetSnippet. With replicas.max: 1 (the chart default) only one replica ever runs, so the lock is just defensive — but operators who scale the Deployment up (rolling restarts, blue-green) MUST keep this on. Off is supported (single replica only) but unsafe to scale.</td>
    </tr>
    <tr>
      <td><code>operator.leaderElection.enabled</code></td>
      <td><code>boolean</code></td>
      <td><code>true</code></td>
      <td></td>
    </tr>
    <tr>
      <td><code>operator.leaderElection.id</code></td>
      <td><code>string</code></td>
      <td><em>(empty)</em></td>
      <td>Lease object name. Defaults to &#34;&lt;release name&gt;-operator&#34; so multiple JaaS installs in the same namespace don&#39;t fight over a shared lease.</td>
    </tr>
    <tr>
      <td><code>operator.maxWithdrawWait</code></td>
      <td><code>string</code></td>
      <td><code>1h</code></td>
      <td>Bound the time a deleted JsonnetSnippet&#39;s finalizer can hold while Publisher.Withdraw keeps failing. Past this, the operator emits a Warning WithdrawForced event, drops the finalizer, and lets the snippet be garbage-collected — possibly leaving an orphan tarball in storage (`&lt;namespace&gt;/&lt;name&gt;/&lt;rev&gt;.tar.gz`). Required so a permanently-broken backend (S3 perma-down, deleted bucket, revoked RBAC) doesn&#39;t block namespace teardown. 1h rides out transient apiserver/S3 incidents while bounding the wait.</td>
    </tr>
    <tr>
      <td><code>operator.metrics</code></td>
      <td><code>object</code></td>
      <td><em>(empty)</em></td>
      <td>Controller-runtime exposes a Prometheus metrics endpoint covering workqueue depth, reconcile latencies, the manager&#39;s REST client, and whatever custom collectors the reconciler registers. The chart wires the endpoint to a dedicated Service; ServiceMonitor rendering is opt-in for clusters running the Prometheus Operator.</td>
    </tr>
    <tr>
      <td><code>operator.metrics.enabled</code></td>
      <td><code>boolean</code></td>
      <td><code>true</code></td>
      <td></td>
    </tr>
    <tr>
      <td><code>operator.metrics.prometheusRule</code></td>
      <td><code>object</code></td>
      <td><em>(empty)</em></td>
      <td>Opt-in PrometheusRule shipping a starter set of alerts on the operator&#39;s custom metrics plus a handful of standard controller-runtime signals. Requires the Prometheus Operator&#39;s `monitoring.coreos.com/v1` API to be installed in the cluster. Thresholds default to conservative values — page on sustained failure patterns, not single bad reconciles. Tune per cluster.</td>
    </tr>
    <tr>
      <td><code>operator.metrics.prometheusRule.annotations</code></td>
      <td><code>object</code></td>
      <td><em>(empty)</em></td>
      <td>Extra annotations on the PrometheusRule.</td>
    </tr>
    <tr>
      <td><code>operator.metrics.prometheusRule.enabled</code></td>
      <td><code>boolean</code></td>
      <td><code>false</code></td>
      <td></td>
    </tr>
    <tr>
      <td><code>operator.metrics.prometheusRule.extraAlertLabels</code></td>
      <td><code>object</code></td>
      <td><em>(empty)</em></td>
      <td>Labels merged onto EVERY alert this PrometheusRule renders. Useful for routing all jaas alerts through one Alertmanager receiver (e.g., `team: platform`).</td>
    </tr>
    <tr>
      <td><code>operator.metrics.prometheusRule.extraRules</code></td>
      <td><code>array</code></td>
      <td><em>(empty)</em></td>
      <td>Extra alert rules appended to the rendered PrometheusRule under a separate &#34;jaas-operator-extras&#34; group. Lets operators ship cluster-specific alerts (e.g. custom thresholds on jaas metrics, alerts joined against deployment labels) without forking the chart. The list is rendered verbatim — each entry must be a valid PrometheusRule alert (see Prometheus documentation for the schema).  To silence a built-in alert: raise its threshold under the `thresholds` block above to an impossibly high value. There is no per-built-in disable toggle — the threshold pattern is the recommended path because it surfaces &#34;this alert is intentionally inert&#34; in the chart values rather than hiding the configuration behind a flag list.  Example: extraRules:   - alert: JaaSManySnippetsDown     expr: count(jaas_snippet_reconcile_total{status=&#34;False&#34;}) &gt; 10     for: 5m     labels: { severity: critical, team: platform }     annotations:       summary: &#39;&gt;10 snippets failing across all namespaces&#39;</td>
    </tr>
    <tr>
      <td><code>operator.metrics.prometheusRule.interval</code></td>
      <td><code>string</code></td>
      <td><code>30s</code></td>
      <td>Evaluation interval for every group in the rendered rule.</td>
    </tr>
    <tr>
      <td><code>operator.metrics.prometheusRule.labels</code></td>
      <td><code>object</code></td>
      <td><em>(empty)</em></td>
      <td>Extra labels copied onto the PrometheusRule object itself (typically the labels your Prometheus instance selects on).</td>
    </tr>
    <tr>
      <td><code>operator.metrics.prometheusRule.runbookAnnotationKey</code></td>
      <td><code>string</code></td>
      <td><code>runbook_url</code></td>
      <td>Annotation key the runbook URL lands under on every alert. `runbook_url` is the Prometheus-operator convention (kube-prom, Alertmanager templates, most third-party tooling key off it); `runbook` is common in older internal stacks. Anything goes — the value is just a YAML map key.</td>
    </tr>
    <tr>
      <td><code>operator.metrics.prometheusRule.thresholds</code></td>
      <td><code>object</code></td>
      <td><em>(empty)</em></td>
      <td>All-numeric/duration thresholds in one place so operators can adjust the noise floor without copy-pasting the rule body.</td>
    </tr>
    <tr>
      <td><code>operator.metrics.prometheusRule.thresholds.artifactSizeBytes</code></td>
      <td><code>integer</code></td>
      <td><code>16777216</code></td>
      <td>JaaSSnippetArtifactGrowing: p99 rendered bytes threshold. Defaults to 16 MiB — well below the 64 MiB MaxArchiveBytes default but enough to catch a runaway snippet early.</td>
    </tr>
    <tr>
      <td><code>operator.metrics.prometheusRule.thresholds.artifactSizeDuration</code></td>
      <td><code>string</code></td>
      <td><code>30m</code></td>
      <td></td>
    </tr>
    <tr>
      <td><code>operator.metrics.prometheusRule.thresholds.crdWatchEngagementFailuresDuration</code></td>
      <td><code>string</code></td>
      <td><code>30m</code></td>
      <td></td>
    </tr>
    <tr>
      <td><code>operator.metrics.prometheusRule.thresholds.crdWatchEngagementFailuresPerHour</code></td>
      <td><code>integer</code></td>
      <td><code>1</code></td>
      <td>JaaSCRDWatchEngagementFailing: hourly increase on the CRD watch engagement failure counter. The retry pipeline will eventually re-engage, but a sustained inability to engage means dependent snippets aren&#39;t re-rendering on upstream source events.</td>
    </tr>
    <tr>
      <td><code>operator.metrics.prometheusRule.thresholds.evalLeakedDuration</code></td>
      <td><code>string</code></td>
      <td><code>5m</code></td>
      <td></td>
    </tr>
    <tr>
      <td><code>operator.metrics.prometheusRule.thresholds.evalLeakedFloor</code></td>
      <td><code>integer</code></td>
      <td><code>0</code></td>
      <td>JaaSEvalLeakedGoroutines: floor on the leak gauge before alerting. The gauge counts eval goroutines whose parent ctx already fired — orphans still consuming CPU until their synchronous go-jsonnet call returns naturally. CLAUDE.md&#39;s documented design is &#34;&gt; 0 for 5m&#34; — any sustained leak is textbook runaway-snippet signal. 0 is intentional; the accompanying duration window catches transient spikes without paging.</td>
    </tr>
    <tr>
      <td><code>operator.metrics.prometheusRule.thresholds.evalRejectedDuration</code></td>
      <td><code>string</code></td>
      <td><code>10m</code></td>
      <td></td>
    </tr>
    <tr>
      <td><code>operator.metrics.prometheusRule.thresholds.evalRejectedRate</code></td>
      <td><code>number</code></td>
      <td><code>0.05</code></td>
      <td>JaaSEvalRejected: per-second rate of evaluations the semaphore turned away. 0.05/s ≈ one rejection every 20s, a rate that indicates sustained overload rather than a transient spike a single retry would clear.</td>
    </tr>
    <tr>
      <td><code>operator.metrics.prometheusRule.thresholds.evalSaturationDuration</code></td>
      <td><code>string</code></td>
      <td><code>10m</code></td>
      <td></td>
    </tr>
    <tr>
      <td><code>operator.metrics.prometheusRule.thresholds.evalSaturationRatio</code></td>
      <td><code>number</code></td>
      <td><code>0.9</code></td>
      <td>JaaSEvalSaturation: ratio of in-flight evaluations to the configured cap before alerting. 0.9 means &#34;running at 90 %&#43; of the eval semaphore for evalSaturationDuration&#34; — close enough to the cap that any new burst will start landing rejections. The alert guards on jaas_eval_max_concurrent &gt; 0 so a disabled gate (--max-concurrent-evals=0) never fires.</td>
    </tr>
    <tr>
      <td><code>operator.metrics.prometheusRule.thresholds.forceDropsDuration</code></td>
      <td><code>string</code></td>
      <td><code>5m</code></td>
      <td></td>
    </tr>
    <tr>
      <td><code>operator.metrics.prometheusRule.thresholds.forceDropsPerHour</code></td>
      <td><code>integer</code></td>
      <td><code>0</code></td>
      <td>JaaSForceDropsAccumulating: hourly increase on the force-drop counter. Even a single force-drop means an orphan tarball was left behind; sustained drops mean a permanently-broken pipeline (revoked RBAC on the EA Delete, deleted S3 bucket, missing CRD). The remediation runbook is storage-recovery.md.</td>
    </tr>
    <tr>
      <td><code>operator.metrics.prometheusRule.thresholds.podDownDuration</code></td>
      <td><code>string</code></td>
      <td><code>5m</code></td>
      <td>JaaSOperatorPodDown: how long an operator pod can stay NotReady before paging. Below 5m the alert tends to fire during normal pod rolls.</td>
    </tr>
    <tr>
      <td><code>operator.metrics.prometheusRule.thresholds.reconcileErrorDuration</code></td>
      <td><code>string</code></td>
      <td><code>10m</code></td>
      <td></td>
    </tr>
    <tr>
      <td><code>operator.metrics.prometheusRule.thresholds.reconcileErrorRate</code></td>
      <td><code>number</code></td>
      <td><code>0.1</code></td>
      <td>JaaSSnippetReconcileErrorsHigh: per-snippet Ready=False rate (reconciles per second, 5m window). 0.1/s ≈ one failure every 10s — a clearly stuck snippet, not transient flapping.</td>
    </tr>
    <tr>
      <td><code>operator.metrics.prometheusRule.thresholds.reconcileLatencyDuration</code></td>
      <td><code>string</code></td>
      <td><code>15m</code></td>
      <td></td>
    </tr>
    <tr>
      <td><code>operator.metrics.prometheusRule.thresholds.reconcileLatencySeconds</code></td>
      <td><code>integer</code></td>
      <td><code>30</code></td>
      <td>JaaSReconcileLatencyHigh: reconcile-time p99 ceiling, in seconds. 30s is generous — long enough for an OCI fetch plus eval of a non-trivial snippet, short enough to catch a genuinely stuck reconciler.</td>
    </tr>
    <tr>
      <td><code>operator.metrics.prometheusRule.thresholds.sweepFailuresDuration</code></td>
      <td><code>string</code></td>
      <td><code>30m</code></td>
      <td></td>
    </tr>
    <tr>
      <td><code>operator.metrics.prometheusRule.thresholds.sweepFailuresPerHour</code></td>
      <td><code>integer</code></td>
      <td><code>3</code></td>
      <td>JaaSStorageSweepFailures: per-hour count of failing background sweep passes before alerting. Default 3 absorbs the occasional flake without paging while catching a genuinely degraded backend (chronic disk full, revoked permissions). The sweep runs on operator.storage.sweep.interval (default 10m), so 3/hour ≈ half the passes failing.</td>
    </tr>
    <tr>
      <td><code>operator.metrics.prometheusRule.thresholds.tenantTokenMintFailureDuration</code></td>
      <td><code>string</code></td>
      <td><code>10m</code></td>
      <td></td>
    </tr>
    <tr>
      <td><code>operator.metrics.prometheusRule.thresholds.tenantTokenMintFailureRate</code></td>
      <td><code>number</code></td>
      <td><code>0.01</code></td>
      <td>JaaSTenantTokenMintFailing: per-second mint failure rate across the cluster. Even one mint failure on a (namespace, SA) pair flags revoked `serviceaccounts/token: create` or a deleted SA — actionable immediately.</td>
    </tr>
    <tr>
      <td><code>operator.metrics.prometheusRule.thresholds.webhookCertRenewalFailuresDuration</code></td>
      <td><code>string</code></td>
      <td><code>30m</code></td>
      <td></td>
    </tr>
    <tr>
      <td><code>operator.metrics.prometheusRule.thresholds.webhookCertRenewalFailuresPerHour</code></td>
      <td><code>integer</code></td>
      <td><code>1</code></td>
      <td>JaaSWebhookCertRenewalFailing: hourly increase on the cert renewal failure counter. The Renewer ticks every Validity/3 (typically every few months), so a single failure shouldn&#39;t page; sustained failures across multiple ticks (RBAC drift, CertDir write-perm loss) mean the rotation pipeline is broken and the existing cert&#39;s natural expiry is the deadline.</td>
    </tr>
    <tr>
      <td><code>operator.metrics.prometheusRule.thresholds.workqueueDepth</code></td>
      <td><code>integer</code></td>
      <td><code>50</code></td>
      <td>JaaSControllerWorkqueueDepthHigh: items the workqueue holds before alerting. Default 50 is high enough to absorb a normal roll-out, low enough that genuine apiserver stalls page.</td>
    </tr>
    <tr>
      <td><code>operator.metrics.prometheusRule.thresholds.workqueueDuration</code></td>
      <td><code>string</code></td>
      <td><code>15m</code></td>
      <td></td>
    </tr>
    <tr>
      <td><code>operator.metrics.serviceMonitor</code></td>
      <td><code>object</code></td>
      <td><em>(empty)</em></td>
      <td>Opt-in ServiceMonitor (Prometheus Operator) for the metrics endpoint.</td>
    </tr>
    <tr>
      <td><code>operator.metrics.serviceMonitor.annotations</code></td>
      <td><code>object</code></td>
      <td><em>(empty)</em></td>
      <td>Extra annotations on the ServiceMonitor.</td>
    </tr>
    <tr>
      <td><code>operator.metrics.serviceMonitor.enabled</code></td>
      <td><code>boolean</code></td>
      <td><code>false</code></td>
      <td></td>
    </tr>
    <tr>
      <td><code>operator.metrics.serviceMonitor.interval</code></td>
      <td><code>string</code></td>
      <td><code>30s</code></td>
      <td>Scrape interval the Prometheus instance honors. Match this to your shared scrape config.</td>
    </tr>
    <tr>
      <td><code>operator.metrics.serviceMonitor.labels</code></td>
      <td><code>object</code></td>
      <td><em>(empty)</em></td>
      <td>Extra labels the ServiceMonitor carries (typically the labels your Prometheus instance selects on, e.g., release: kube-prom).</td>
    </tr>
    <tr>
      <td><code>operator.metrics.serviceMonitor.scrapeTimeout</code></td>
      <td><code>string</code></td>
      <td><code>10s</code></td>
      <td>Per-scrape timeout. Must stay strictly less than interval.</td>
    </tr>
    <tr>
      <td><code>operator.noCrossNamespaceRefs</code></td>
      <td><code>boolean</code></td>
      <td><code>true</code></td>
      <td>When true, the operator rejects JsonnetSnippet / library refs that point at a SourceRef in a different namespace.</td>
    </tr>
    <tr>
      <td><code>operator.rbac</code></td>
      <td><code>object</code></td>
      <td><em>(empty)</em></td>
      <td>Opt out of chart-rendered RBAC when external governance owns ClusterRole/RoleBinding shape (ArgoCD app-of-apps, Crossplane, GitOps controller that manages RBAC separately). The operator still expects the named SA to have the verbs documented in README&#39;s &#34;Operator Mode&#34; section; with rbac.create=false you&#39;re responsible for granting them.</td>
    </tr>
    <tr>
      <td><code>operator.rbac.create</code></td>
      <td><code>boolean</code></td>
      <td><code>true</code></td>
      <td></td>
    </tr>
    <tr>
      <td><code>operator.rerenderBurst</code></td>
      <td><code>integer</code></td>
      <td><code>120</code></td>
      <td>Per-snippet token-bucket depth.</td>
    </tr>
    <tr>
      <td><code>operator.rerenderRate</code></td>
      <td><code>string</code></td>
      <td><code>60/min</code></td>
      <td>Per-snippet steady-state re-render budget. N/period where period is one of sec, min, hour. Token-bucket combined with rerenderBurst.</td>
    </tr>
    <tr>
      <td><code>operator.serviceAccount</code></td>
      <td><code>object</code></td>
      <td><em>(empty)</em></td>
      <td>ServiceAccount the operator runs as. When create is true the chart provisions one; otherwise the operator binds against the named SA.</td>
    </tr>
    <tr>
      <td><code>operator.serviceAccount.annotations</code></td>
      <td><code>object</code></td>
      <td><em>(empty)</em></td>
      <td>Annotations to attach to the operator ServiceAccount. Common uses: AWS IRSA (eks.amazonaws.com/role-arn), GCP Workload Identity (iam.gke.io/gcp-service-account), Azure Workload Identity (azure.workload.identity/client-id). These bind cloud-IAM identities to the SA so the operator can access cloud resources without static credentials.</td>
    </tr>
    <tr>
      <td><code>operator.serviceAccount.create</code></td>
      <td><code>boolean</code></td>
      <td><code>true</code></td>
      <td></td>
    </tr>
    <tr>
      <td><code>operator.serviceAccount.name</code></td>
      <td><code>string</code></td>
      <td><code>jaas</code></td>
      <td></td>
    </tr>
    <tr>
      <td><code>operator.storage</code></td>
      <td><code>object</code></td>
      <td><em>(empty)</em></td>
      <td>Artifact storage for published ExternalArtifact tarballs (operator mode).</td>
    </tr>
    <tr>
      <td><code>operator.storage.backend</code></td>
      <td><code>string</code></td>
      <td><code>local</code></td>
      <td>Artifact backend used to persist ExternalArtifact tarballs.   local — emptyDir or PVC mounted into the pod. Simple,     single-pod (RWO PVC) unless a ReadWriteMany storage class     is configured.   s3    — any S3-compatible bucket (AWS S3, MinIO, Ceph RGW,     etc.). The chart-default story for multi-replica HA: every     replica reads from the same bucket; leader election still     gates writes. PVC settings below are ignored when backend=s3. Artifact backend used to persist ExternalArtifact tarballs (local | s3).</td>
    </tr>
    <tr>
      <td><code>operator.storage.baseURL</code></td>
      <td><code>string</code></td>
      <td><em>(empty)</em></td>
      <td>Public URL prefix downstream Flux consumers dereference. Defaults to the in-cluster Service DNS name when empty — operators wiring this up via Ingress should set it explicitly.</td>
    </tr>
    <tr>
      <td><code>operator.storage.gcGrace</code></td>
      <td><code>string</code></td>
      <td><code>5m</code></td>
      <td>Minimum time a superseded artifact revision stays fetchable after being evicted from the keep-set. Closes the pin→fetch race in which a Flux consumer reads ExternalArtifact.status.artifact a moment before the operator garbage-collects the superseded revision and then 404s on the URL. Supersession time is derived from on-disk storage metadata so the window survives operator restarts. Zero restores eager pruning; the snippet teardown path (finalizer Withdraw) is unaffected. See docs/consumers.md for when to leave the default vs raise spec.history for deliberate rollback retention.</td>
    </tr>
    <tr>
      <td><code>operator.storage.headless</code></td>
      <td><code>boolean</code></td>
      <td><code>false</code></td>
      <td>When true, the storage Service is rendered as headless (clusterIP: None) so DNS resolves each pod IP directly. Useful for external sidecars that want stable per-pod targets, or for traffic that should skip kube-proxy. Default ClusterIP otherwise.</td>
    </tr>
    <tr>
      <td><code>operator.storage.listenAddress</code></td>
      <td><code>string</code></td>
      <td><code>::</code></td>
      <td>Bind address for the storage HTTP server. Same dual-stack semantics as arguments.listenAddress: `::` accepts both IPv6 and IPv4-mapped traffic on Linux.</td>
    </tr>
    <tr>
      <td><code>operator.storage.maxArtifactBytes</code></td>
      <td><code>integer</code></td>
      <td><code>0</code></td>
      <td>Cap the rendered artifact size in bytes. Snippets whose rendered output exceeds this fail with ReasonArtifactTooLarge before any disk/S3 write — stops one runaway snippet from filling a shared storage volume. Zero disables.</td>
    </tr>
    <tr>
      <td><code>operator.storage.path</code></td>
      <td><code>string</code></td>
      <td><code>/var/lib/jaas/artifacts</code></td>
      <td>Directory inside the container the operator writes tarballs to. Only consulted when backend=local.</td>
    </tr>
    <tr>
      <td><code>operator.storage.persistence</code></td>
      <td><code>object</code></td>
      <td><em>(empty)</em></td>
      <td>Persist tarballs across pod restarts via a PVC. With persistence OFF (the default) the chart uses an emptyDir, which is lost on pod restart — downstream Flux consumers then see brief 404s while every snippet re-renders. With persistence ON, the PVC survives restarts and downstream consumers see no gap.  PVC access mode is RWO by default, which constrains the chart to a single replica (RWO can only attach to one pod). For multi-replica HA you need an RWX storage class — uncommon in cloud providers.</td>
    </tr>
    <tr>
      <td><code>operator.storage.persistence.accessModes</code></td>
      <td><code>array</code></td>
      <td><em>(empty)</em></td>
      <td>accessModes the PVC declares. ReadWriteOnce is the safe default; ReadWriteMany unlocks multi-replica HA when the StorageClass supports it.</td>
    </tr>
    <tr>
      <td><code>operator.storage.persistence.annotations</code></td>
      <td><code>object</code></td>
      <td><em>(empty)</em></td>
      <td>Extra annotations on the PVC (e.g., CSI volume tags).</td>
    </tr>
    <tr>
      <td><code>operator.storage.persistence.enabled</code></td>
      <td><code>boolean</code></td>
      <td><code>false</code></td>
      <td></td>
    </tr>
    <tr>
      <td><code>operator.storage.persistence.labels</code></td>
      <td><code>object</code></td>
      <td><em>(empty)</em></td>
      <td>Extra labels on the PVC.</td>
    </tr>
    <tr>
      <td><code>operator.storage.persistence.selector</code></td>
      <td><code>object</code></td>
      <td><em>(empty)</em></td>
      <td>Optional PVC selector for matching pre-provisioned volumes.</td>
    </tr>
    <tr>
      <td><code>operator.storage.persistence.size</code></td>
      <td><code>string</code></td>
      <td><code>10Gi</code></td>
      <td>Requested volume size. 10 GiB holds many thousand small tarballs.</td>
    </tr>
    <tr>
      <td><code>operator.storage.persistence.storageClassName</code></td>
      <td><code>string</code></td>
      <td><em>(empty)</em></td>
      <td>storageClassName picks the StorageClass. Empty means &#34;cluster default&#34; — usually right.</td>
    </tr>
    <tr>
      <td><code>operator.storage.readTimeout</code></td>
      <td><code>string</code></td>
      <td><code>30s</code></td>
      <td>Read timeout on the storage HTTP server.</td>
    </tr>
    <tr>
      <td><code>operator.storage.s3</code></td>
      <td><code>object</code></td>
      <td><em>(empty)</em></td>
      <td>S3 backend configuration. Only consulted when backend=s3.</td>
    </tr>
    <tr>
      <td><code>operator.storage.s3.anonymous</code></td>
      <td><code>boolean</code></td>
      <td><code>false</code></td>
      <td>Skip request signing entirely. Public-bucket test mode only.</td>
    </tr>
    <tr>
      <td><code>operator.storage.s3.bucket</code></td>
      <td><code>string</code></td>
      <td><em>(empty)</em></td>
      <td>Bucket the artifacts live in. Must already exist; jaas does not create it. Required.</td>
    </tr>
    <tr>
      <td><code>operator.storage.s3.credentialsSecret</code></td>
      <td><code>object</code></td>
      <td><em>(empty)</em></td>
      <td>Credentials are supplied one of two ways — never inline. Inline values would land on the Pod&#39;s command line (visible in `ps`, in the PodSpec, and in the stored Helm release), so the chart deliberately offers no accessKey/secretKey/sessionToken field:    credentialsSecret.name — an existing Secret carrying     AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, and optionally     AWS_SESSION_TOKEN. It is wired into the pod via     envFrom.secretRef; minio-go&#39;s env chain picks the keys up     automatically since no -s3-* credential flag is passed.    leave credentialsSecret empty — minio-go&#39;s IAM/IRSA discovery     chain takes over (AWS_* env vars, EKS web-identity, EC2     metadata). Bind a cloud identity to the operator&#39;s     ServiceAccount via operator.serviceAccount.annotations.</td>
    </tr>
    <tr>
      <td><code>operator.storage.s3.credentialsSecret.name</code></td>
      <td><code>string</code></td>
      <td><em>(empty)</em></td>
      <td></td>
    </tr>
    <tr>
      <td><code>operator.storage.s3.endpoint</code></td>
      <td><code>string</code></td>
      <td><em>(empty)</em></td>
      <td>Endpoint host:port (e.g. s3.amazonaws.com, minio.minio.svc:9000). Required.</td>
    </tr>
    <tr>
      <td><code>operator.storage.s3.prefix</code></td>
      <td><code>string</code></td>
      <td><em>(empty)</em></td>
      <td>Optional object-key prefix so jaas can coexist with other tenants in one bucket: &lt;prefix&gt;/&lt;namespace&gt;/&lt;snippet&gt;/&lt;rev&gt;.tar.gz</td>
    </tr>
    <tr>
      <td><code>operator.storage.s3.region</code></td>
      <td><code>string</code></td>
      <td><em>(empty)</em></td>
      <td>Region the bucket lives in. Required for AWS multi-region; ignored by most S3-compatible servers.</td>
    </tr>
    <tr>
      <td><code>operator.storage.s3.useSSL</code></td>
      <td><code>boolean</code></td>
      <td><code>true</code></td>
      <td>Talk HTTPS. Disable only for local MinIO over HTTP.</td>
    </tr>
    <tr>
      <td><code>operator.storage.sizeLimit</code></td>
      <td><code>string</code></td>
      <td><code>256Mi</code></td>
      <td>emptyDir size limit. Only consulted when persistence.enabled is false. Tarballs typically are well under 1 MiB; the sizing here is generous so transient bursts don&#39;t OOM the pod.</td>
    </tr>
    <tr>
      <td><code>operator.storage.sweep</code></td>
      <td><code>object</code></td>
      <td><em>(empty)</em></td>
      <td>Periodic GC: sweep orphaned &lt;rev&gt;.tar.gz.tmp residue left by Puts whose process died mid-rename. Local backend only — the S3 backend&#39;s Put is atomic so no .tmp files exist. Set interval to &#34;0s&#34; to disable.</td>
    </tr>
    <tr>
      <td><code>operator.storage.sweep.interval</code></td>
      <td><code>string</code></td>
      <td><code>10m</code></td>
      <td></td>
    </tr>
    <tr>
      <td><code>operator.storage.sweep.maxTmpAge</code></td>
      <td><code>string</code></td>
      <td><code>30m</code></td>
      <td></td>
    </tr>
    <tr>
      <td><code>operator.storage.writeTimeout</code></td>
      <td><code>string</code></td>
      <td><code>5m</code></td>
      <td>Write timeout on the storage HTTP server (--storage-write-timeout): how long a single response is allowed to take before the connection is closed. To cap artifact size, use operator.storage.maxArtifactBytes.</td>
    </tr>
    <tr>
      <td><code>operator.tracing</code></td>
      <td><code>object</code></td>
      <td><em>(empty)</em></td>
      <td>OpenTelemetry tracing for the operator. When endpoint is empty (the default), no spans are emitted and the OTel SDK is in no-op mode. Set the endpoint to ship spans to an OTLP gRPC collector:   tracing:     endpoint: otel-collector.observability.svc:4317     insecure: true</td>
    </tr>
    <tr>
      <td><code>operator.tracing.endpoint</code></td>
      <td><code>string</code></td>
      <td><em>(empty)</em></td>
      <td></td>
    </tr>
    <tr>
      <td><code>operator.tracing.insecure</code></td>
      <td><code>boolean</code></td>
      <td><code>false</code></td>
      <td></td>
    </tr>
    <tr>
      <td><code>operator.tracing.sampleRatio</code></td>
      <td><code>number</code></td>
      <td><code>1</code></td>
      <td></td>
    </tr>
    <tr>
      <td><code>operator.watchNamespaces</code></td>
      <td><code>array</code></td>
      <td><em>(empty)</em></td>
      <td>Namespaces this operator instance scopes its cache to. Empty (the default) means cluster-wide. When set, the manager&#39;s informer cache only observes CRs in these namespaces, AND the rendered RBAC pivots from a single ClusterRoleBinding to one RoleBinding per namespace — multi-tenant operator-instances pattern.</td>
    </tr>
    <tr>
      <td><code>operator.webhook</code></td>
      <td><code>object</code></td>
      <td><em>(empty)</em></td>
      <td>Validating admission webhook (operator mode). Rejects JsonnetSnippets whose spec.externalVariables collide with the operator&#39;s extVars.</td>
    </tr>
    <tr>
      <td><code>operator.webhook.certDir</code></td>
      <td><code>string</code></td>
      <td><code>/tmp/k8s-webhook-server/serving-certs</code></td>
      <td>Directory inside the container where the TLS cert and key land. With certMode=cert-manager this is mounted read-only from the secretName below. With certMode=self-signed it&#39;s a writable emptyDir the operator writes into on startup.</td>
    </tr>
    <tr>
      <td><code>operator.webhook.certManager</code></td>
      <td><code>object</code></td>
      <td><em>(empty)</em></td>
      <td>cert-manager-mode knobs. Only consulted when certMode is &#34;cert-manager&#34;. When self-signed, this block is ignored and no cert-manager API objects are rendered.</td>
    </tr>
    <tr>
      <td><code>operator.webhook.certManager.enabled</code></td>
      <td><code>boolean</code></td>
      <td><code>true</code></td>
      <td></td>
    </tr>
    <tr>
      <td><code>operator.webhook.certManager.issuerRef</code></td>
      <td><code>object</code></td>
      <td><em>(empty)</em></td>
      <td></td>
    </tr>
    <tr>
      <td><code>operator.webhook.certManager.issuerRef.kind</code></td>
      <td><code>string</code></td>
      <td><code>Issuer</code></td>
      <td>Issuer kind: Issuer or ClusterIssuer</td>
    </tr>
    <tr>
      <td><code>operator.webhook.certManager.issuerRef.name</code></td>
      <td><code>string</code></td>
      <td><em>(empty)</em></td>
      <td>Issuer name in the chart&#39;s namespace (Issuer) or cluster (ClusterIssuer). Required when certMode=cert-manager.</td>
    </tr>
    <tr>
      <td><code>operator.webhook.certMode</code></td>
      <td><code>string</code></td>
      <td><code>self-signed</code></td>
      <td>How TLS material for the webhook is provisioned:   cert-manager — chart renders a cert-manager Certificate;     the Secret is mounted into the pod read-only.   self-signed  — operator generates a CA &#43; serving cert at     startup and stamps its own ValidatingWebhookConfiguration     caBundle. Requires the operator to have get/update on the     named VWC (added automatically). Removes the cert-manager     dependency entirely. The default is self-signed because it needs no prerequisites and always works; cert-manager mode only works when cert-manager is installed in the cluster.</td>
    </tr>
    <tr>
      <td><code>operator.webhook.enabled</code></td>
      <td><code>boolean</code></td>
      <td><code>false</code></td>
      <td></td>
    </tr>
    <tr>
      <td><code>operator.webhook.failurePolicy</code></td>
      <td><code>string</code></td>
      <td><code>Fail</code></td>
      <td>Failure policy for the ValidatingWebhookConfiguration. Fail is the safer default — a webhook outage blocks JsonnetSnippet writes cluster-wide until the operator is back. Switch to Ignore only when you&#39;re willing to accept invalid snippets reaching the reconciler. Either way, the reconciler enforces the same ext-var-conflict invariant as a fallback so admission bypass never silently breaks the contract.  During an operator restart (rolling update, crash-loop, eviction) the webhook is briefly unreachable and Fail blocks every JsonnetSnippet create/update for that window — typically &lt;5s with `LeaderElectionReleaseOnCancel: true`. If your CI/GitOps tooling can&#39;t tolerate that, scope the webhook with objectSelector / namespaceSelector to limit the blast radius, or set failurePolicy: Ignore.</td>
    </tr>
    <tr>
      <td><code>operator.webhook.matchConditions</code></td>
      <td><code>array</code></td>
      <td><em>(empty)</em></td>
      <td>matchConditions are CEL expressions evaluated server-side before the webhook is dialed. Lets operators short-circuit admission checks for specific snippet shapes without a round trip. Requires Kubernetes 1.30&#43;.</td>
    </tr>
    <tr>
      <td><code>operator.webhook.namespaceSelector</code></td>
      <td><code>object</code></td>
      <td><em>(empty)</em></td>
      <td>namespaceSelector restricts the webhook to Namespaces carrying the listed labels. A common pattern: opt-in per namespace via a label so the apiserver doesn&#39;t dial the webhook for unrelated namespaces during operator outages. Example:   namespaceSelector:     matchLabels:       jaas.metio.wtf/admission: enforce</td>
    </tr>
    <tr>
      <td><code>operator.webhook.objectSelector</code></td>
      <td><code>object</code></td>
      <td><em>(empty)</em></td>
      <td>objectSelector restricts the webhook to JsonnetSnippets carrying the listed labels. Useful when only some snippets are operator-managed (the rest live in Argo or kustomize). Example:   objectSelector:     matchLabels:       jaas.metio.wtf/managed: &#34;true&#34;</td>
    </tr>
    <tr>
      <td><code>operator.webhook.secretName</code></td>
      <td><code>string</code></td>
      <td><code>jaas-webhook-cert</code></td>
      <td>The Secret the cert lives in. cert-manager writes it; the Deployment mounts it read-only at certDir.</td>
    </tr>
    <tr>
      <td><code>operator.webhook.selfSignedValidity</code></td>
      <td><code>string</code></td>
      <td><code>8760h</code></td>
      <td>Validity of the self-signed serving cert. Operators that want short-lived rotation should use cert-manager instead — the self-signed mode renews on every pod restart, no in-flight rotation. Only consulted when certMode=self-signed.</td>
    </tr>
    <tr>
      <td><code>operator.webhook.sideEffects</code></td>
      <td><code>string</code></td>
      <td><code>None</code></td>
      <td>Side-effect mode for the ValidatingWebhookConfiguration.</td>
    </tr>
    <tr>
      <td><code>paths</code></td>
      <td><code>object</code></td>
      <td><em>(empty)</em></td>
      <td>Filesystem paths inside the container. Modify the paths used, use this in case you run a custom image.</td>
    </tr>
    <tr>
      <td><code>paths.libraries</code></td>
      <td><code>string</code></td>
      <td><code>/srv/libraries</code></td>
      <td>Directory library OCI volumes mount beneath.</td>
    </tr>
    <tr>
      <td><code>paths.snippets</code></td>
      <td><code>string</code></td>
      <td><code>/srv/snippets</code></td>
      <td>Directory snippet OCI volumes mount beneath.</td>
    </tr>
    <tr>
      <td><code>pdb</code></td>
      <td><code>object</code></td>
      <td><em>(empty)</em></td>
      <td>Fine tune the PodDisruptionBudget. Rendered only when replicas.max &gt; replicas.min.</td>
    </tr>
    <tr>
      <td><code>pdb.maxUnavailable</code></td>
      <td><code>integer|string|null</code></td>
      <td><code>1</code></td>
      <td>PDB maxUnavailable. Default 1 caps voluntary disruptions to one pod at a time. Set to null when using minAvailable instead.</td>
    </tr>
    <tr>
      <td><code>pdb.minAvailable</code></td>
      <td><code>integer|string|null</code></td>
      <td><code>null</code></td>
      <td>Disruption budget shape. Exactly one of minAvailable / maxUnavailable is rendered into the PDB spec; setting both is a config error K8s would reject.  Default `maxUnavailable: 1` caps voluntary disruptions to one pod at a time regardless of replica count — the right knob for a rolling restart / node drain story. `minAvailable: N` is the alternative when you want &#34;always keep at least N pods up&#34; instead — useful only when the workload&#39;s headroom is a fixed floor, not the more common &#34;one out at a time&#34;.  Override examples:   pdb: { maxUnavailable: 2 }       # allow draining two nodes in parallel   pdb: { minAvailable: 1, maxUnavailable: null }  # floor of 1 instead   pdb: { minAvailable: &#34;50%&#34;, maxUnavailable: null }  # percentage form PDB minAvailable. Mutually exclusive with maxUnavailable; null leaves it unset.</td>
    </tr>
    <tr>
      <td><code>pdb.unhealthyPodEvictionPolicy</code></td>
      <td><code>string</code></td>
      <td><em>(empty)</em></td>
      <td>spec.unhealthyPodEvictionPolicy on the PDB. Empty defers to the cluster default.</td>
    </tr>
    <tr>
      <td><code>pod</code></td>
      <td><code>object</code></td>
      <td><em>(empty)</em></td>
      <td>Fine tune the managed Pod resources.</td>
    </tr>
    <tr>
      <td><code>pod.additionalLabels</code></td>
      <td><code>object</code></td>
      <td><em>(empty)</em></td>
      <td>Extra labels merged onto the pod template.</td>
    </tr>
    <tr>
      <td><code>ports</code></td>
      <td><code>object</code></td>
      <td><em>(empty)</em></td>
      <td>Container port assignments for the jaas pod.</td>
    </tr>
    <tr>
      <td><code>ports.http</code></td>
      <td><code>integer</code></td>
      <td><code>8080</code></td>
      <td>Used by the /jsonnet endpoint</td>
    </tr>
    <tr>
      <td><code>ports.management</code></td>
      <td><code>integer</code></td>
      <td><code>8081</code></td>
      <td>Used by startup, readiness, and liveness probes</td>
    </tr>
    <tr>
      <td><code>ports.metrics</code></td>
      <td><code>integer</code></td>
      <td><code>8083</code></td>
      <td>Used by controller-runtime&#39;s Prometheus metrics endpoint (operator.enabled only). Defaults to 8083 to avoid the collision with controller-runtime&#39;s own :8080 default and the jsonnet HTTP port. Set to 0 in operator.metrics.enabled to disable entirely.</td>
    </tr>
    <tr>
      <td><code>ports.storage</code></td>
      <td><code>integer</code></td>
      <td><code>8082</code></td>
      <td>Used by the operator&#39;s storage HTTP server (operator.enabled only)</td>
    </tr>
    <tr>
      <td><code>ports.webhook</code></td>
      <td><code>integer</code></td>
      <td><code>9443</code></td>
      <td>Used by the validating admission webhook (operator.webhook.enabled only)</td>
    </tr>
    <tr>
      <td><code>replicas</code></td>
      <td><code>object</code></td>
      <td><em>(empty)</em></td>
      <td>Replica bounds for the Deployment. It is safe to increase the number of replicas.</td>
    </tr>
    <tr>
      <td><code>replicas.max</code></td>
      <td><code>integer</code></td>
      <td><code>1</code></td>
      <td></td>
    </tr>
    <tr>
      <td><code>replicas.min</code></td>
      <td><code>integer</code></td>
      <td><code>1</code></td>
      <td></td>
    </tr>
    <tr>
      <td><code>resources</code></td>
      <td><code>object</code></td>
      <td><em>(empty)</em></td>
      <td>Per-container resource requests/limits. Smallest possible values here, increase if you have more resources to spare.</td>
    </tr>
    <tr>
      <td><code>resources.cpu</code></td>
      <td><code>string</code></td>
      <td><code>32m</code></td>
      <td></td>
    </tr>
    <tr>
      <td><code>resources.ephemeralStorage</code></td>
      <td><code>string</code></td>
      <td><code>10Mi</code></td>
      <td></td>
    </tr>
    <tr>
      <td><code>resources.memory</code></td>
      <td><code>string</code></td>
      <td><code>64Mi</code></td>
      <td></td>
    </tr>
    <tr>
      <td><code>service</code></td>
      <td><code>object</code></td>
      <td><em>(empty)</em></td>
      <td>Fine tune the Service resource.</td>
    </tr>
    <tr>
      <td><code>service.ipFamilies</code></td>
      <td><code>array</code></td>
      <td><em>(empty)</em></td>
      <td>Explicit list of IP families for the Service. Empty defers to the cluster&#39;s default. Example for explicit IPv4&#43;IPv6:   ipFamilies: [IPv4, IPv6] Order matters: the first family becomes the primary ClusterIP.</td>
    </tr>
    <tr>
      <td><code>service.ipFamilyPolicy</code></td>
      <td><code>string</code></td>
      <td><em>(empty)</em></td>
      <td>K8s Service ipFamilyPolicy. Empty leaves the cluster&#39;s default (usually SingleStack). Set to PreferDualStack on dual-stack clusters that may still have single-stack nodes, or RequireDualStack to refuse single-stack clusters at admission.</td>
    </tr>
    <tr>
      <td><code>serviceMesh</code></td>
      <td><code>object</code></td>
      <td><em>(empty)</em></td>
      <td>Opt-in service-mesh L7 authorization &#43; mTLS for the jaas pod. Complementary to networkPolicy, NOT a replacement: networkPolicy is L3/L4 (which pods/IPs may open a connection), serviceMesh is L7/identity (which mesh identities may call which port). Both can be enabled at once and stack additively. Authorizes only the mesh-reachable ports — http (jsonnet), storage (artifacts), and metrics (operator). The webhook (kube-apiserver) and management/probe (kubelet) ports are deliberately left out: those callers speak plain TLS / no mesh identity, and locking them to mesh principals would break admission and health probing.</td>
    </tr>
    <tr>
      <td><code>serviceMesh.defaultDeny</code></td>
      <td><code>object</code></td>
      <td><em>(empty)</em></td>
      <td>Render a namespace-wide default-deny so every pod in the install namespace rejects unauthorized mesh traffic and the per-workload allows above are the only exceptions (zero-trust namespace). Istio renders an empty-spec AuthorizationPolicy (deny-all) scoped to the whole namespace, which sits at lower precedence than the workload ALLOW. Linkerd has no per-object deny-all; the namespace default is set via the config.linkerd.io/default-inbound-policy annotation, so this is stamped onto the chart-managed Namespace (requires namespace.create=true) — otherwise annotate the namespace out-of-band. Enable only when jaas owns its namespace; it also denies co-located workloads.</td>
    </tr>
    <tr>
      <td><code>serviceMesh.defaultDeny.enabled</code></td>
      <td><code>boolean</code></td>
      <td><code>false</code></td>
      <td></td>
    </tr>
    <tr>
      <td><code>serviceMesh.enabled</code></td>
      <td><code>boolean</code></td>
      <td><code>false</code></td>
      <td>Opt-in. When enabled, the chart renders the selected engine&#39;s authorization &#43; mTLS objects for the jaas pod. Inert unless that mesh (Istio or Linkerd) is actually installed and the pod is injected.</td>
    </tr>
    <tr>
      <td><code>serviceMesh.engine</code></td>
      <td><code>string</code></td>
      <td><code>istio</code></td>
      <td>Mesh engine to render for. istio emits security.istio.io AuthorizationPolicy (&#43; optional PeerAuthentication). linkerd emits policy.linkerd.io Server / AuthorizationPolicy / MeshTLSAuthentication objects.</td>
    </tr>
    <tr>
      <td><code>serviceMesh.http</code></td>
      <td><code>object</code></td>
      <td><em>(empty)</em></td>
      <td>Per-port allowed mesh identities. Each `from` entry is a source matcher:   - principals: SPIFFE/mesh identities (Istio source.principals, e.g.       cluster.local/ns/&lt;ns&gt;/sa/&lt;sa&gt;). For Linkerd these map to       MeshTLSAuthentication identities (the proxy identity string, e.g.       &lt;sa&gt;.&lt;ns&gt;.serviceaccount.identity.linkerd.cluster.local, or &#34;*&#34;).   - namespaces: source namespaces (Istio source.namespaces). ISTIO-ONLY —       Linkerd authenticates by workload identity, not by namespace, so this       field is ignored on the linkerd engine. An EMPTY `from` list on a port means OPEN (allow any caller), mirroring networkPolicy&#39;s empty-`from`=open semantics. On Istio that is a port rule with no `from`; on Linkerd it is a MeshTLSAuthentication of identities: [&#34;*&#34;] (any authenticated meshed client). Mesh identities allowed to hit the jsonnet HTTP endpoint (ports.http). Empty = open.</td>
    </tr>
    <tr>
      <td><code>serviceMesh.http.from</code></td>
      <td><code>array</code></td>
      <td><em>(empty)</em></td>
      <td></td>
    </tr>
    <tr>
      <td><code>serviceMesh.istio</code></td>
      <td><code>object</code></td>
      <td><em>(empty)</em></td>
      <td>Istio engine raw passthrough. Entries are merged verbatim into the AuthorizationPolicy&#39;s spec.rules (security.istio.io/v1 rule schema). Use this to add rules the per-port `from` knobs above can&#39;t express — path/method matchers, `when` JWT-claim conditions, ipBlocks, etc.</td>
    </tr>
    <tr>
      <td><code>serviceMesh.istio.rules</code></td>
      <td><code>array</code></td>
      <td><em>(empty)</em></td>
      <td></td>
    </tr>
    <tr>
      <td><code>serviceMesh.linkerd</code></td>
      <td><code>object</code></td>
      <td><em>(empty)</em></td>
      <td>Linkerd engine raw passthrough. Entries are appended verbatim as additional documents after the rendered Server / AuthorizationPolicy / MeshTLSAuthentication set — each entry must be a complete object (policy.linkerd.io AuthorizationPolicy, HTTPRoute, etc.).</td>
    </tr>
    <tr>
      <td><code>serviceMesh.linkerd.authorizations</code></td>
      <td><code>array</code></td>
      <td><em>(empty)</em></td>
      <td></td>
    </tr>
    <tr>
      <td><code>serviceMesh.metrics</code></td>
      <td><code>object</code></td>
      <td><em>(empty)</em></td>
      <td>Mesh identities allowed to scrape the operator metrics port (ports.metrics). Operator mode &#43; metrics only. Empty = open.</td>
    </tr>
    <tr>
      <td><code>serviceMesh.metrics.from</code></td>
      <td><code>array</code></td>
      <td><em>(empty)</em></td>
      <td></td>
    </tr>
    <tr>
      <td><code>serviceMesh.mtls</code></td>
      <td><code>string</code></td>
      <td><em>(empty)</em></td>
      <td>mTLS posture (Istio engine only). Empty defers to the mesh&#39;s own default (mesh-wide PeerAuthentication / MeshConfig). permissive renders a PeerAuthentication accepting both mTLS and plaintext. strict requires mTLS on the workload&#39;s ports EXCEPT the webhook &#43; management ports, which get a port-level PERMISSIVE carve-out so the non-mesh kube-apiserver and kubelet still connect. Linkerd negotiates mTLS automatically between meshed pods, so this knob does not apply to the linkerd engine.</td>
    </tr>
    <tr>
      <td><code>serviceMesh.storage</code></td>
      <td><code>object</code></td>
      <td><em>(empty)</em></td>
      <td>Mesh identities allowed to read published artifacts from the storage port (ports.storage). Operator mode only. Empty = open.</td>
    </tr>
    <tr>
      <td><code>serviceMesh.storage.from</code></td>
      <td><code>array</code></td>
      <td><em>(empty)</em></td>
      <td></td>
    </tr>
    <tr>
      <td><code>snippets</code></td>
      <td><code>object|null</code></td>
      <td><em>(empty)</em></td>
      <td>Snippet OCI volumes to mount. The key is the name of the directory and the value is an OCI volume that will be mounted beneath .Values.paths.snippets, e.g., the commented example below would result in all files from &#39;ghcr.io/metio/something:latest&#39; to be mounted at &#39;/srv/snippets/something/&#39; (using the default configuration)</td>
    </tr>
  </tbody>
</table>
<h2 id="joi-library-chart">joi library chart</h2>
<p>The <a href="https://github.com/metio/helm-charts/tree/main/charts/joi">joi</a>
 chart
publishes <a href="https://github.com/metio/jsonnet-oci-images">Jsonnet OCI Images</a>
 as
<code>JsonnetLibrary</code> + <code>OCIRepository</code> pairs, so snippets can import vendored
libraries (grafonnet, k8s-libsonnet, …) without bundling them. Deploy it
alongside jaas when snippets reference shared libraries.</p>
<table>
  <thead>
    <tr><th>Value</th><th>Type</th><th>Default</th><th>Description</th></tr>
  </thead>
  <tbody>
    <tr>
      <td><code>global</code></td>
      <td><code>object</code></td>
      <td><em>(empty)</em></td>
      <td>Global values are values that can be accessed from any chart or subchart by exactly the same name.</td>
    </tr>
    <tr>
      <td><code>imagePullSecret</code></td>
      <td><code>string</code></td>
      <td><em>(empty)</em></td>
      <td>dockerconfigjson Secret for authenticated (private-mirror) pulls.</td>
    </tr>
    <tr>
      <td><code>interval</code></td>
      <td><code>string</code></td>
      <td><code>60m</code></td>
      <td>OCIRepository re-pull cadence (a moved `latest` tag is picked up here).</td>
    </tr>
    <tr>
      <td><code>libraries</code></td>
      <td><code></code></td>
      <td><em>(empty)</em></td>
      <td>One entry per JOI image. Pick the LIBRARY version in the import path, e.g. `import &#39;github.com/jsonnet-libs/k8s-libsonnet/1.34/main.libsonnet&#39;` or `.../latest/main.libsonnet`. `path: &#34;&#34;` exposes the whole vendor tree. The `tag` defaults to `latest` (moving); set it to a dated JOI snapshot (e.g. `2026.6.16`) to pin the image for reproducible pulls. </td>
    </tr>
    <tr>
      <td><code>libraries.actions-runner-controller-libsonnet</code></td>
      <td><code>object</code></td>
      <td><em>(empty)</em></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.actions-runner-controller-libsonnet.closure</code></td>
      <td><code>array</code></td>
      <td><em>(empty)</em></td>
      <td>libraries this one imports; auto-enabled with it</td>
    </tr>
    <tr>
      <td><code>libraries.actions-runner-controller-libsonnet.enabled</code></td>
      <td><code>boolean</code></td>
      <td><code>false</code></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.actions-runner-controller-libsonnet.image</code></td>
      <td><code>string</code></td>
      <td><code>ghcr.io/metio/joi-jsonnet-libs-actions-runner-controller-libsonnet</code></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.actions-runner-controller-libsonnet.path</code></td>
      <td><code>string</code></td>
      <td><em>(empty)</em></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.actions-runner-controller-libsonnet.tag</code></td>
      <td><code>string</code></td>
      <td><code>latest</code></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.aiven-libsonnet</code></td>
      <td><code>object</code></td>
      <td><em>(empty)</em></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.aiven-libsonnet.closure</code></td>
      <td><code>array</code></td>
      <td><em>(empty)</em></td>
      <td>libraries this one imports; auto-enabled with it</td>
    </tr>
    <tr>
      <td><code>libraries.aiven-libsonnet.enabled</code></td>
      <td><code>boolean</code></td>
      <td><code>false</code></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.aiven-libsonnet.image</code></td>
      <td><code>string</code></td>
      <td><code>ghcr.io/metio/joi-jsonnet-libs-aiven-libsonnet</code></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.aiven-libsonnet.path</code></td>
      <td><code>string</code></td>
      <td><em>(empty)</em></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.aiven-libsonnet.tag</code></td>
      <td><code>string</code></td>
      <td><code>latest</code></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.amazon-vpc-resource-controller-libsonnet</code></td>
      <td><code>object</code></td>
      <td><em>(empty)</em></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.amazon-vpc-resource-controller-libsonnet.closure</code></td>
      <td><code>array</code></td>
      <td><em>(empty)</em></td>
      <td>libraries this one imports; auto-enabled with it</td>
    </tr>
    <tr>
      <td><code>libraries.amazon-vpc-resource-controller-libsonnet.enabled</code></td>
      <td><code>boolean</code></td>
      <td><code>false</code></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.amazon-vpc-resource-controller-libsonnet.image</code></td>
      <td><code>string</code></td>
      <td><code>ghcr.io/metio/joi-jsonnet-libs-amazon-vpc-resource-controller-libsonnet</code></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.amazon-vpc-resource-controller-libsonnet.path</code></td>
      <td><code>string</code></td>
      <td><em>(empty)</em></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.amazon-vpc-resource-controller-libsonnet.tag</code></td>
      <td><code>string</code></td>
      <td><code>latest</code></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.argo-cd-libsonnet</code></td>
      <td><code>object</code></td>
      <td><em>(empty)</em></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.argo-cd-libsonnet.closure</code></td>
      <td><code>array</code></td>
      <td><em>(empty)</em></td>
      <td>libraries this one imports; auto-enabled with it</td>
    </tr>
    <tr>
      <td><code>libraries.argo-cd-libsonnet.enabled</code></td>
      <td><code>boolean</code></td>
      <td><code>false</code></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.argo-cd-libsonnet.image</code></td>
      <td><code>string</code></td>
      <td><code>ghcr.io/metio/joi-jsonnet-libs-argo-cd-libsonnet</code></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.argo-cd-libsonnet.path</code></td>
      <td><code>string</code></td>
      <td><em>(empty)</em></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.argo-cd-libsonnet.tag</code></td>
      <td><code>string</code></td>
      <td><code>latest</code></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.argo-rollouts-libsonnet</code></td>
      <td><code>object</code></td>
      <td><em>(empty)</em></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.argo-rollouts-libsonnet.closure</code></td>
      <td><code>array</code></td>
      <td><em>(empty)</em></td>
      <td>libraries this one imports; auto-enabled with it</td>
    </tr>
    <tr>
      <td><code>libraries.argo-rollouts-libsonnet.enabled</code></td>
      <td><code>boolean</code></td>
      <td><code>false</code></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.argo-rollouts-libsonnet.image</code></td>
      <td><code>string</code></td>
      <td><code>ghcr.io/metio/joi-jsonnet-libs-argo-rollouts-libsonnet</code></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.argo-rollouts-libsonnet.path</code></td>
      <td><code>string</code></td>
      <td><em>(empty)</em></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.argo-rollouts-libsonnet.tag</code></td>
      <td><code>string</code></td>
      <td><code>latest</code></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.argo-workflows-libsonnet</code></td>
      <td><code>object</code></td>
      <td><em>(empty)</em></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.argo-workflows-libsonnet.closure</code></td>
      <td><code>array</code></td>
      <td><em>(empty)</em></td>
      <td>libraries this one imports; auto-enabled with it</td>
    </tr>
    <tr>
      <td><code>libraries.argo-workflows-libsonnet.enabled</code></td>
      <td><code>boolean</code></td>
      <td><code>false</code></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.argo-workflows-libsonnet.image</code></td>
      <td><code>string</code></td>
      <td><code>ghcr.io/metio/joi-jsonnet-libs-argo-workflows-libsonnet</code></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.argo-workflows-libsonnet.path</code></td>
      <td><code>string</code></td>
      <td><em>(empty)</em></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.argo-workflows-libsonnet.tag</code></td>
      <td><code>string</code></td>
      <td><code>latest</code></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.argocd-operator-libsonnet</code></td>
      <td><code>object</code></td>
      <td><em>(empty)</em></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.argocd-operator-libsonnet.closure</code></td>
      <td><code>array</code></td>
      <td><em>(empty)</em></td>
      <td>libraries this one imports; auto-enabled with it</td>
    </tr>
    <tr>
      <td><code>libraries.argocd-operator-libsonnet.enabled</code></td>
      <td><code>boolean</code></td>
      <td><code>false</code></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.argocd-operator-libsonnet.image</code></td>
      <td><code>string</code></td>
      <td><code>ghcr.io/metio/joi-jsonnet-libs-argocd-operator-libsonnet</code></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.argocd-operator-libsonnet.path</code></td>
      <td><code>string</code></td>
      <td><em>(empty)</em></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.argocd-operator-libsonnet.tag</code></td>
      <td><code>string</code></td>
      <td><code>latest</code></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.aws-load-balancer-controller-libsonnet</code></td>
      <td><code>object</code></td>
      <td><em>(empty)</em></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.aws-load-balancer-controller-libsonnet.closure</code></td>
      <td><code>array</code></td>
      <td><em>(empty)</em></td>
      <td>libraries this one imports; auto-enabled with it</td>
    </tr>
    <tr>
      <td><code>libraries.aws-load-balancer-controller-libsonnet.enabled</code></td>
      <td><code>boolean</code></td>
      <td><code>false</code></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.aws-load-balancer-controller-libsonnet.image</code></td>
      <td><code>string</code></td>
      <td><code>ghcr.io/metio/joi-jsonnet-libs-aws-load-balancer-controller-libsonnet</code></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.aws-load-balancer-controller-libsonnet.path</code></td>
      <td><code>string</code></td>
      <td><em>(empty)</em></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.aws-load-balancer-controller-libsonnet.tag</code></td>
      <td><code>string</code></td>
      <td><code>latest</code></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.aws-rds-controller-libsonnet</code></td>
      <td><code>object</code></td>
      <td><em>(empty)</em></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.aws-rds-controller-libsonnet.closure</code></td>
      <td><code>array</code></td>
      <td><em>(empty)</em></td>
      <td>libraries this one imports; auto-enabled with it</td>
    </tr>
    <tr>
      <td><code>libraries.aws-rds-controller-libsonnet.enabled</code></td>
      <td><code>boolean</code></td>
      <td><code>false</code></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.aws-rds-controller-libsonnet.image</code></td>
      <td><code>string</code></td>
      <td><code>ghcr.io/metio/joi-jsonnet-libs-aws-rds-controller-libsonnet</code></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.aws-rds-controller-libsonnet.path</code></td>
      <td><code>string</code></td>
      <td><em>(empty)</em></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.aws-rds-controller-libsonnet.tag</code></td>
      <td><code>string</code></td>
      <td><code>latest</code></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.azure-load-balancer-controller-libsonnet</code></td>
      <td><code>object</code></td>
      <td><em>(empty)</em></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.azure-load-balancer-controller-libsonnet.closure</code></td>
      <td><code>array</code></td>
      <td><em>(empty)</em></td>
      <td>libraries this one imports; auto-enabled with it</td>
    </tr>
    <tr>
      <td><code>libraries.azure-load-balancer-controller-libsonnet.enabled</code></td>
      <td><code>boolean</code></td>
      <td><code>false</code></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.azure-load-balancer-controller-libsonnet.image</code></td>
      <td><code>string</code></td>
      <td><code>ghcr.io/metio/joi-jsonnet-libs-azure-load-balancer-controller-libsonnet</code></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.azure-load-balancer-controller-libsonnet.path</code></td>
      <td><code>string</code></td>
      <td><em>(empty)</em></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.azure-load-balancer-controller-libsonnet.tag</code></td>
      <td><code>string</code></td>
      <td><code>latest</code></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.banzai-logging-libsonnet</code></td>
      <td><code>object</code></td>
      <td><em>(empty)</em></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.banzai-logging-libsonnet.closure</code></td>
      <td><code>array</code></td>
      <td><em>(empty)</em></td>
      <td>libraries this one imports; auto-enabled with it</td>
    </tr>
    <tr>
      <td><code>libraries.banzai-logging-libsonnet.enabled</code></td>
      <td><code>boolean</code></td>
      <td><code>false</code></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.banzai-logging-libsonnet.image</code></td>
      <td><code>string</code></td>
      <td><code>ghcr.io/metio/joi-jsonnet-libs-banzai-logging-libsonnet</code></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.banzai-logging-libsonnet.path</code></td>
      <td><code>string</code></td>
      <td><em>(empty)</em></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.banzai-logging-libsonnet.tag</code></td>
      <td><code>string</code></td>
      <td><code>latest</code></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.banzaicloud-bank-vaults-libsonnet</code></td>
      <td><code>object</code></td>
      <td><em>(empty)</em></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.banzaicloud-bank-vaults-libsonnet.closure</code></td>
      <td><code>array</code></td>
      <td><em>(empty)</em></td>
      <td>libraries this one imports; auto-enabled with it</td>
    </tr>
    <tr>
      <td><code>libraries.banzaicloud-bank-vaults-libsonnet.enabled</code></td>
      <td><code>boolean</code></td>
      <td><code>false</code></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.banzaicloud-bank-vaults-libsonnet.image</code></td>
      <td><code>string</code></td>
      <td><code>ghcr.io/metio/joi-jsonnet-libs-banzaicloud-bank-vaults-libsonnet</code></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.banzaicloud-bank-vaults-libsonnet.path</code></td>
      <td><code>string</code></td>
      <td><em>(empty)</em></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.banzaicloud-bank-vaults-libsonnet.tag</code></td>
      <td><code>string</code></td>
      <td><code>latest</code></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.calico-libsonnet</code></td>
      <td><code>object</code></td>
      <td><em>(empty)</em></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.calico-libsonnet.closure</code></td>
      <td><code>array</code></td>
      <td><em>(empty)</em></td>
      <td>libraries this one imports; auto-enabled with it</td>
    </tr>
    <tr>
      <td><code>libraries.calico-libsonnet.enabled</code></td>
      <td><code>boolean</code></td>
      <td><code>false</code></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.calico-libsonnet.image</code></td>
      <td><code>string</code></td>
      <td><code>ghcr.io/metio/joi-jsonnet-libs-calico-libsonnet</code></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.calico-libsonnet.path</code></td>
      <td><code>string</code></td>
      <td><em>(empty)</em></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.calico-libsonnet.tag</code></td>
      <td><code>string</code></td>
      <td><code>latest</code></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.cert-manager-libsonnet</code></td>
      <td><code>object</code></td>
      <td><em>(empty)</em></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.cert-manager-libsonnet.closure</code></td>
      <td><code>array</code></td>
      <td><em>(empty)</em></td>
      <td>libraries this one imports; auto-enabled with it</td>
    </tr>
    <tr>
      <td><code>libraries.cert-manager-libsonnet.enabled</code></td>
      <td><code>boolean</code></td>
      <td><code>false</code></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.cert-manager-libsonnet.image</code></td>
      <td><code>string</code></td>
      <td><code>ghcr.io/metio/joi-jsonnet-libs-cert-manager-libsonnet</code></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.cert-manager-libsonnet.path</code></td>
      <td><code>string</code></td>
      <td><em>(empty)</em></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.cert-manager-libsonnet.tag</code></td>
      <td><code>string</code></td>
      <td><code>latest</code></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.cilium-libsonnet</code></td>
      <td><code>object</code></td>
      <td><em>(empty)</em></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.cilium-libsonnet.closure</code></td>
      <td><code>array</code></td>
      <td><em>(empty)</em></td>
      <td>libraries this one imports; auto-enabled with it</td>
    </tr>
    <tr>
      <td><code>libraries.cilium-libsonnet.enabled</code></td>
      <td><code>boolean</code></td>
      <td><code>false</code></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.cilium-libsonnet.image</code></td>
      <td><code>string</code></td>
      <td><code>ghcr.io/metio/joi-jsonnet-libs-cilium-libsonnet</code></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.cilium-libsonnet.path</code></td>
      <td><code>string</code></td>
      <td><em>(empty)</em></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.cilium-libsonnet.tag</code></td>
      <td><code>string</code></td>
      <td><code>latest</code></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.clickhouse-operator-libsonnet</code></td>
      <td><code>object</code></td>
      <td><em>(empty)</em></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.clickhouse-operator-libsonnet.closure</code></td>
      <td><code>array</code></td>
      <td><em>(empty)</em></td>
      <td>libraries this one imports; auto-enabled with it</td>
    </tr>
    <tr>
      <td><code>libraries.clickhouse-operator-libsonnet.enabled</code></td>
      <td><code>boolean</code></td>
      <td><code>false</code></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.clickhouse-operator-libsonnet.image</code></td>
      <td><code>string</code></td>
      <td><code>ghcr.io/metio/joi-jsonnet-libs-clickhouse-operator-libsonnet</code></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.clickhouse-operator-libsonnet.path</code></td>
      <td><code>string</code></td>
      <td><em>(empty)</em></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.clickhouse-operator-libsonnet.tag</code></td>
      <td><code>string</code></td>
      <td><code>latest</code></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.cloudnative-pg-barman-cloud-plugin-libsonnet</code></td>
      <td><code>object</code></td>
      <td><em>(empty)</em></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.cloudnative-pg-barman-cloud-plugin-libsonnet.closure</code></td>
      <td><code>array</code></td>
      <td><em>(empty)</em></td>
      <td>libraries this one imports; auto-enabled with it</td>
    </tr>
    <tr>
      <td><code>libraries.cloudnative-pg-barman-cloud-plugin-libsonnet.enabled</code></td>
      <td><code>boolean</code></td>
      <td><code>false</code></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.cloudnative-pg-barman-cloud-plugin-libsonnet.image</code></td>
      <td><code>string</code></td>
      <td><code>ghcr.io/metio/joi-jsonnet-libs-cloudnative-pg-barman-cloud-plugin-libsonnet</code></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.cloudnative-pg-barman-cloud-plugin-libsonnet.path</code></td>
      <td><code>string</code></td>
      <td><em>(empty)</em></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.cloudnative-pg-barman-cloud-plugin-libsonnet.tag</code></td>
      <td><code>string</code></td>
      <td><code>latest</code></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.cloudnative-pg-libsonnet</code></td>
      <td><code>object</code></td>
      <td><em>(empty)</em></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.cloudnative-pg-libsonnet.closure</code></td>
      <td><code>array</code></td>
      <td><em>(empty)</em></td>
      <td>libraries this one imports; auto-enabled with it</td>
    </tr>
    <tr>
      <td><code>libraries.cloudnative-pg-libsonnet.enabled</code></td>
      <td><code>boolean</code></td>
      <td><code>false</code></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.cloudnative-pg-libsonnet.image</code></td>
      <td><code>string</code></td>
      <td><code>ghcr.io/metio/joi-jsonnet-libs-cloudnative-pg-libsonnet</code></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.cloudnative-pg-libsonnet.path</code></td>
      <td><code>string</code></td>
      <td><em>(empty)</em></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.cloudnative-pg-libsonnet.tag</code></td>
      <td><code>string</code></td>
      <td><code>latest</code></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.cluster-api-libsonnet</code></td>
      <td><code>object</code></td>
      <td><em>(empty)</em></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.cluster-api-libsonnet.closure</code></td>
      <td><code>array</code></td>
      <td><em>(empty)</em></td>
      <td>libraries this one imports; auto-enabled with it</td>
    </tr>
    <tr>
      <td><code>libraries.cluster-api-libsonnet.enabled</code></td>
      <td><code>boolean</code></td>
      <td><code>false</code></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.cluster-api-libsonnet.image</code></td>
      <td><code>string</code></td>
      <td><code>ghcr.io/metio/joi-jsonnet-libs-cluster-api-libsonnet</code></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.cluster-api-libsonnet.path</code></td>
      <td><code>string</code></td>
      <td><em>(empty)</em></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.cluster-api-libsonnet.tag</code></td>
      <td><code>string</code></td>
      <td><code>latest</code></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.cluster-api-operator-libsonnet</code></td>
      <td><code>object</code></td>
      <td><em>(empty)</em></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.cluster-api-operator-libsonnet.closure</code></td>
      <td><code>array</code></td>
      <td><em>(empty)</em></td>
      <td>libraries this one imports; auto-enabled with it</td>
    </tr>
    <tr>
      <td><code>libraries.cluster-api-operator-libsonnet.enabled</code></td>
      <td><code>boolean</code></td>
      <td><code>false</code></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.cluster-api-operator-libsonnet.image</code></td>
      <td><code>string</code></td>
      <td><code>ghcr.io/metio/joi-jsonnet-libs-cluster-api-operator-libsonnet</code></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.cluster-api-operator-libsonnet.path</code></td>
      <td><code>string</code></td>
      <td><em>(empty)</em></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.cluster-api-operator-libsonnet.tag</code></td>
      <td><code>string</code></td>
      <td><code>latest</code></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.cluster-api-provider-aws-libsonnet</code></td>
      <td><code>object</code></td>
      <td><em>(empty)</em></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.cluster-api-provider-aws-libsonnet.closure</code></td>
      <td><code>array</code></td>
      <td><em>(empty)</em></td>
      <td>libraries this one imports; auto-enabled with it</td>
    </tr>
    <tr>
      <td><code>libraries.cluster-api-provider-aws-libsonnet.enabled</code></td>
      <td><code>boolean</code></td>
      <td><code>false</code></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.cluster-api-provider-aws-libsonnet.image</code></td>
      <td><code>string</code></td>
      <td><code>ghcr.io/metio/joi-jsonnet-libs-cluster-api-provider-aws-libsonnet</code></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.cluster-api-provider-aws-libsonnet.path</code></td>
      <td><code>string</code></td>
      <td><em>(empty)</em></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.cluster-api-provider-aws-libsonnet.tag</code></td>
      <td><code>string</code></td>
      <td><code>latest</code></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.cluster-api-provider-tinkerbell-libsonnet</code></td>
      <td><code>object</code></td>
      <td><em>(empty)</em></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.cluster-api-provider-tinkerbell-libsonnet.closure</code></td>
      <td><code>array</code></td>
      <td><em>(empty)</em></td>
      <td>libraries this one imports; auto-enabled with it</td>
    </tr>
    <tr>
      <td><code>libraries.cluster-api-provider-tinkerbell-libsonnet.enabled</code></td>
      <td><code>boolean</code></td>
      <td><code>false</code></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.cluster-api-provider-tinkerbell-libsonnet.image</code></td>
      <td><code>string</code></td>
      <td><code>ghcr.io/metio/joi-jsonnet-libs-cluster-api-provider-tinkerbell-libsonnet</code></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.cluster-api-provider-tinkerbell-libsonnet.path</code></td>
      <td><code>string</code></td>
      <td><em>(empty)</em></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.cluster-api-provider-tinkerbell-libsonnet.tag</code></td>
      <td><code>string</code></td>
      <td><code>latest</code></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.cnrm-libsonnet</code></td>
      <td><code>object</code></td>
      <td><em>(empty)</em></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.cnrm-libsonnet.closure</code></td>
      <td><code>array</code></td>
      <td><em>(empty)</em></td>
      <td>libraries this one imports; auto-enabled with it</td>
    </tr>
    <tr>
      <td><code>libraries.cnrm-libsonnet.enabled</code></td>
      <td><code>boolean</code></td>
      <td><code>false</code></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.cnrm-libsonnet.image</code></td>
      <td><code>string</code></td>
      <td><code>ghcr.io/metio/joi-jsonnet-libs-cnrm-libsonnet</code></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.cnrm-libsonnet.path</code></td>
      <td><code>string</code></td>
      <td><em>(empty)</em></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.cnrm-libsonnet.tag</code></td>
      <td><code>string</code></td>
      <td><code>latest</code></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.composable-libsonnet</code></td>
      <td><code>object</code></td>
      <td><em>(empty)</em></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.composable-libsonnet.closure</code></td>
      <td><code>array</code></td>
      <td><em>(empty)</em></td>
      <td>libraries this one imports; auto-enabled with it</td>
    </tr>
    <tr>
      <td><code>libraries.composable-libsonnet.enabled</code></td>
      <td><code>boolean</code></td>
      <td><code>false</code></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.composable-libsonnet.image</code></td>
      <td><code>string</code></td>
      <td><code>ghcr.io/metio/joi-jsonnet-libs-composable-libsonnet</code></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.composable-libsonnet.path</code></td>
      <td><code>string</code></td>
      <td><em>(empty)</em></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.composable-libsonnet.tag</code></td>
      <td><code>string</code></td>
      <td><code>latest</code></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.consul-libsonnet</code></td>
      <td><code>object</code></td>
      <td><em>(empty)</em></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.consul-libsonnet.closure</code></td>
      <td><code>array</code></td>
      <td><em>(empty)</em></td>
      <td>libraries this one imports; auto-enabled with it</td>
    </tr>
    <tr>
      <td><code>libraries.consul-libsonnet.enabled</code></td>
      <td><code>boolean</code></td>
      <td><code>false</code></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.consul-libsonnet.image</code></td>
      <td><code>string</code></td>
      <td><code>ghcr.io/metio/joi-jsonnet-libs-consul-libsonnet</code></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.consul-libsonnet.path</code></td>
      <td><code>string</code></td>
      <td><em>(empty)</em></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.consul-libsonnet.tag</code></td>
      <td><code>string</code></td>
      <td><code>latest</code></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.contour-libsonnet</code></td>
      <td><code>object</code></td>
      <td><em>(empty)</em></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.contour-libsonnet.closure</code></td>
      <td><code>array</code></td>
      <td><em>(empty)</em></td>
      <td>libraries this one imports; auto-enabled with it</td>
    </tr>
    <tr>
      <td><code>libraries.contour-libsonnet.enabled</code></td>
      <td><code>boolean</code></td>
      <td><code>false</code></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.contour-libsonnet.image</code></td>
      <td><code>string</code></td>
      <td><code>ghcr.io/metio/joi-jsonnet-libs-contour-libsonnet</code></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.contour-libsonnet.path</code></td>
      <td><code>string</code></td>
      <td><em>(empty)</em></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.contour-libsonnet.tag</code></td>
      <td><code>string</code></td>
      <td><code>latest</code></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.crossplane-core-libsonnet</code></td>
      <td><code>object</code></td>
      <td><em>(empty)</em></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.crossplane-core-libsonnet.closure</code></td>
      <td><code>array</code></td>
      <td><em>(empty)</em></td>
      <td>libraries this one imports; auto-enabled with it</td>
    </tr>
    <tr>
      <td><code>libraries.crossplane-core-libsonnet.enabled</code></td>
      <td><code>boolean</code></td>
      <td><code>false</code></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.crossplane-core-libsonnet.image</code></td>
      <td><code>string</code></td>
      <td><code>ghcr.io/metio/joi-jsonnet-libs-crossplane-core-libsonnet</code></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.crossplane-core-libsonnet.path</code></td>
      <td><code>string</code></td>
      <td><em>(empty)</em></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.crossplane-core-libsonnet.tag</code></td>
      <td><code>string</code></td>
      <td><code>latest</code></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.crossplane-libsonnet</code></td>
      <td><code>object</code></td>
      <td><em>(empty)</em></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.crossplane-libsonnet.closure</code></td>
      <td><code>array</code></td>
      <td><em>(empty)</em></td>
      <td>libraries this one imports; auto-enabled with it</td>
    </tr>
    <tr>
      <td><code>libraries.crossplane-libsonnet.enabled</code></td>
      <td><code>boolean</code></td>
      <td><code>false</code></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.crossplane-libsonnet.image</code></td>
      <td><code>string</code></td>
      <td><code>ghcr.io/metio/joi-jsonnet-libs-crossplane-libsonnet</code></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.crossplane-libsonnet.path</code></td>
      <td><code>string</code></td>
      <td><em>(empty)</em></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.crossplane-libsonnet.tag</code></td>
      <td><code>string</code></td>
      <td><code>latest</code></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.crossplane-provider-grafana-libsonnet</code></td>
      <td><code>object</code></td>
      <td><em>(empty)</em></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.crossplane-provider-grafana-libsonnet.closure</code></td>
      <td><code>array</code></td>
      <td><em>(empty)</em></td>
      <td>libraries this one imports; auto-enabled with it</td>
    </tr>
    <tr>
      <td><code>libraries.crossplane-provider-grafana-libsonnet.enabled</code></td>
      <td><code>boolean</code></td>
      <td><code>false</code></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.crossplane-provider-grafana-libsonnet.image</code></td>
      <td><code>string</code></td>
      <td><code>ghcr.io/metio/joi-jsonnet-libs-crossplane-provider-grafana-libsonnet</code></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.crossplane-provider-grafana-libsonnet.path</code></td>
      <td><code>string</code></td>
      <td><em>(empty)</em></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.crossplane-provider-grafana-libsonnet.tag</code></td>
      <td><code>string</code></td>
      <td><code>latest</code></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.crossplane-provider-upjet-aws-libsonnet</code></td>
      <td><code>object</code></td>
      <td><em>(empty)</em></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.crossplane-provider-upjet-aws-libsonnet.closure</code></td>
      <td><code>array</code></td>
      <td><em>(empty)</em></td>
      <td>libraries this one imports; auto-enabled with it</td>
    </tr>
    <tr>
      <td><code>libraries.crossplane-provider-upjet-aws-libsonnet.enabled</code></td>
      <td><code>boolean</code></td>
      <td><code>false</code></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.crossplane-provider-upjet-aws-libsonnet.image</code></td>
      <td><code>string</code></td>
      <td><code>ghcr.io/metio/joi-jsonnet-libs-crossplane-provider-upjet-aws-libsonnet</code></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.crossplane-provider-upjet-aws-libsonnet.path</code></td>
      <td><code>string</code></td>
      <td><em>(empty)</em></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.crossplane-provider-upjet-aws-libsonnet.tag</code></td>
      <td><code>string</code></td>
      <td><code>latest</code></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.crossplane-provider-upjet-azure-libsonnet</code></td>
      <td><code>object</code></td>
      <td><em>(empty)</em></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.crossplane-provider-upjet-azure-libsonnet.closure</code></td>
      <td><code>array</code></td>
      <td><em>(empty)</em></td>
      <td>libraries this one imports; auto-enabled with it</td>
    </tr>
    <tr>
      <td><code>libraries.crossplane-provider-upjet-azure-libsonnet.enabled</code></td>
      <td><code>boolean</code></td>
      <td><code>false</code></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.crossplane-provider-upjet-azure-libsonnet.image</code></td>
      <td><code>string</code></td>
      <td><code>ghcr.io/metio/joi-jsonnet-libs-crossplane-provider-upjet-azure-libsonnet</code></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.crossplane-provider-upjet-azure-libsonnet.path</code></td>
      <td><code>string</code></td>
      <td><em>(empty)</em></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.crossplane-provider-upjet-azure-libsonnet.tag</code></td>
      <td><code>string</code></td>
      <td><code>latest</code></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.crossplane-provider-upjet-azuread-libsonnet</code></td>
      <td><code>object</code></td>
      <td><em>(empty)</em></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.crossplane-provider-upjet-azuread-libsonnet.closure</code></td>
      <td><code>array</code></td>
      <td><em>(empty)</em></td>
      <td>libraries this one imports; auto-enabled with it</td>
    </tr>
    <tr>
      <td><code>libraries.crossplane-provider-upjet-azuread-libsonnet.enabled</code></td>
      <td><code>boolean</code></td>
      <td><code>false</code></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.crossplane-provider-upjet-azuread-libsonnet.image</code></td>
      <td><code>string</code></td>
      <td><code>ghcr.io/metio/joi-jsonnet-libs-crossplane-provider-upjet-azuread-libsonnet</code></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.crossplane-provider-upjet-azuread-libsonnet.path</code></td>
      <td><code>string</code></td>
      <td><em>(empty)</em></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.crossplane-provider-upjet-azuread-libsonnet.tag</code></td>
      <td><code>string</code></td>
      <td><code>latest</code></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.crossplane-provider-upjet-gcp-libsonnet</code></td>
      <td><code>object</code></td>
      <td><em>(empty)</em></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.crossplane-provider-upjet-gcp-libsonnet.closure</code></td>
      <td><code>array</code></td>
      <td><em>(empty)</em></td>
      <td>libraries this one imports; auto-enabled with it</td>
    </tr>
    <tr>
      <td><code>libraries.crossplane-provider-upjet-gcp-libsonnet.enabled</code></td>
      <td><code>boolean</code></td>
      <td><code>false</code></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.crossplane-provider-upjet-gcp-libsonnet.image</code></td>
      <td><code>string</code></td>
      <td><code>ghcr.io/metio/joi-jsonnet-libs-crossplane-provider-upjet-gcp-libsonnet</code></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.crossplane-provider-upjet-gcp-libsonnet.path</code></td>
      <td><code>string</code></td>
      <td><em>(empty)</em></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.crossplane-provider-upjet-gcp-libsonnet.tag</code></td>
      <td><code>string</code></td>
      <td><code>latest</code></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.crossplane-provider-upjet-github-libsonnet</code></td>
      <td><code>object</code></td>
      <td><em>(empty)</em></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.crossplane-provider-upjet-github-libsonnet.closure</code></td>
      <td><code>array</code></td>
      <td><em>(empty)</em></td>
      <td>libraries this one imports; auto-enabled with it</td>
    </tr>
    <tr>
      <td><code>libraries.crossplane-provider-upjet-github-libsonnet.enabled</code></td>
      <td><code>boolean</code></td>
      <td><code>false</code></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.crossplane-provider-upjet-github-libsonnet.image</code></td>
      <td><code>string</code></td>
      <td><code>ghcr.io/metio/joi-jsonnet-libs-crossplane-provider-upjet-github-libsonnet</code></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.crossplane-provider-upjet-github-libsonnet.path</code></td>
      <td><code>string</code></td>
      <td><em>(empty)</em></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.crossplane-provider-upjet-github-libsonnet.tag</code></td>
      <td><code>string</code></td>
      <td><code>latest</code></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.dapr-libsonnet</code></td>
      <td><code>object</code></td>
      <td><em>(empty)</em></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.dapr-libsonnet.closure</code></td>
      <td><code>array</code></td>
      <td><em>(empty)</em></td>
      <td>libraries this one imports; auto-enabled with it</td>
    </tr>
    <tr>
      <td><code>libraries.dapr-libsonnet.enabled</code></td>
      <td><code>boolean</code></td>
      <td><code>false</code></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.dapr-libsonnet.image</code></td>
      <td><code>string</code></td>
      <td><code>ghcr.io/metio/joi-jsonnet-libs-dapr-libsonnet</code></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.dapr-libsonnet.path</code></td>
      <td><code>string</code></td>
      <td><em>(empty)</em></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.dapr-libsonnet.tag</code></td>
      <td><code>string</code></td>
      <td><code>latest</code></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.datadog-operator-libsonnet</code></td>
      <td><code>object</code></td>
      <td><em>(empty)</em></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.datadog-operator-libsonnet.closure</code></td>
      <td><code>array</code></td>
      <td><em>(empty)</em></td>
      <td>libraries this one imports; auto-enabled with it</td>
    </tr>
    <tr>
      <td><code>libraries.datadog-operator-libsonnet.enabled</code></td>
      <td><code>boolean</code></td>
      <td><code>false</code></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.datadog-operator-libsonnet.image</code></td>
      <td><code>string</code></td>
      <td><code>ghcr.io/metio/joi-jsonnet-libs-datadog-operator-libsonnet</code></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.datadog-operator-libsonnet.path</code></td>
      <td><code>string</code></td>
      <td><em>(empty)</em></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.datadog-operator-libsonnet.tag</code></td>
      <td><code>string</code></td>
      <td><code>latest</code></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.docsonnet</code></td>
      <td><code>object</code></td>
      <td><em>(empty)</em></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.docsonnet.closure</code></td>
      <td><code>array</code></td>
      <td><em>(empty)</em></td>
      <td>libraries this one imports; auto-enabled with it</td>
    </tr>
    <tr>
      <td><code>libraries.docsonnet.enabled</code></td>
      <td><code>boolean</code></td>
      <td><code>false</code></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.docsonnet.image</code></td>
      <td><code>string</code></td>
      <td><code>ghcr.io/metio/joi-jsonnet-libs-docsonnet</code></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.docsonnet.path</code></td>
      <td><code>string</code></td>
      <td><em>(empty)</em></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.docsonnet.tag</code></td>
      <td><code>string</code></td>
      <td><code>latest</code></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.eck-operator-libsonnet</code></td>
      <td><code>object</code></td>
      <td><em>(empty)</em></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.eck-operator-libsonnet.closure</code></td>
      <td><code>array</code></td>
      <td><em>(empty)</em></td>
      <td>libraries this one imports; auto-enabled with it</td>
    </tr>
    <tr>
      <td><code>libraries.eck-operator-libsonnet.enabled</code></td>
      <td><code>boolean</code></td>
      <td><code>false</code></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.eck-operator-libsonnet.image</code></td>
      <td><code>string</code></td>
      <td><code>ghcr.io/metio/joi-jsonnet-libs-eck-operator-libsonnet</code></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.eck-operator-libsonnet.path</code></td>
      <td><code>string</code></td>
      <td><em>(empty)</em></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.eck-operator-libsonnet.tag</code></td>
      <td><code>string</code></td>
      <td><code>latest</code></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.edp-keycloak-operator-libsonnet</code></td>
      <td><code>object</code></td>
      <td><em>(empty)</em></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.edp-keycloak-operator-libsonnet.closure</code></td>
      <td><code>array</code></td>
      <td><em>(empty)</em></td>
      <td>libraries this one imports; auto-enabled with it</td>
    </tr>
    <tr>
      <td><code>libraries.edp-keycloak-operator-libsonnet.enabled</code></td>
      <td><code>boolean</code></td>
      <td><code>false</code></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.edp-keycloak-operator-libsonnet.image</code></td>
      <td><code>string</code></td>
      <td><code>ghcr.io/metio/joi-jsonnet-libs-edp-keycloak-operator-libsonnet</code></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.edp-keycloak-operator-libsonnet.path</code></td>
      <td><code>string</code></td>
      <td><em>(empty)</em></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.edp-keycloak-operator-libsonnet.tag</code></td>
      <td><code>string</code></td>
      <td><code>latest</code></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.emissary-libsonnet</code></td>
      <td><code>object</code></td>
      <td><em>(empty)</em></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.emissary-libsonnet.closure</code></td>
      <td><code>array</code></td>
      <td><em>(empty)</em></td>
      <td>libraries this one imports; auto-enabled with it</td>
    </tr>
    <tr>
      <td><code>libraries.emissary-libsonnet.enabled</code></td>
      <td><code>boolean</code></td>
      <td><code>false</code></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.emissary-libsonnet.image</code></td>
      <td><code>string</code></td>
      <td><code>ghcr.io/metio/joi-jsonnet-libs-emissary-libsonnet</code></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.emissary-libsonnet.path</code></td>
      <td><code>string</code></td>
      <td><em>(empty)</em></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.emissary-libsonnet.tag</code></td>
      <td><code>string</code></td>
      <td><code>latest</code></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.envoy-gateway-libsonnet</code></td>
      <td><code>object</code></td>
      <td><em>(empty)</em></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.envoy-gateway-libsonnet.closure</code></td>
      <td><code>array</code></td>
      <td><em>(empty)</em></td>
      <td>libraries this one imports; auto-enabled with it</td>
    </tr>
    <tr>
      <td><code>libraries.envoy-gateway-libsonnet.enabled</code></td>
      <td><code>boolean</code></td>
      <td><code>false</code></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.envoy-gateway-libsonnet.image</code></td>
      <td><code>string</code></td>
      <td><code>ghcr.io/metio/joi-jsonnet-libs-envoy-gateway-libsonnet</code></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.envoy-gateway-libsonnet.path</code></td>
      <td><code>string</code></td>
      <td><em>(empty)</em></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.envoy-gateway-libsonnet.tag</code></td>
      <td><code>string</code></td>
      <td><code>latest</code></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.etcd-operator-libsonnet</code></td>
      <td><code>object</code></td>
      <td><em>(empty)</em></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.etcd-operator-libsonnet.closure</code></td>
      <td><code>array</code></td>
      <td><em>(empty)</em></td>
      <td>libraries this one imports; auto-enabled with it</td>
    </tr>
    <tr>
      <td><code>libraries.etcd-operator-libsonnet.enabled</code></td>
      <td><code>boolean</code></td>
      <td><code>false</code></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.etcd-operator-libsonnet.image</code></td>
      <td><code>string</code></td>
      <td><code>ghcr.io/metio/joi-jsonnet-libs-etcd-operator-libsonnet</code></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.etcd-operator-libsonnet.path</code></td>
      <td><code>string</code></td>
      <td><em>(empty)</em></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.etcd-operator-libsonnet.tag</code></td>
      <td><code>string</code></td>
      <td><code>latest</code></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.external-dns-libsonnet</code></td>
      <td><code>object</code></td>
      <td><em>(empty)</em></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.external-dns-libsonnet.closure</code></td>
      <td><code>array</code></td>
      <td><em>(empty)</em></td>
      <td>libraries this one imports; auto-enabled with it</td>
    </tr>
    <tr>
      <td><code>libraries.external-dns-libsonnet.enabled</code></td>
      <td><code>boolean</code></td>
      <td><code>false</code></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.external-dns-libsonnet.image</code></td>
      <td><code>string</code></td>
      <td><code>ghcr.io/metio/joi-jsonnet-libs-external-dns-libsonnet</code></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.external-dns-libsonnet.path</code></td>
      <td><code>string</code></td>
      <td><em>(empty)</em></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.external-dns-libsonnet.tag</code></td>
      <td><code>string</code></td>
      <td><code>latest</code></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.external-secrets-libsonnet</code></td>
      <td><code>object</code></td>
      <td><em>(empty)</em></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.external-secrets-libsonnet.closure</code></td>
      <td><code>array</code></td>
      <td><em>(empty)</em></td>
      <td>libraries this one imports; auto-enabled with it</td>
    </tr>
    <tr>
      <td><code>libraries.external-secrets-libsonnet.enabled</code></td>
      <td><code>boolean</code></td>
      <td><code>false</code></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.external-secrets-libsonnet.image</code></td>
      <td><code>string</code></td>
      <td><code>ghcr.io/metio/joi-jsonnet-libs-external-secrets-libsonnet</code></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.external-secrets-libsonnet.path</code></td>
      <td><code>string</code></td>
      <td><em>(empty)</em></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.external-secrets-libsonnet.tag</code></td>
      <td><code>string</code></td>
      <td><code>latest</code></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.flagger-libsonnet</code></td>
      <td><code>object</code></td>
      <td><em>(empty)</em></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.flagger-libsonnet.closure</code></td>
      <td><code>array</code></td>
      <td><em>(empty)</em></td>
      <td>libraries this one imports; auto-enabled with it</td>
    </tr>
    <tr>
      <td><code>libraries.flagger-libsonnet.enabled</code></td>
      <td><code>boolean</code></td>
      <td><code>false</code></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.flagger-libsonnet.image</code></td>
      <td><code>string</code></td>
      <td><code>ghcr.io/metio/joi-jsonnet-libs-flagger-libsonnet</code></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.flagger-libsonnet.path</code></td>
      <td><code>string</code></td>
      <td><em>(empty)</em></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.flagger-libsonnet.tag</code></td>
      <td><code>string</code></td>
      <td><code>latest</code></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.fluxcd-libsonnet</code></td>
      <td><code>object</code></td>
      <td><em>(empty)</em></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.fluxcd-libsonnet.closure</code></td>
      <td><code>array</code></td>
      <td><em>(empty)</em></td>
      <td>libraries this one imports; auto-enabled with it</td>
    </tr>
    <tr>
      <td><code>libraries.fluxcd-libsonnet.enabled</code></td>
      <td><code>boolean</code></td>
      <td><code>false</code></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.fluxcd-libsonnet.image</code></td>
      <td><code>string</code></td>
      <td><code>ghcr.io/metio/joi-jsonnet-libs-fluxcd-libsonnet</code></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.fluxcd-libsonnet.path</code></td>
      <td><code>string</code></td>
      <td><em>(empty)</em></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.fluxcd-libsonnet.tag</code></td>
      <td><code>string</code></td>
      <td><code>latest</code></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.gatekeeper-libsonnet</code></td>
      <td><code>object</code></td>
      <td><em>(empty)</em></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.gatekeeper-libsonnet.closure</code></td>
      <td><code>array</code></td>
      <td><em>(empty)</em></td>
      <td>libraries this one imports; auto-enabled with it</td>
    </tr>
    <tr>
      <td><code>libraries.gatekeeper-libsonnet.enabled</code></td>
      <td><code>boolean</code></td>
      <td><code>false</code></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.gatekeeper-libsonnet.image</code></td>
      <td><code>string</code></td>
      <td><code>ghcr.io/metio/joi-jsonnet-libs-gatekeeper-libsonnet</code></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.gatekeeper-libsonnet.path</code></td>
      <td><code>string</code></td>
      <td><em>(empty)</em></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.gatekeeper-libsonnet.tag</code></td>
      <td><code>string</code></td>
      <td><code>latest</code></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.gateway-api-libsonnet</code></td>
      <td><code>object</code></td>
      <td><em>(empty)</em></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.gateway-api-libsonnet.closure</code></td>
      <td><code>array</code></td>
      <td><em>(empty)</em></td>
      <td>libraries this one imports; auto-enabled with it</td>
    </tr>
    <tr>
      <td><code>libraries.gateway-api-libsonnet.enabled</code></td>
      <td><code>boolean</code></td>
      <td><code>false</code></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.gateway-api-libsonnet.image</code></td>
      <td><code>string</code></td>
      <td><code>ghcr.io/metio/joi-jsonnet-libs-gateway-api-libsonnet</code></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.gateway-api-libsonnet.path</code></td>
      <td><code>string</code></td>
      <td><em>(empty)</em></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.gateway-api-libsonnet.tag</code></td>
      <td><code>string</code></td>
      <td><code>latest</code></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.google-cloud-sql-proxy-operator-libsonnet</code></td>
      <td><code>object</code></td>
      <td><em>(empty)</em></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.google-cloud-sql-proxy-operator-libsonnet.closure</code></td>
      <td><code>array</code></td>
      <td><em>(empty)</em></td>
      <td>libraries this one imports; auto-enabled with it</td>
    </tr>
    <tr>
      <td><code>libraries.google-cloud-sql-proxy-operator-libsonnet.enabled</code></td>
      <td><code>boolean</code></td>
      <td><code>false</code></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.google-cloud-sql-proxy-operator-libsonnet.image</code></td>
      <td><code>string</code></td>
      <td><code>ghcr.io/metio/joi-jsonnet-libs-google-cloud-sql-proxy-operator-libsonnet</code></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.google-cloud-sql-proxy-operator-libsonnet.path</code></td>
      <td><code>string</code></td>
      <td><em>(empty)</em></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.google-cloud-sql-proxy-operator-libsonnet.tag</code></td>
      <td><code>string</code></td>
      <td><code>latest</code></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.grafana-agent-libsonnet</code></td>
      <td><code>object</code></td>
      <td><em>(empty)</em></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.grafana-agent-libsonnet.closure</code></td>
      <td><code>array</code></td>
      <td><em>(empty)</em></td>
      <td>libraries this one imports; auto-enabled with it</td>
    </tr>
    <tr>
      <td><code>libraries.grafana-agent-libsonnet.enabled</code></td>
      <td><code>boolean</code></td>
      <td><code>false</code></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.grafana-agent-libsonnet.image</code></td>
      <td><code>string</code></td>
      <td><code>ghcr.io/metio/joi-jsonnet-libs-grafana-agent-libsonnet</code></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.grafana-agent-libsonnet.path</code></td>
      <td><code>string</code></td>
      <td><em>(empty)</em></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.grafana-agent-libsonnet.tag</code></td>
      <td><code>string</code></td>
      <td><code>latest</code></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.grafana-alloy-libsonnet</code></td>
      <td><code>object</code></td>
      <td><em>(empty)</em></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.grafana-alloy-libsonnet.closure</code></td>
      <td><code>array</code></td>
      <td><em>(empty)</em></td>
      <td>libraries this one imports; auto-enabled with it</td>
    </tr>
    <tr>
      <td><code>libraries.grafana-alloy-libsonnet.enabled</code></td>
      <td><code>boolean</code></td>
      <td><code>false</code></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.grafana-alloy-libsonnet.image</code></td>
      <td><code>string</code></td>
      <td><code>ghcr.io/metio/joi-jsonnet-libs-grafana-alloy-libsonnet</code></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.grafana-alloy-libsonnet.path</code></td>
      <td><code>string</code></td>
      <td><em>(empty)</em></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.grafana-alloy-libsonnet.tag</code></td>
      <td><code>string</code></td>
      <td><code>latest</code></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.grafana-operator-libsonnet</code></td>
      <td><code>object</code></td>
      <td><em>(empty)</em></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.grafana-operator-libsonnet.closure</code></td>
      <td><code>array</code></td>
      <td><em>(empty)</em></td>
      <td>libraries this one imports; auto-enabled with it</td>
    </tr>
    <tr>
      <td><code>libraries.grafana-operator-libsonnet.enabled</code></td>
      <td><code>boolean</code></td>
      <td><code>false</code></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.grafana-operator-libsonnet.image</code></td>
      <td><code>string</code></td>
      <td><code>ghcr.io/metio/joi-jsonnet-libs-grafana-operator-libsonnet</code></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.grafana-operator-libsonnet.path</code></td>
      <td><code>string</code></td>
      <td><em>(empty)</em></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.grafana-operator-libsonnet.tag</code></td>
      <td><code>string</code></td>
      <td><code>latest</code></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.grafonnet</code></td>
      <td><code>object</code></td>
      <td><em>(empty)</em></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.grafonnet.closure</code></td>
      <td><code>array</code></td>
      <td><em>(empty)</em></td>
      <td>libraries this one imports; auto-enabled with it</td>
    </tr>
    <tr>
      <td><code>libraries.grafonnet.enabled</code></td>
      <td><code>boolean</code></td>
      <td><code>false</code></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.grafonnet.image</code></td>
      <td><code>string</code></td>
      <td><code>ghcr.io/metio/joi-grafana-grafonnet</code></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.grafonnet.path</code></td>
      <td><code>string</code></td>
      <td><em>(empty)</em></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.grafonnet.tag</code></td>
      <td><code>string</code></td>
      <td><code>latest</code></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.harbor-operator-libsonnet</code></td>
      <td><code>object</code></td>
      <td><em>(empty)</em></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.harbor-operator-libsonnet.closure</code></td>
      <td><code>array</code></td>
      <td><em>(empty)</em></td>
      <td>libraries this one imports; auto-enabled with it</td>
    </tr>
    <tr>
      <td><code>libraries.harbor-operator-libsonnet.enabled</code></td>
      <td><code>boolean</code></td>
      <td><code>false</code></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.harbor-operator-libsonnet.image</code></td>
      <td><code>string</code></td>
      <td><code>ghcr.io/metio/joi-jsonnet-libs-harbor-operator-libsonnet</code></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.harbor-operator-libsonnet.path</code></td>
      <td><code>string</code></td>
      <td><em>(empty)</em></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.harbor-operator-libsonnet.tag</code></td>
      <td><code>string</code></td>
      <td><code>latest</code></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.hcp-terraform-operator-libsonnet</code></td>
      <td><code>object</code></td>
      <td><em>(empty)</em></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.hcp-terraform-operator-libsonnet.closure</code></td>
      <td><code>array</code></td>
      <td><em>(empty)</em></td>
      <td>libraries this one imports; auto-enabled with it</td>
    </tr>
    <tr>
      <td><code>libraries.hcp-terraform-operator-libsonnet.enabled</code></td>
      <td><code>boolean</code></td>
      <td><code>false</code></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.hcp-terraform-operator-libsonnet.image</code></td>
      <td><code>string</code></td>
      <td><code>ghcr.io/metio/joi-jsonnet-libs-hcp-terraform-operator-libsonnet</code></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.hcp-terraform-operator-libsonnet.path</code></td>
      <td><code>string</code></td>
      <td><em>(empty)</em></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.hcp-terraform-operator-libsonnet.tag</code></td>
      <td><code>string</code></td>
      <td><code>latest</code></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.istio-libsonnet</code></td>
      <td><code>object</code></td>
      <td><em>(empty)</em></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.istio-libsonnet.closure</code></td>
      <td><code>array</code></td>
      <td><em>(empty)</em></td>
      <td>libraries this one imports; auto-enabled with it</td>
    </tr>
    <tr>
      <td><code>libraries.istio-libsonnet.enabled</code></td>
      <td><code>boolean</code></td>
      <td><code>false</code></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.istio-libsonnet.image</code></td>
      <td><code>string</code></td>
      <td><code>ghcr.io/metio/joi-jsonnet-libs-istio-libsonnet</code></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.istio-libsonnet.path</code></td>
      <td><code>string</code></td>
      <td><em>(empty)</em></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.istio-libsonnet.tag</code></td>
      <td><code>string</code></td>
      <td><code>latest</code></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.k8s-libsonnet</code></td>
      <td><code>object</code></td>
      <td><em>(empty)</em></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.k8s-libsonnet.closure</code></td>
      <td><code>array</code></td>
      <td><em>(empty)</em></td>
      <td>libraries this one imports; auto-enabled with it</td>
    </tr>
    <tr>
      <td><code>libraries.k8s-libsonnet.enabled</code></td>
      <td><code>boolean</code></td>
      <td><code>false</code></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.k8s-libsonnet.image</code></td>
      <td><code>string</code></td>
      <td><code>ghcr.io/metio/joi-jsonnet-libs-k8s-libsonnet</code></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.k8s-libsonnet.path</code></td>
      <td><code>string</code></td>
      <td><em>(empty)</em></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.k8s-libsonnet.tag</code></td>
      <td><code>string</code></td>
      <td><code>latest</code></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.kargo-libsonnet</code></td>
      <td><code>object</code></td>
      <td><em>(empty)</em></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.kargo-libsonnet.closure</code></td>
      <td><code>array</code></td>
      <td><em>(empty)</em></td>
      <td>libraries this one imports; auto-enabled with it</td>
    </tr>
    <tr>
      <td><code>libraries.kargo-libsonnet.enabled</code></td>
      <td><code>boolean</code></td>
      <td><code>false</code></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.kargo-libsonnet.image</code></td>
      <td><code>string</code></td>
      <td><code>ghcr.io/metio/joi-jsonnet-libs-kargo-libsonnet</code></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.kargo-libsonnet.path</code></td>
      <td><code>string</code></td>
      <td><em>(empty)</em></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.kargo-libsonnet.tag</code></td>
      <td><code>string</code></td>
      <td><code>latest</code></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.karpenter-libsonnet</code></td>
      <td><code>object</code></td>
      <td><em>(empty)</em></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.karpenter-libsonnet.closure</code></td>
      <td><code>array</code></td>
      <td><em>(empty)</em></td>
      <td>libraries this one imports; auto-enabled with it</td>
    </tr>
    <tr>
      <td><code>libraries.karpenter-libsonnet.enabled</code></td>
      <td><code>boolean</code></td>
      <td><code>false</code></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.karpenter-libsonnet.image</code></td>
      <td><code>string</code></td>
      <td><code>ghcr.io/metio/joi-jsonnet-libs-karpenter-libsonnet</code></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.karpenter-libsonnet.path</code></td>
      <td><code>string</code></td>
      <td><em>(empty)</em></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.karpenter-libsonnet.tag</code></td>
      <td><code>string</code></td>
      <td><code>latest</code></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.keda-libsonnet</code></td>
      <td><code>object</code></td>
      <td><em>(empty)</em></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.keda-libsonnet.closure</code></td>
      <td><code>array</code></td>
      <td><em>(empty)</em></td>
      <td>libraries this one imports; auto-enabled with it</td>
    </tr>
    <tr>
      <td><code>libraries.keda-libsonnet.enabled</code></td>
      <td><code>boolean</code></td>
      <td><code>false</code></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.keda-libsonnet.image</code></td>
      <td><code>string</code></td>
      <td><code>ghcr.io/metio/joi-jsonnet-libs-keda-libsonnet</code></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.keda-libsonnet.path</code></td>
      <td><code>string</code></td>
      <td><em>(empty)</em></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.keda-libsonnet.tag</code></td>
      <td><code>string</code></td>
      <td><code>latest</code></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.knative-eventing-libsonnet</code></td>
      <td><code>object</code></td>
      <td><em>(empty)</em></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.knative-eventing-libsonnet.closure</code></td>
      <td><code>array</code></td>
      <td><em>(empty)</em></td>
      <td>libraries this one imports; auto-enabled with it</td>
    </tr>
    <tr>
      <td><code>libraries.knative-eventing-libsonnet.enabled</code></td>
      <td><code>boolean</code></td>
      <td><code>false</code></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.knative-eventing-libsonnet.image</code></td>
      <td><code>string</code></td>
      <td><code>ghcr.io/metio/joi-jsonnet-libs-knative-eventing-libsonnet</code></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.knative-eventing-libsonnet.path</code></td>
      <td><code>string</code></td>
      <td><em>(empty)</em></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.knative-eventing-libsonnet.tag</code></td>
      <td><code>string</code></td>
      <td><code>latest</code></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.knative-serving-libsonnet</code></td>
      <td><code>object</code></td>
      <td><em>(empty)</em></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.knative-serving-libsonnet.closure</code></td>
      <td><code>array</code></td>
      <td><em>(empty)</em></td>
      <td>libraries this one imports; auto-enabled with it</td>
    </tr>
    <tr>
      <td><code>libraries.knative-serving-libsonnet.enabled</code></td>
      <td><code>boolean</code></td>
      <td><code>false</code></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.knative-serving-libsonnet.image</code></td>
      <td><code>string</code></td>
      <td><code>ghcr.io/metio/joi-jsonnet-libs-knative-serving-libsonnet</code></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.knative-serving-libsonnet.path</code></td>
      <td><code>string</code></td>
      <td><em>(empty)</em></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.knative-serving-libsonnet.tag</code></td>
      <td><code>string</code></td>
      <td><code>latest</code></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.kro-libsonnet</code></td>
      <td><code>object</code></td>
      <td><em>(empty)</em></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.kro-libsonnet.closure</code></td>
      <td><code>array</code></td>
      <td><em>(empty)</em></td>
      <td>libraries this one imports; auto-enabled with it</td>
    </tr>
    <tr>
      <td><code>libraries.kro-libsonnet.enabled</code></td>
      <td><code>boolean</code></td>
      <td><code>false</code></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.kro-libsonnet.image</code></td>
      <td><code>string</code></td>
      <td><code>ghcr.io/metio/joi-jsonnet-libs-kro-libsonnet</code></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.kro-libsonnet.path</code></td>
      <td><code>string</code></td>
      <td><em>(empty)</em></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.kro-libsonnet.tag</code></td>
      <td><code>string</code></td>
      <td><code>latest</code></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.kube-prometheus-libsonnet</code></td>
      <td><code>object</code></td>
      <td><em>(empty)</em></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.kube-prometheus-libsonnet.closure</code></td>
      <td><code>array</code></td>
      <td><em>(empty)</em></td>
      <td>libraries this one imports; auto-enabled with it</td>
    </tr>
    <tr>
      <td><code>libraries.kube-prometheus-libsonnet.enabled</code></td>
      <td><code>boolean</code></td>
      <td><code>false</code></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.kube-prometheus-libsonnet.image</code></td>
      <td><code>string</code></td>
      <td><code>ghcr.io/metio/joi-jsonnet-libs-kube-prometheus-libsonnet</code></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.kube-prometheus-libsonnet.path</code></td>
      <td><code>string</code></td>
      <td><em>(empty)</em></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.kube-prometheus-libsonnet.tag</code></td>
      <td><code>string</code></td>
      <td><code>latest</code></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.kubernetes-nmstate-libsonnet</code></td>
      <td><code>object</code></td>
      <td><em>(empty)</em></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.kubernetes-nmstate-libsonnet.closure</code></td>
      <td><code>array</code></td>
      <td><em>(empty)</em></td>
      <td>libraries this one imports; auto-enabled with it</td>
    </tr>
    <tr>
      <td><code>libraries.kubernetes-nmstate-libsonnet.enabled</code></td>
      <td><code>boolean</code></td>
      <td><code>false</code></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.kubernetes-nmstate-libsonnet.image</code></td>
      <td><code>string</code></td>
      <td><code>ghcr.io/metio/joi-jsonnet-libs-kubernetes-nmstate-libsonnet</code></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.kubernetes-nmstate-libsonnet.path</code></td>
      <td><code>string</code></td>
      <td><em>(empty)</em></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.kubernetes-nmstate-libsonnet.tag</code></td>
      <td><code>string</code></td>
      <td><code>latest</code></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.kubernetes-secret-generator-libsonnet</code></td>
      <td><code>object</code></td>
      <td><em>(empty)</em></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.kubernetes-secret-generator-libsonnet.closure</code></td>
      <td><code>array</code></td>
      <td><em>(empty)</em></td>
      <td>libraries this one imports; auto-enabled with it</td>
    </tr>
    <tr>
      <td><code>libraries.kubernetes-secret-generator-libsonnet.enabled</code></td>
      <td><code>boolean</code></td>
      <td><code>false</code></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.kubernetes-secret-generator-libsonnet.image</code></td>
      <td><code>string</code></td>
      <td><code>ghcr.io/metio/joi-jsonnet-libs-kubernetes-secret-generator-libsonnet</code></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.kubernetes-secret-generator-libsonnet.path</code></td>
      <td><code>string</code></td>
      <td><em>(empty)</em></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.kubernetes-secret-generator-libsonnet.tag</code></td>
      <td><code>string</code></td>
      <td><code>latest</code></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.kubevela-libsonnet</code></td>
      <td><code>object</code></td>
      <td><em>(empty)</em></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.kubevela-libsonnet.closure</code></td>
      <td><code>array</code></td>
      <td><em>(empty)</em></td>
      <td>libraries this one imports; auto-enabled with it</td>
    </tr>
    <tr>
      <td><code>libraries.kubevela-libsonnet.enabled</code></td>
      <td><code>boolean</code></td>
      <td><code>false</code></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.kubevela-libsonnet.image</code></td>
      <td><code>string</code></td>
      <td><code>ghcr.io/metio/joi-jsonnet-libs-kubevela-libsonnet</code></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.kubevela-libsonnet.path</code></td>
      <td><code>string</code></td>
      <td><em>(empty)</em></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.kubevela-libsonnet.tag</code></td>
      <td><code>string</code></td>
      <td><code>latest</code></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.kueue-libsonnet</code></td>
      <td><code>object</code></td>
      <td><em>(empty)</em></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.kueue-libsonnet.closure</code></td>
      <td><code>array</code></td>
      <td><em>(empty)</em></td>
      <td>libraries this one imports; auto-enabled with it</td>
    </tr>
    <tr>
      <td><code>libraries.kueue-libsonnet.enabled</code></td>
      <td><code>boolean</code></td>
      <td><code>false</code></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.kueue-libsonnet.image</code></td>
      <td><code>string</code></td>
      <td><code>ghcr.io/metio/joi-jsonnet-libs-kueue-libsonnet</code></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.kueue-libsonnet.path</code></td>
      <td><code>string</code></td>
      <td><em>(empty)</em></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.kueue-libsonnet.tag</code></td>
      <td><code>string</code></td>
      <td><code>latest</code></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.kyverno-libsonnet</code></td>
      <td><code>object</code></td>
      <td><em>(empty)</em></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.kyverno-libsonnet.closure</code></td>
      <td><code>array</code></td>
      <td><em>(empty)</em></td>
      <td>libraries this one imports; auto-enabled with it</td>
    </tr>
    <tr>
      <td><code>libraries.kyverno-libsonnet.enabled</code></td>
      <td><code>boolean</code></td>
      <td><code>false</code></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.kyverno-libsonnet.image</code></td>
      <td><code>string</code></td>
      <td><code>ghcr.io/metio/joi-jsonnet-libs-kyverno-libsonnet</code></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.kyverno-libsonnet.path</code></td>
      <td><code>string</code></td>
      <td><em>(empty)</em></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.kyverno-libsonnet.tag</code></td>
      <td><code>string</code></td>
      <td><code>latest</code></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.litmus-chaos-libsonnet</code></td>
      <td><code>object</code></td>
      <td><em>(empty)</em></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.litmus-chaos-libsonnet.closure</code></td>
      <td><code>array</code></td>
      <td><em>(empty)</em></td>
      <td>libraries this one imports; auto-enabled with it</td>
    </tr>
    <tr>
      <td><code>libraries.litmus-chaos-libsonnet.enabled</code></td>
      <td><code>boolean</code></td>
      <td><code>false</code></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.litmus-chaos-libsonnet.image</code></td>
      <td><code>string</code></td>
      <td><code>ghcr.io/metio/joi-jsonnet-libs-litmus-chaos-libsonnet</code></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.litmus-chaos-libsonnet.path</code></td>
      <td><code>string</code></td>
      <td><em>(empty)</em></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.litmus-chaos-libsonnet.tag</code></td>
      <td><code>string</code></td>
      <td><code>latest</code></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.mariadb-operator-libsonnet</code></td>
      <td><code>object</code></td>
      <td><em>(empty)</em></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.mariadb-operator-libsonnet.closure</code></td>
      <td><code>array</code></td>
      <td><em>(empty)</em></td>
      <td>libraries this one imports; auto-enabled with it</td>
    </tr>
    <tr>
      <td><code>libraries.mariadb-operator-libsonnet.enabled</code></td>
      <td><code>boolean</code></td>
      <td><code>false</code></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.mariadb-operator-libsonnet.image</code></td>
      <td><code>string</code></td>
      <td><code>ghcr.io/metio/joi-jsonnet-libs-mariadb-operator-libsonnet</code></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.mariadb-operator-libsonnet.path</code></td>
      <td><code>string</code></td>
      <td><em>(empty)</em></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.mariadb-operator-libsonnet.tag</code></td>
      <td><code>string</code></td>
      <td><code>latest</code></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.metacontroller-libsonnet</code></td>
      <td><code>object</code></td>
      <td><em>(empty)</em></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.metacontroller-libsonnet.closure</code></td>
      <td><code>array</code></td>
      <td><em>(empty)</em></td>
      <td>libraries this one imports; auto-enabled with it</td>
    </tr>
    <tr>
      <td><code>libraries.metacontroller-libsonnet.enabled</code></td>
      <td><code>boolean</code></td>
      <td><code>false</code></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.metacontroller-libsonnet.image</code></td>
      <td><code>string</code></td>
      <td><code>ghcr.io/metio/joi-jsonnet-libs-metacontroller-libsonnet</code></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.metacontroller-libsonnet.path</code></td>
      <td><code>string</code></td>
      <td><em>(empty)</em></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.metacontroller-libsonnet.tag</code></td>
      <td><code>string</code></td>
      <td><code>latest</code></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.metallb-libsonnet</code></td>
      <td><code>object</code></td>
      <td><em>(empty)</em></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.metallb-libsonnet.closure</code></td>
      <td><code>array</code></td>
      <td><em>(empty)</em></td>
      <td>libraries this one imports; auto-enabled with it</td>
    </tr>
    <tr>
      <td><code>libraries.metallb-libsonnet.enabled</code></td>
      <td><code>boolean</code></td>
      <td><code>false</code></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.metallb-libsonnet.image</code></td>
      <td><code>string</code></td>
      <td><code>ghcr.io/metio/joi-jsonnet-libs-metallb-libsonnet</code></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.metallb-libsonnet.path</code></td>
      <td><code>string</code></td>
      <td><em>(empty)</em></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.metallb-libsonnet.tag</code></td>
      <td><code>string</code></td>
      <td><code>latest</code></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.milvus-operator-libsonnet</code></td>
      <td><code>object</code></td>
      <td><em>(empty)</em></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.milvus-operator-libsonnet.closure</code></td>
      <td><code>array</code></td>
      <td><em>(empty)</em></td>
      <td>libraries this one imports; auto-enabled with it</td>
    </tr>
    <tr>
      <td><code>libraries.milvus-operator-libsonnet.enabled</code></td>
      <td><code>boolean</code></td>
      <td><code>false</code></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.milvus-operator-libsonnet.image</code></td>
      <td><code>string</code></td>
      <td><code>ghcr.io/metio/joi-jsonnet-libs-milvus-operator-libsonnet</code></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.milvus-operator-libsonnet.path</code></td>
      <td><code>string</code></td>
      <td><em>(empty)</em></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.milvus-operator-libsonnet.tag</code></td>
      <td><code>string</code></td>
      <td><code>latest</code></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.minio-operator-libsonnet</code></td>
      <td><code>object</code></td>
      <td><em>(empty)</em></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.minio-operator-libsonnet.closure</code></td>
      <td><code>array</code></td>
      <td><em>(empty)</em></td>
      <td>libraries this one imports; auto-enabled with it</td>
    </tr>
    <tr>
      <td><code>libraries.minio-operator-libsonnet.enabled</code></td>
      <td><code>boolean</code></td>
      <td><code>false</code></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.minio-operator-libsonnet.image</code></td>
      <td><code>string</code></td>
      <td><code>ghcr.io/metio/joi-jsonnet-libs-minio-operator-libsonnet</code></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.minio-operator-libsonnet.path</code></td>
      <td><code>string</code></td>
      <td><em>(empty)</em></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.minio-operator-libsonnet.tag</code></td>
      <td><code>string</code></td>
      <td><code>latest</code></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.mysql-operator-libsonnet</code></td>
      <td><code>object</code></td>
      <td><em>(empty)</em></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.mysql-operator-libsonnet.closure</code></td>
      <td><code>array</code></td>
      <td><em>(empty)</em></td>
      <td>libraries this one imports; auto-enabled with it</td>
    </tr>
    <tr>
      <td><code>libraries.mysql-operator-libsonnet.enabled</code></td>
      <td><code>boolean</code></td>
      <td><code>false</code></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.mysql-operator-libsonnet.image</code></td>
      <td><code>string</code></td>
      <td><code>ghcr.io/metio/joi-jsonnet-libs-mysql-operator-libsonnet</code></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.mysql-operator-libsonnet.path</code></td>
      <td><code>string</code></td>
      <td><em>(empty)</em></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.mysql-operator-libsonnet.tag</code></td>
      <td><code>string</code></td>
      <td><code>latest</code></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.nats-libsonnet</code></td>
      <td><code>object</code></td>
      <td><em>(empty)</em></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.nats-libsonnet.closure</code></td>
      <td><code>array</code></td>
      <td><em>(empty)</em></td>
      <td>libraries this one imports; auto-enabled with it</td>
    </tr>
    <tr>
      <td><code>libraries.nats-libsonnet.enabled</code></td>
      <td><code>boolean</code></td>
      <td><code>false</code></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.nats-libsonnet.image</code></td>
      <td><code>string</code></td>
      <td><code>ghcr.io/metio/joi-jsonnet-libs-nats-libsonnet</code></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.nats-libsonnet.path</code></td>
      <td><code>string</code></td>
      <td><em>(empty)</em></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.nats-libsonnet.tag</code></td>
      <td><code>string</code></td>
      <td><code>latest</code></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.node-feature-discovery-libsonnet</code></td>
      <td><code>object</code></td>
      <td><em>(empty)</em></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.node-feature-discovery-libsonnet.closure</code></td>
      <td><code>array</code></td>
      <td><em>(empty)</em></td>
      <td>libraries this one imports; auto-enabled with it</td>
    </tr>
    <tr>
      <td><code>libraries.node-feature-discovery-libsonnet.enabled</code></td>
      <td><code>boolean</code></td>
      <td><code>false</code></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.node-feature-discovery-libsonnet.image</code></td>
      <td><code>string</code></td>
      <td><code>ghcr.io/metio/joi-jsonnet-libs-node-feature-discovery-libsonnet</code></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.node-feature-discovery-libsonnet.path</code></td>
      <td><code>string</code></td>
      <td><em>(empty)</em></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.node-feature-discovery-libsonnet.tag</code></td>
      <td><code>string</code></td>
      <td><code>latest</code></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.openshift-libsonnet</code></td>
      <td><code>object</code></td>
      <td><em>(empty)</em></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.openshift-libsonnet.closure</code></td>
      <td><code>array</code></td>
      <td><em>(empty)</em></td>
      <td>libraries this one imports; auto-enabled with it</td>
    </tr>
    <tr>
      <td><code>libraries.openshift-libsonnet.enabled</code></td>
      <td><code>boolean</code></td>
      <td><code>false</code></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.openshift-libsonnet.image</code></td>
      <td><code>string</code></td>
      <td><code>ghcr.io/metio/joi-jsonnet-libs-openshift-libsonnet</code></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.openshift-libsonnet.path</code></td>
      <td><code>string</code></td>
      <td><em>(empty)</em></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.openshift-libsonnet.tag</code></td>
      <td><code>string</code></td>
      <td><code>latest</code></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.prometheus-operator-libsonnet</code></td>
      <td><code>object</code></td>
      <td><em>(empty)</em></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.prometheus-operator-libsonnet.closure</code></td>
      <td><code>array</code></td>
      <td><em>(empty)</em></td>
      <td>libraries this one imports; auto-enabled with it</td>
    </tr>
    <tr>
      <td><code>libraries.prometheus-operator-libsonnet.enabled</code></td>
      <td><code>boolean</code></td>
      <td><code>false</code></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.prometheus-operator-libsonnet.image</code></td>
      <td><code>string</code></td>
      <td><code>ghcr.io/metio/joi-jsonnet-libs-prometheus-operator-libsonnet</code></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.prometheus-operator-libsonnet.path</code></td>
      <td><code>string</code></td>
      <td><em>(empty)</em></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.prometheus-operator-libsonnet.tag</code></td>
      <td><code>string</code></td>
      <td><code>latest</code></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.pyrra-libsonnet</code></td>
      <td><code>object</code></td>
      <td><em>(empty)</em></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.pyrra-libsonnet.closure</code></td>
      <td><code>array</code></td>
      <td><em>(empty)</em></td>
      <td>libraries this one imports; auto-enabled with it</td>
    </tr>
    <tr>
      <td><code>libraries.pyrra-libsonnet.enabled</code></td>
      <td><code>boolean</code></td>
      <td><code>false</code></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.pyrra-libsonnet.image</code></td>
      <td><code>string</code></td>
      <td><code>ghcr.io/metio/joi-jsonnet-libs-pyrra-libsonnet</code></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.pyrra-libsonnet.path</code></td>
      <td><code>string</code></td>
      <td><em>(empty)</em></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.pyrra-libsonnet.tag</code></td>
      <td><code>string</code></td>
      <td><code>latest</code></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.rabbitmq-libsonnet</code></td>
      <td><code>object</code></td>
      <td><em>(empty)</em></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.rabbitmq-libsonnet.closure</code></td>
      <td><code>array</code></td>
      <td><em>(empty)</em></td>
      <td>libraries this one imports; auto-enabled with it</td>
    </tr>
    <tr>
      <td><code>libraries.rabbitmq-libsonnet.enabled</code></td>
      <td><code>boolean</code></td>
      <td><code>false</code></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.rabbitmq-libsonnet.image</code></td>
      <td><code>string</code></td>
      <td><code>ghcr.io/metio/joi-jsonnet-libs-rabbitmq-libsonnet</code></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.rabbitmq-libsonnet.path</code></td>
      <td><code>string</code></td>
      <td><em>(empty)</em></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.rabbitmq-libsonnet.tag</code></td>
      <td><code>string</code></td>
      <td><code>latest</code></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.rabbitmq-messaging-topology-operator-libsonnet</code></td>
      <td><code>object</code></td>
      <td><em>(empty)</em></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.rabbitmq-messaging-topology-operator-libsonnet.closure</code></td>
      <td><code>array</code></td>
      <td><em>(empty)</em></td>
      <td>libraries this one imports; auto-enabled with it</td>
    </tr>
    <tr>
      <td><code>libraries.rabbitmq-messaging-topology-operator-libsonnet.enabled</code></td>
      <td><code>boolean</code></td>
      <td><code>false</code></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.rabbitmq-messaging-topology-operator-libsonnet.image</code></td>
      <td><code>string</code></td>
      <td><code>ghcr.io/metio/joi-jsonnet-libs-rabbitmq-messaging-topology-operator-libsonnet</code></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.rabbitmq-messaging-topology-operator-libsonnet.path</code></td>
      <td><code>string</code></td>
      <td><em>(empty)</em></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.rabbitmq-messaging-topology-operator-libsonnet.tag</code></td>
      <td><code>string</code></td>
      <td><code>latest</code></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.secrets-store-csi-driver-libsonnet</code></td>
      <td><code>object</code></td>
      <td><em>(empty)</em></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.secrets-store-csi-driver-libsonnet.closure</code></td>
      <td><code>array</code></td>
      <td><em>(empty)</em></td>
      <td>libraries this one imports; auto-enabled with it</td>
    </tr>
    <tr>
      <td><code>libraries.secrets-store-csi-driver-libsonnet.enabled</code></td>
      <td><code>boolean</code></td>
      <td><code>false</code></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.secrets-store-csi-driver-libsonnet.image</code></td>
      <td><code>string</code></td>
      <td><code>ghcr.io/metio/joi-jsonnet-libs-secrets-store-csi-driver-libsonnet</code></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.secrets-store-csi-driver-libsonnet.path</code></td>
      <td><code>string</code></td>
      <td><em>(empty)</em></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.secrets-store-csi-driver-libsonnet.tag</code></td>
      <td><code>string</code></td>
      <td><code>latest</code></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.securecodebox-libsonnet</code></td>
      <td><code>object</code></td>
      <td><em>(empty)</em></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.securecodebox-libsonnet.closure</code></td>
      <td><code>array</code></td>
      <td><em>(empty)</em></td>
      <td>libraries this one imports; auto-enabled with it</td>
    </tr>
    <tr>
      <td><code>libraries.securecodebox-libsonnet.enabled</code></td>
      <td><code>boolean</code></td>
      <td><code>false</code></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.securecodebox-libsonnet.image</code></td>
      <td><code>string</code></td>
      <td><code>ghcr.io/metio/joi-jsonnet-libs-securecodebox-libsonnet</code></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.securecodebox-libsonnet.path</code></td>
      <td><code>string</code></td>
      <td><em>(empty)</em></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.securecodebox-libsonnet.tag</code></td>
      <td><code>string</code></td>
      <td><code>latest</code></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.spicedb-operator-libsonnet</code></td>
      <td><code>object</code></td>
      <td><em>(empty)</em></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.spicedb-operator-libsonnet.closure</code></td>
      <td><code>array</code></td>
      <td><em>(empty)</em></td>
      <td>libraries this one imports; auto-enabled with it</td>
    </tr>
    <tr>
      <td><code>libraries.spicedb-operator-libsonnet.enabled</code></td>
      <td><code>boolean</code></td>
      <td><code>false</code></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.spicedb-operator-libsonnet.image</code></td>
      <td><code>string</code></td>
      <td><code>ghcr.io/metio/joi-jsonnet-libs-spicedb-operator-libsonnet</code></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.spicedb-operator-libsonnet.path</code></td>
      <td><code>string</code></td>
      <td><em>(empty)</em></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.spicedb-operator-libsonnet.tag</code></td>
      <td><code>string</code></td>
      <td><code>latest</code></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.strimzi-libsonnet</code></td>
      <td><code>object</code></td>
      <td><em>(empty)</em></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.strimzi-libsonnet.closure</code></td>
      <td><code>array</code></td>
      <td><em>(empty)</em></td>
      <td>libraries this one imports; auto-enabled with it</td>
    </tr>
    <tr>
      <td><code>libraries.strimzi-libsonnet.enabled</code></td>
      <td><code>boolean</code></td>
      <td><code>false</code></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.strimzi-libsonnet.image</code></td>
      <td><code>string</code></td>
      <td><code>ghcr.io/metio/joi-jsonnet-libs-strimzi-libsonnet</code></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.strimzi-libsonnet.path</code></td>
      <td><code>string</code></td>
      <td><em>(empty)</em></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.strimzi-libsonnet.tag</code></td>
      <td><code>string</code></td>
      <td><code>latest</code></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.tailscale-operator-libsonnet</code></td>
      <td><code>object</code></td>
      <td><em>(empty)</em></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.tailscale-operator-libsonnet.closure</code></td>
      <td><code>array</code></td>
      <td><em>(empty)</em></td>
      <td>libraries this one imports; auto-enabled with it</td>
    </tr>
    <tr>
      <td><code>libraries.tailscale-operator-libsonnet.enabled</code></td>
      <td><code>boolean</code></td>
      <td><code>false</code></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.tailscale-operator-libsonnet.image</code></td>
      <td><code>string</code></td>
      <td><code>ghcr.io/metio/joi-jsonnet-libs-tailscale-operator-libsonnet</code></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.tailscale-operator-libsonnet.path</code></td>
      <td><code>string</code></td>
      <td><em>(empty)</em></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.tailscale-operator-libsonnet.tag</code></td>
      <td><code>string</code></td>
      <td><code>latest</code></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.tektoncd-libsonnet</code></td>
      <td><code>object</code></td>
      <td><em>(empty)</em></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.tektoncd-libsonnet.closure</code></td>
      <td><code>array</code></td>
      <td><em>(empty)</em></td>
      <td>libraries this one imports; auto-enabled with it</td>
    </tr>
    <tr>
      <td><code>libraries.tektoncd-libsonnet.enabled</code></td>
      <td><code>boolean</code></td>
      <td><code>false</code></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.tektoncd-libsonnet.image</code></td>
      <td><code>string</code></td>
      <td><code>ghcr.io/metio/joi-jsonnet-libs-tektoncd-libsonnet</code></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.tektoncd-libsonnet.path</code></td>
      <td><code>string</code></td>
      <td><em>(empty)</em></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.tektoncd-libsonnet.tag</code></td>
      <td><code>string</code></td>
      <td><code>latest</code></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.teleport-operator-libsonnet</code></td>
      <td><code>object</code></td>
      <td><em>(empty)</em></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.teleport-operator-libsonnet.closure</code></td>
      <td><code>array</code></td>
      <td><em>(empty)</em></td>
      <td>libraries this one imports; auto-enabled with it</td>
    </tr>
    <tr>
      <td><code>libraries.teleport-operator-libsonnet.enabled</code></td>
      <td><code>boolean</code></td>
      <td><code>false</code></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.teleport-operator-libsonnet.image</code></td>
      <td><code>string</code></td>
      <td><code>ghcr.io/metio/joi-jsonnet-libs-teleport-operator-libsonnet</code></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.teleport-operator-libsonnet.path</code></td>
      <td><code>string</code></td>
      <td><em>(empty)</em></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.teleport-operator-libsonnet.tag</code></td>
      <td><code>string</code></td>
      <td><code>latest</code></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.testonnet</code></td>
      <td><code>object</code></td>
      <td><em>(empty)</em></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.testonnet.closure</code></td>
      <td><code>array</code></td>
      <td><em>(empty)</em></td>
      <td>libraries this one imports; auto-enabled with it</td>
    </tr>
    <tr>
      <td><code>libraries.testonnet.enabled</code></td>
      <td><code>boolean</code></td>
      <td><code>false</code></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.testonnet.image</code></td>
      <td><code>string</code></td>
      <td><code>ghcr.io/metio/joi-jsonnet-libs-testonnet</code></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.testonnet.path</code></td>
      <td><code>string</code></td>
      <td><em>(empty)</em></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.testonnet.tag</code></td>
      <td><code>string</code></td>
      <td><code>latest</code></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.tinkerbell-libsonnet</code></td>
      <td><code>object</code></td>
      <td><em>(empty)</em></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.tinkerbell-libsonnet.closure</code></td>
      <td><code>array</code></td>
      <td><em>(empty)</em></td>
      <td>libraries this one imports; auto-enabled with it</td>
    </tr>
    <tr>
      <td><code>libraries.tinkerbell-libsonnet.enabled</code></td>
      <td><code>boolean</code></td>
      <td><code>false</code></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.tinkerbell-libsonnet.image</code></td>
      <td><code>string</code></td>
      <td><code>ghcr.io/metio/joi-jsonnet-libs-tinkerbell-libsonnet</code></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.tinkerbell-libsonnet.path</code></td>
      <td><code>string</code></td>
      <td><em>(empty)</em></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.tinkerbell-libsonnet.tag</code></td>
      <td><code>string</code></td>
      <td><code>latest</code></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.traefik-libsonnet</code></td>
      <td><code>object</code></td>
      <td><em>(empty)</em></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.traefik-libsonnet.closure</code></td>
      <td><code>array</code></td>
      <td><em>(empty)</em></td>
      <td>libraries this one imports; auto-enabled with it</td>
    </tr>
    <tr>
      <td><code>libraries.traefik-libsonnet.enabled</code></td>
      <td><code>boolean</code></td>
      <td><code>false</code></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.traefik-libsonnet.image</code></td>
      <td><code>string</code></td>
      <td><code>ghcr.io/metio/joi-jsonnet-libs-traefik-libsonnet</code></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.traefik-libsonnet.path</code></td>
      <td><code>string</code></td>
      <td><em>(empty)</em></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.traefik-libsonnet.tag</code></td>
      <td><code>string</code></td>
      <td><code>latest</code></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.triggermesh-libsonnet</code></td>
      <td><code>object</code></td>
      <td><em>(empty)</em></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.triggermesh-libsonnet.closure</code></td>
      <td><code>array</code></td>
      <td><em>(empty)</em></td>
      <td>libraries this one imports; auto-enabled with it</td>
    </tr>
    <tr>
      <td><code>libraries.triggermesh-libsonnet.enabled</code></td>
      <td><code>boolean</code></td>
      <td><code>false</code></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.triggermesh-libsonnet.image</code></td>
      <td><code>string</code></td>
      <td><code>ghcr.io/metio/joi-jsonnet-libs-triggermesh-libsonnet</code></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.triggermesh-libsonnet.path</code></td>
      <td><code>string</code></td>
      <td><em>(empty)</em></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.triggermesh-libsonnet.tag</code></td>
      <td><code>string</code></td>
      <td><code>latest</code></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.trust-manager-libsonnet</code></td>
      <td><code>object</code></td>
      <td><em>(empty)</em></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.trust-manager-libsonnet.closure</code></td>
      <td><code>array</code></td>
      <td><em>(empty)</em></td>
      <td>libraries this one imports; auto-enabled with it</td>
    </tr>
    <tr>
      <td><code>libraries.trust-manager-libsonnet.enabled</code></td>
      <td><code>boolean</code></td>
      <td><code>false</code></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.trust-manager-libsonnet.image</code></td>
      <td><code>string</code></td>
      <td><code>ghcr.io/metio/joi-jsonnet-libs-trust-manager-libsonnet</code></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.trust-manager-libsonnet.path</code></td>
      <td><code>string</code></td>
      <td><em>(empty)</em></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.trust-manager-libsonnet.tag</code></td>
      <td><code>string</code></td>
      <td><code>latest</code></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.upbound-provider-opentofu-libsonnet</code></td>
      <td><code>object</code></td>
      <td><em>(empty)</em></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.upbound-provider-opentofu-libsonnet.closure</code></td>
      <td><code>array</code></td>
      <td><em>(empty)</em></td>
      <td>libraries this one imports; auto-enabled with it</td>
    </tr>
    <tr>
      <td><code>libraries.upbound-provider-opentofu-libsonnet.enabled</code></td>
      <td><code>boolean</code></td>
      <td><code>false</code></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.upbound-provider-opentofu-libsonnet.image</code></td>
      <td><code>string</code></td>
      <td><code>ghcr.io/metio/joi-jsonnet-libs-upbound-provider-opentofu-libsonnet</code></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.upbound-provider-opentofu-libsonnet.path</code></td>
      <td><code>string</code></td>
      <td><em>(empty)</em></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.upbound-provider-opentofu-libsonnet.tag</code></td>
      <td><code>string</code></td>
      <td><code>latest</code></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.vault-secrets-operator-libsonnet</code></td>
      <td><code>object</code></td>
      <td><em>(empty)</em></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.vault-secrets-operator-libsonnet.closure</code></td>
      <td><code>array</code></td>
      <td><em>(empty)</em></td>
      <td>libraries this one imports; auto-enabled with it</td>
    </tr>
    <tr>
      <td><code>libraries.vault-secrets-operator-libsonnet.enabled</code></td>
      <td><code>boolean</code></td>
      <td><code>false</code></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.vault-secrets-operator-libsonnet.image</code></td>
      <td><code>string</code></td>
      <td><code>ghcr.io/metio/joi-jsonnet-libs-vault-secrets-operator-libsonnet</code></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.vault-secrets-operator-libsonnet.path</code></td>
      <td><code>string</code></td>
      <td><em>(empty)</em></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.vault-secrets-operator-libsonnet.tag</code></td>
      <td><code>string</code></td>
      <td><code>latest</code></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.vertical-pod-autoscaler-libsonnet</code></td>
      <td><code>object</code></td>
      <td><em>(empty)</em></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.vertical-pod-autoscaler-libsonnet.closure</code></td>
      <td><code>array</code></td>
      <td><em>(empty)</em></td>
      <td>libraries this one imports; auto-enabled with it</td>
    </tr>
    <tr>
      <td><code>libraries.vertical-pod-autoscaler-libsonnet.enabled</code></td>
      <td><code>boolean</code></td>
      <td><code>false</code></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.vertical-pod-autoscaler-libsonnet.image</code></td>
      <td><code>string</code></td>
      <td><code>ghcr.io/metio/joi-jsonnet-libs-vertical-pod-autoscaler-libsonnet</code></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.vertical-pod-autoscaler-libsonnet.path</code></td>
      <td><code>string</code></td>
      <td><em>(empty)</em></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.vertical-pod-autoscaler-libsonnet.tag</code></td>
      <td><code>string</code></td>
      <td><code>latest</code></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.victoria-metrics-operator-libsonnet</code></td>
      <td><code>object</code></td>
      <td><em>(empty)</em></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.victoria-metrics-operator-libsonnet.closure</code></td>
      <td><code>array</code></td>
      <td><em>(empty)</em></td>
      <td>libraries this one imports; auto-enabled with it</td>
    </tr>
    <tr>
      <td><code>libraries.victoria-metrics-operator-libsonnet.enabled</code></td>
      <td><code>boolean</code></td>
      <td><code>false</code></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.victoria-metrics-operator-libsonnet.image</code></td>
      <td><code>string</code></td>
      <td><code>ghcr.io/metio/joi-jsonnet-libs-victoria-metrics-operator-libsonnet</code></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.victoria-metrics-operator-libsonnet.path</code></td>
      <td><code>string</code></td>
      <td><em>(empty)</em></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.victoria-metrics-operator-libsonnet.tag</code></td>
      <td><code>string</code></td>
      <td><code>latest</code></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.xtd</code></td>
      <td><code>object</code></td>
      <td><em>(empty)</em></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.xtd.closure</code></td>
      <td><code>array</code></td>
      <td><em>(empty)</em></td>
      <td>libraries this one imports; auto-enabled with it</td>
    </tr>
    <tr>
      <td><code>libraries.xtd.enabled</code></td>
      <td><code>boolean</code></td>
      <td><code>false</code></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.xtd.image</code></td>
      <td><code>string</code></td>
      <td><code>ghcr.io/metio/joi-jsonnet-libs-xtd</code></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.xtd.path</code></td>
      <td><code>string</code></td>
      <td><em>(empty)</em></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.xtd.tag</code></td>
      <td><code>string</code></td>
      <td><code>latest</code></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.zalando-postgres-operator-libsonnet</code></td>
      <td><code>object</code></td>
      <td><em>(empty)</em></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.zalando-postgres-operator-libsonnet.closure</code></td>
      <td><code>array</code></td>
      <td><em>(empty)</em></td>
      <td>libraries this one imports; auto-enabled with it</td>
    </tr>
    <tr>
      <td><code>libraries.zalando-postgres-operator-libsonnet.enabled</code></td>
      <td><code>boolean</code></td>
      <td><code>false</code></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.zalando-postgres-operator-libsonnet.image</code></td>
      <td><code>string</code></td>
      <td><code>ghcr.io/metio/joi-jsonnet-libs-zalando-postgres-operator-libsonnet</code></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.zalando-postgres-operator-libsonnet.path</code></td>
      <td><code>string</code></td>
      <td><em>(empty)</em></td>
      <td></td>
    </tr>
    <tr>
      <td><code>libraries.zalando-postgres-operator-libsonnet.tag</code></td>
      <td><code>string</code></td>
      <td><code>latest</code></td>
      <td></td>
    </tr>
    <tr>
      <td><code>namespace</code></td>
      <td><code>string</code></td>
      <td><em>(empty)</em></td>
      <td>Namespace the OCIRepository &#43; JsonnetLibrary objects land in (release ns when empty).</td>
    </tr>
    <tr>
      <td><code>registryMirror</code></td>
      <td><code>string</code></td>
      <td><em>(empty)</em></td>
      <td>Internal registry mirror for the JOI OCI artifacts (rewrites the registry host).</td>
    </tr>
  </tbody>
</table>
]]></content><category scheme="https://jaas.projects.metio.wtf/tags/installation" term="installation" label="installation"/><category scheme="https://jaas.projects.metio.wtf/tags/helm" term="helm" label="helm"/><category scheme="https://jaas.projects.metio.wtf/tags/chart" term="chart" label="chart"/><category scheme="https://jaas.projects.metio.wtf/tags/values" term="values" label="values"/><category scheme="https://jaas.projects.metio.wtf/tags/reference" term="reference" label="reference"/></entry><entry><title type="html">High reconcile latency</title><link href="https://jaas.projects.metio.wtf/runbooks/reconcile-latency/?utm_source=atom_feed" rel="alternate" type="text/html"/><link href="https://jaas.projects.metio.wtf/runbooks/workqueue-saturation/?utm_source=atom_feed" rel="related" type="text/html" title="Workqueue saturation"/><link href="https://jaas.projects.metio.wtf/runbooks/artifacttoolarge/?utm_source=atom_feed" rel="related" type="text/html" title="ArtifactTooLarge"/><link href="https://jaas.projects.metio.wtf/runbooks/crd-watch-engagement/?utm_source=atom_feed" rel="related" type="text/html" title="CRD watch engagement failing"/><link href="https://jaas.projects.metio.wtf/runbooks/crossnamespacerefrejected/?utm_source=atom_feed" rel="related" type="text/html" title="CrossNamespaceRefRejected"/><link href="https://jaas.projects.metio.wtf/runbooks/dependencycycle/?utm_source=atom_feed" rel="related" type="text/html" title="DependencyCycle"/><id>https://jaas.projects.metio.wtf/runbooks/reconcile-latency/</id><published>0001-01-01T00:00:00+00:00</published><updated>2026-06-17T10:56:50+02:00</updated><content type="html"><![CDATA[<blockquote>Individual reconcile calls are taking longer than the configured p99 threshold, indicating slow source fetches, heavy evaluation, or a sluggish storage backend</blockquote><p>Linked from the <code>JaaSReconcileLatencyHigh</code> alert. Fires when the controller-runtime <code>controller_runtime_reconcile_time_seconds</code> histogram p99 exceeds the configured threshold (default 30s) for the alert window.</p>
<h2 id="symptom">Symptom</h2>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">ALERTS{alertname=&#34;JaaSReconcileLatencyHigh&#34;, controller=&#34;jsonnetsnippet&#34;}
</span></span></code></pre></div><ul>
<li><code>kubectl get jsonnetsnippet</code> shows status updates trickling in well after spec changes.</li>
<li>Operator pod CPU is moderate-to-high but the queue is draining (distinguishes this from <a href="/runbooks/workqueue-saturation/">workqueue-saturation.md</a>
, where the queue itself is growing).</li>
</ul>
<h2 id="cause">Cause</h2>
<p>Reconcile latency is the wall-clock cost of one <code>Reconcile()</code> call. Inside the call, JaaS does (in order):</p>
<ol>
<li><code>Get</code> the snippet from the cache.</li>
<li>Run the dependency-cycle BFS (one Get per touched node).</li>
<li>Resolve the source (inline files, or sourceRef → Fetcher: source-CR Get + tarball HTTP fetch + tar extract).</li>
<li>Resolve libraries (one Get per <code>LibraryRef</code>).</li>
<li>Evaluate the snippet via go-jsonnet.</li>
<li>Publish via the storage backend (<code>Put</code>).</li>
<li>Status update + ExternalArtifact upsert.</li>
</ol>
<p>Slow reconciles are almost always one of:</p>
<ul>
<li><strong>Slow <code>Fetcher</code></strong> — a large tarball over a slow network, or a misbehaving source-controller (digest mismatch retries).</li>
<li><strong>Heavy jsonnet evaluation</strong> — a snippet that imports lots of large libraries or runs unbounded recursion below the stack limit.</li>
<li><strong>Slow <code>Publisher</code></strong> — S3 throttling, a slow PVC, or large rendered output (close to <code>--max-artifact-bytes</code>).</li>
<li><strong>Cycle-detection blowup</strong> — a dense graph of snippets cross-referencing via <code>sourceRef</code>. The BFS is O(V+E) but each visit is a <code>Get</code>.</li>
</ul>
<h2 id="diagnosis">Diagnosis</h2>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl"><span class="c1"># Where is the time going? OTel spans break Reconcile into sub-stages.</span>
</span></span><span class="line"><span class="cl"><span class="c1"># Requires --tracing-endpoint set on the operator.</span>
</span></span><span class="line"><span class="cl">kubectl --namespace &lt;jaas-ns&gt; get deploy jaas <span class="se">\
</span></span></span><span class="line"><span class="cl">  --output <span class="nv">jsonpath</span><span class="o">=</span><span class="s1">&#39;{.spec.template.spec.containers[0].args}&#39;</span> <span class="se">\
</span></span></span><span class="line"><span class="cl">  <span class="p">|</span> tr <span class="s1">&#39;,&#39;</span> <span class="s1">&#39;\n&#39;</span> <span class="p">|</span> grep tracing
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Without tracing: the histograms expose enough to triangulate.</span>
</span></span><span class="line"><span class="cl">kubectl --namespace &lt;jaas-ns&gt; port-forward svc/jaas-metrics 8083:8083 <span class="p">&amp;</span>
</span></span><span class="line"><span class="cl">curl -s localhost:8083/metrics <span class="p">|</span> grep -E <span class="s1">&#39;reconcile_time|rendered_bytes&#39;</span>
</span></span></code></pre></div><p>The <code>jaas_snippet_rendered_bytes</code> histogram tells you whether a slow Publisher is the cause (large outputs) vs. a slow Fetcher (small outputs but the histogram is dominated by upstream IO).</p>
<p>For a single suspect snippet, force a reconcile under load and observe:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">kubectl annotate jsonnetsnippet &lt;ns&gt;/&lt;name&gt; <span class="se">\
</span></span></span><span class="line"><span class="cl">  jaas.metio.wtf/reconcile-at<span class="o">=</span><span class="k">$(</span>date -u +%FT%TZ<span class="k">)</span> --overwrite
</span></span><span class="line"><span class="cl">kubectl --namespace &lt;jaas-ns&gt; logs deploy/jaas --tail<span class="o">=</span><span class="m">50</span> <span class="p">|</span> grep &lt;name&gt;
</span></span></code></pre></div><h2 id="remediation">Remediation</h2>
<ul>
<li><strong>Slow Fetcher.</strong> Narrow <code>spec.sourceRef.path</code> to the subdirectory the snippet actually needs. Tarballs balloon when an entire monorepo is published; the filter trims what JaaS has to download.</li>
<li><strong>Heavy eval.</strong> Cap <code>--max-stack</code> to bound runaway recursion. Profile the snippet locally via <code>jsonnet</code> (the CLI) — the operator&rsquo;s evaluation is identical.</li>
<li><strong>Slow Publisher.</strong> See <a href="/runbooks/storage-recovery/">storage-recovery.md</a>
 for backend-specific tuning.</li>
<li><strong>Cycle-detection blowup.</strong> Reorganize snippets so the cross-reference graph is shallow; cycle detection visits every reachable node, so a fan-out of N snippets multiplies the cost.</li>
<li><strong>OTel for forensics.</strong> Enable <code>--tracing-endpoint</code> and the per-stage spans turn this from guessing into measurement. The chart values key is <code>operator.tracing.endpoint</code>.</li>
</ul>
<h2 id="prevention">Prevention</h2>
<ul>
<li>Pair the alert with <code>JaaSSnippetArtifactGrowing</code> (<a href="/runbooks/artifacttoolarge/">artifacttoolarge.md</a>
). A snippet whose rendered bytes are climbing is almost always headed for a latency spike too.</li>
<li>For multi-replica HA, leader election keeps only one replica in the reconcile loop — sustained latency on the lease-holder is what matters; standby latency is not measured.</li>
</ul>
]]></content><category scheme="https://jaas.projects.metio.wtf/tags/runbooks" term="runbooks" label="runbooks"/><category scheme="https://jaas.projects.metio.wtf/tags/troubleshooting" term="troubleshooting" label="troubleshooting"/><category scheme="https://jaas.projects.metio.wtf/tags/metrics" term="metrics" label="metrics"/></entry><entry><title type="html">InvalidSpec</title><link href="https://jaas.projects.metio.wtf/runbooks/invalidspec/?utm_source=atom_feed" rel="alternate" type="text/html"/><link href="https://jaas.projects.metio.wtf/runbooks/crd-watch-engagement/?utm_source=atom_feed" rel="related" type="text/html" title="CRD watch engagement failing"/><link href="https://jaas.projects.metio.wtf/runbooks/operator-pod-down/?utm_source=atom_feed" rel="related" type="text/html" title="Operator pod not ready"/><link href="https://jaas.projects.metio.wtf/runbooks/pending/?utm_source=atom_feed" rel="related" type="text/html" title="Pending"/><link href="https://jaas.projects.metio.wtf/runbooks/suspended/?utm_source=atom_feed" rel="related" type="text/html" title="Suspended"/><link href="https://jaas.projects.metio.wtf/runbooks/synced/?utm_source=atom_feed" rel="related" type="text/html" title="Synced"/><id>https://jaas.projects.metio.wtf/runbooks/invalidspec/</id><published>0001-01-01T00:00:00+00:00</published><updated>2026-06-16T20:48:39+02:00</updated><content type="html"><![CDATA[<blockquote>The snippet&rsquo;s spec contains a field combination the reconciler cannot process, such as a missing entryFile or conflicting source fields</blockquote><h2 id="symptom">Symptom</h2>
<p><code>READY=False</code>, <code>REASON=InvalidSpec</code>. The condition Message names which field is at fault.</p>
<h2 id="cause">Cause</h2>
<p>Spec-level validation that admission should have caught but the reconciler is enforcing as a fallback:</p>
<ul>
<li><code>spec.entryFile</code> is empty</li>
<li>both <code>spec.files</code> and <code>spec.sourceRef</code> are set (mutually exclusive)</li>
<li>neither <code>spec.files</code> nor <code>spec.sourceRef</code> is set</li>
<li><code>spec.entryFile</code> does not match any key in the resolved file map</li>
</ul>
<h2 id="diagnosis">Diagnosis</h2>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">kubectl describe jsonnetsnippet &lt;name&gt;
</span></span></code></pre></div><p>Read the Message — it names the field.</p>
<h2 id="remediation">Remediation</h2>
<p>Fix the spec and reapply. If the validating webhook is enabled (<code>--enable-webhook</code>), <code>kubectl apply</code> rejects the invalid spec at admission instead of letting it land and fail later.</p>
<p>If you&rsquo;re seeing <code>InvalidSpec</code> on apply through the webhook, that&rsquo;s a bug — file an issue with the rejected manifest.</p>
]]></content><category scheme="https://jaas.projects.metio.wtf/tags/runbooks" term="runbooks" label="runbooks"/><category scheme="https://jaas.projects.metio.wtf/tags/troubleshooting" term="troubleshooting" label="troubleshooting"/><category scheme="https://jaas.projects.metio.wtf/tags/lifecycle" term="lifecycle" label="lifecycle"/></entry><entry><title type="html">JaaS and grafana-operator</title><link href="https://jaas.projects.metio.wtf/comparisons/grafana-operator/?utm_source=atom_feed" rel="alternate" type="text/html"/><link href="https://jaas.projects.metio.wtf/comparisons/tanka/?utm_source=atom_feed" rel="related" type="text/html" title="JaaS vs Tanka"/><link href="https://jaas.projects.metio.wtf/comparisons/jsonnet-controller/?utm_source=atom_feed" rel="related" type="text/html" title="JaaS vs jsonnet-controller"/><link href="https://jaas.projects.metio.wtf/comparisons/jsonnet-cli/?utm_source=atom_feed" rel="related" type="text/html" title="JaaS vs the jsonnet CLI"/><id>https://jaas.projects.metio.wtf/comparisons/grafana-operator/</id><published>0001-01-01T00:00:00+00:00</published><updated>2026-06-16T20:48:39+02:00</updated><content type="html"><![CDATA[<blockquote>How JaaS renders Grafana dashboard JSON from Jsonnet while grafana-operator reconciles it into Grafana.</blockquote><p>JaaS and the <a href="https://grafana.github.io/grafana-operator/">grafana-operator</a>
 are
not alternatives — they do different jobs and are commonly used together. JaaS
<em>produces</em> dashboard JSON from Jsonnet; grafana-operator <em>consumes</em> dashboard
JSON and reconciles it into a Grafana instance.</p>
<h2 id="division-of-labour">Division of labour</h2>
<p>grafana-operator manages Grafana itself. It reconciles <code>GrafanaDashboard</code>,
<code>GrafanaDatasource</code>, <code>GrafanaFolder</code>, and related resources into one or more
Grafana instances, handling authentication, folder placement, datasource wiring,
and drift correction inside Grafana. A <code>GrafanaDashboard</code> can take its dashboard
model from inline JSON, a URL, a ConfigMap, a <code>grafana.com</code> dashboard ID, or a
remote source.</p>
<p>JaaS evaluates Jsonnet — including <a href="https://grafana.github.io/grafonnet/">grafonnet</a>

— and publishes the rendered dashboard JSON as a Flux <code>ExternalArtifact</code>. It
knows nothing about Grafana: it renders JSON and stops there.</p>
<p>So the two compose along a clean seam. You author dashboards in grafonnet, JaaS
renders them to JSON, and grafana-operator takes that JSON and reconciles it into
Grafana. Each tool owns one half of the pipeline and neither reaches into the
other&rsquo;s domain.</p>
<h2 id="when-grafana-operator-alone-is-enough">When grafana-operator alone is enough</h2>
<p>If your dashboards are already plain JSON, or you consume them by <code>grafana.com</code>
dashboard ID, or you maintain them in the Grafana UI and export the model, then
grafana-operator covers the whole workflow on its own. There is no Jsonnet to
render, so there is nothing for JaaS to do. Reach for grafana-operator by itself
whenever the dashboard model exists as static JSON.</p>
<h2 id="when-to-add-jaas">When to add JaaS</h2>
<p>Add JaaS when your dashboards are authored in grafonnet (or any Jsonnet),
typically to share panels, variables, and layout helpers across many dashboards
instead of duplicating JSON. JaaS turns that Jsonnet into the JSON
grafana-operator expects, with the same <code>jsonnet -J vendor</code> import resolution
you use locally, so a dashboard renders identically on your workstation and
in-cluster. grafana-operator then reconciles the rendered output as it would any
other dashboard JSON.</p>
<h2 id="wiring-them-together">Wiring them together</h2>
<p>The grafana-operator project documents the JaaS integration directly, including
the <code>GrafanaDashboard</code> configuration that points at a JaaS-rendered artifact:
<a href="https://grafana.github.io/grafana-operator/docs/examples/dashboard/jaas/readme/">grafana-operator dashboard example with JaaS</a>
.
Keep all <code>GrafanaDashboard</code>, datasource, and folder configuration on the
grafana-operator side; JaaS contributes only the rendering step and the
<code>ExternalArtifact</code> it publishes.</p>
<p>The <a href="/tutorials/grafana-dashboards/">Grafana dashboards</a>
 tutorial shows the JaaS
side — authoring a grafonnet dashboard as a <code>JsonnetSnippet</code> and publishing the
rendered JSON.</p>
]]></content><category scheme="https://jaas.projects.metio.wtf/tags/comparison" term="comparison" label="comparison"/><category scheme="https://jaas.projects.metio.wtf/tags/grafana-operator" term="grafana-operator" label="grafana-operator"/></entry><entry><title type="html">JaaS vs jsonnet-controller</title><link href="https://jaas.projects.metio.wtf/comparisons/jsonnet-controller/?utm_source=atom_feed" rel="alternate" type="text/html"/><link href="https://jaas.projects.metio.wtf/comparisons/grafana-operator/?utm_source=atom_feed" rel="related" type="text/html" title="JaaS and grafana-operator"/><link href="https://jaas.projects.metio.wtf/comparisons/tanka/?utm_source=atom_feed" rel="related" type="text/html" title="JaaS vs Tanka"/><link href="https://jaas.projects.metio.wtf/comparisons/jsonnet-cli/?utm_source=atom_feed" rel="related" type="text/html" title="JaaS vs the jsonnet CLI"/><id>https://jaas.projects.metio.wtf/comparisons/jsonnet-controller/</id><published>0001-01-01T00:00:00+00:00</published><updated>2026-06-16T20:48:39+02:00</updated><content type="html"><![CDATA[<blockquote>How JaaS separates rendering from deployment, where jsonnet-controller builds and applies Jsonnet in one controller.</blockquote><p><a href="https://github.com/pelotech/jsonnet-controller">pelotech/jsonnet-controller</a>
 is
a Flux-style controller that builds Jsonnet inside the controller and applies the
result to the cluster, with a configuration model compatible with
<a href="https://github.com/kubecfg/kubecfg">kubecfg</a>
. JaaS and jsonnet-controller both
turn Jsonnet into Kubernetes objects under Flux, but they draw the boundary
between rendering and deployment in different places.</p>
<h2 id="coupled-build-and-apply-vs-a-rendering-service">Coupled build-and-apply vs a rendering service</h2>
<p>jsonnet-controller couples the two halves: one controller reads a Jsonnet
source, evaluates it, and applies the resulting objects to the cluster. The
rendered output lives inside the controller&rsquo;s reconcile loop; the unit of
configuration is &ldquo;build this Jsonnet and apply it here.&rdquo;</p>
<p>JaaS separates them. The JaaS operator renders a <code>JsonnetSnippet</code> and publishes
the result as a content-addressed Flux <code>ExternalArtifact</code> — a tarball any
source-controller-speaking consumer can fetch. JaaS does not apply anything. A
separate consumer (a Flux <code>Kustomization</code>, a <code>HelmRelease</code>, or a
<a href="https://stageset.projects.metio.wtf/">stageset-controller</a>
 <code>StageSet</code>) reads the
artifact and applies it. The rendered bytes are a first-class, addressable object
that more than one consumer can reference, pin to a revision, or roll back to.</p>
<p>That same rendering is also reachable over HTTP, so callers that are not Flux
consumers — a CI step, another service — can request a render from the same
engine that produces the in-cluster artifacts. See
<a href="/usage/operator-mode/">operator mode</a>
 for how a snippet becomes an artifact.</p>
<h2 id="where-jsonnet-controller-fits">Where jsonnet-controller fits</h2>
<p>jsonnet-controller is the more direct choice when its model matches your needs:</p>
<ul>
<li><strong>kubecfg compatibility.</strong> If you already organise Jsonnet the kubecfg way —
its import conventions, its top-level structure — jsonnet-controller consumes
that directly without restructuring.</li>
<li><strong>One object per build-and-apply.</strong> When you want a single Flux-style
resource that both renders a source and applies it, with no intermediate
artifact to manage, jsonnet-controller keeps the pipeline to one moving part.</li>
</ul>
<p>JaaS is the better fit when you want the rendered output to be an addressable,
revisioned artifact that several consumers can share, when you want the same
renderer available over HTTP to non-Flux callers, or when you want rendering and
deployment owned by separate, independently-evolving controllers.</p>
<h2 id="the-deployment-side-comparison">The deployment-side comparison</h2>
<p>The comparison above is from the rendering angle — rendering as a service that
produces an artifact, versus build-and-apply in one controller. For the
deployment-side comparison against jsonnet-controller — ordered and gated apply,
health gating between stages, rollback — see stageset-controller&rsquo;s own page:
<a href="https://stageset.projects.metio.wtf/comparisons/jsonnet-controller/">stageset-controller vs jsonnet-controller</a>
.</p>
]]></content><category scheme="https://jaas.projects.metio.wtf/tags/comparison" term="comparison" label="comparison"/><category scheme="https://jaas.projects.metio.wtf/tags/jsonnet-controller" term="jsonnet-controller" label="jsonnet-controller"/></entry><entry><title type="html">JaaS vs Tanka</title><link href="https://jaas.projects.metio.wtf/comparisons/tanka/?utm_source=atom_feed" rel="alternate" type="text/html"/><link href="https://jaas.projects.metio.wtf/comparisons/grafana-operator/?utm_source=atom_feed" rel="related" type="text/html" title="JaaS and grafana-operator"/><link href="https://jaas.projects.metio.wtf/comparisons/jsonnet-controller/?utm_source=atom_feed" rel="related" type="text/html" title="JaaS vs jsonnet-controller"/><link href="https://jaas.projects.metio.wtf/comparisons/jsonnet-cli/?utm_source=atom_feed" rel="related" type="text/html" title="JaaS vs the jsonnet CLI"/><id>https://jaas.projects.metio.wtf/comparisons/tanka/</id><published>0001-01-01T00:00:00+00:00</published><updated>2026-06-16T20:48:39+02:00</updated><content type="html"><![CDATA[<blockquote>How JaaS replaces a client-side <code>tk apply</code> workflow with server-side rendering and a Flux pull loop.</blockquote><p><a href="https://tanka.dev">Grafana Tanka</a>
 and JaaS both render Kubernetes manifests
from Jsonnet, and both build on the same <a href="https://github.com/jsonnet-bundler/jsonnet-bundler"><code>jsonnet-bundler</code></a>

vendoring conventions. The difference is <em>where the rendering runs and how the
result reaches the cluster</em>.</p>
<h2 id="the-two-models">The two models</h2>
<p>Tanka renders and applies from a developer workstation or a CI runner. You
organise your code as environments (<code>environments/&lt;env&gt;/main.jsonnet</code> plus a
<code>spec.json</code>), run <code>tk show</code> or <code>tk export</code> to inspect the rendered objects, and
<code>tk apply</code> to push them to the cluster the environment names in its <code>apiServer</code>
field. The workstation or CI runner needs the Jsonnet toolchain, the vendored
library tree, and credentials for the target cluster.</p>
<p>JaaS renders in-cluster. A <code>JsonnetSnippet</code> names the same Jsonnet entry file;
the JaaS operator evaluates it continuously and publishes the result as a Flux
<code>ExternalArtifact</code>. A Flux <code>Kustomization</code> (or <code>HelmRelease</code>, or for ordered
rollouts a <a href="https://stageset.projects.metio.wtf/">stageset-controller</a>

<code>StageSet</code>) consumes that artifact and applies it through the cluster&rsquo;s own
GitOps pull loop. No workstation or CI runner holds cluster credentials, and
developers do not need the Jsonnet toolchain or the vendor tree to ship a
change.</p>
<h2 id="when-tanka-is-the-better-fit">When Tanka is the better fit</h2>
<p>Tanka stays the stronger choice when its model matches the work:</p>
<ul>
<li><strong>Ad-hoc and exploratory renders.</strong> <code>tk show</code> / <code>tk diff</code> give an immediate,
local preview of exactly what would be applied, with no operator, no
<code>ExternalArtifact</code>, and no consumer to configure.</li>
<li><strong>Environments-as-code as the organising abstraction.</strong> Tanka&rsquo;s
environment/<code>spec.json</code> model — <code>namespace</code>, <code>injectLabels</code>, <code>apiServer</code>
per environment, <code>tk env list</code> to enumerate them — is a first-class feature
with no direct JaaS equivalent; JaaS pushes namespace and label concerns down
to the consuming Flux <code>Kustomization</code> instead.</li>
<li><strong>Direct, imperative apply.</strong> When a human running <code>tk apply</code> against a
named cluster is the intended workflow — small teams, bootstrap steps,
break-glass operations — a pull loop adds machinery you may not need.</li>
</ul>
<p>JaaS becomes the better fit when you want a pull-based GitOps loop, continuous
reconciliation and drift correction, server-side rendering so laptops and CI
hold no cluster credentials, per-tenant RBAC isolation on each render, and —
through stageset-controller — ordered, gated progressive delivery.</p>
<h2 id="your-imports-resolve-identically">Your imports resolve identically</h2>
<p>JaaS&rsquo;s importer implements the <strong>same resolution as <code>jsonnet -J vendor</code></strong>, the
scheme Tanka uses. A bare <code>import 'foo/main.libsonnet'</code> finds the library by
alias; an absolute <code>import 'github.com/.../gen/...'</code> resolves against the
vendored tree; sibling and <code>../</code> relative imports resolve from the importing
file. A <code>jb</code>-vendored tree (k8s-libsonnet, grafonnet, and the like) renders the
same bytes through JaaS as it does under <code>tk show</code>. Migration is mostly about
<em>where the files live</em>, not <em>rewriting Jsonnet</em>. See
<a href="/usage/jsonnet-libraries/">Jsonnet libraries</a>
 for how libraries reach a
snippet.</p>
<p>One behaviour to plan for: Tanka walks the evaluated object and extracts every
nested <code>{apiVersion, kind, …}</code> into a resource stream. JaaS publishes exactly
what the entry file evaluates to. Make the entry file emit a flat manifest
stream — wrap resources in a <code>v1</code> <code>List</code>, or apply <code>std.objectValues(...)</code> over
your Tanka object — so the consuming Flux <code>Kustomization</code> applies every
resource.</p>
<h2 id="a-migration-path">A migration path</h2>
<table>
	<thead>
			<tr>
					<th>Tanka</th>
					<th>JaaS / Flux</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td><code>vendor/</code> (jb-installed libs)</td>
					<td><code>JsonnetLibrary</code> with a <code>sourceRef</code>, or OCI-mounted libraries</td>
			</tr>
			<tr>
					<td><code>lib/</code> (project-shared libs)</td>
					<td><code>JsonnetLibrary</code> with inline <code>files</code> in the same namespace</td>
			</tr>
			<tr>
					<td><code>environments/&lt;env&gt;/main.jsonnet</code></td>
					<td>one <code>JsonnetSnippet</code> (<code>spec.entryFile</code> + <code>spec.files</code>/<code>spec.sourceRef</code>)</td>
			</tr>
			<tr>
					<td><code>import</code> resolution (<code>-J vendor</code>)</td>
					<td>identical — JaaS&rsquo;s in-memory importer</td>
			</tr>
			<tr>
					<td>per-env <code>spec.json</code> / conditionals</td>
					<td><code>spec.externalVariables</code> (<code>std.extVar</code>) and <code>spec.tlas</code> (top-level args)</td>
			</tr>
			<tr>
					<td><code>spec.json</code> <code>namespace</code></td>
					<td>Flux <code>Kustomization.spec.targetNamespace</code></td>
			</tr>
			<tr>
					<td><code>spec.json</code> <code>injectLabels</code></td>
					<td>Flux <code>Kustomization.spec.commonMetadata.labels</code></td>
			</tr>
			<tr>
					<td><code>tk show</code> / <code>tk export</code></td>
					<td>the JaaS operator, continuously → <code>ExternalArtifact</code></td>
			</tr>
			<tr>
					<td><code>tk apply</code></td>
					<td>Flux kustomize-/helm-controller (pull)</td>
			</tr>
			<tr>
					<td><code>tk diff</code></td>
					<td>Flux drift detection; stageset verification between stages</td>
			</tr>
			<tr>
					<td><code>tk env list</code></td>
					<td><code>kubectl get jsonnetsnippets -A</code></td>
			</tr>
	</tbody>
</table>
<p>The conversion in three moves:</p>
<ol>
<li>
<p><strong>Move the libraries.</strong> Shared, versioned libraries (k8s-libsonnet,
grafonnet) become a <code>JsonnetLibrary</code> backed by an <code>OCIRepository</code>, or a
static OCI-mounted library on the operator. A project-local <code>lib/</code> becomes a
<code>JsonnetLibrary</code> with inline <code>files</code>. The import alias is preserved either
way.</p>
</li>
<li>
<p><strong>Turn each environment into a <code>JsonnetSnippet</code>.</strong> <code>environments/team-a/main.jsonnet</code>
becomes one snippet. Prefer <code>spec.sourceRef</code> (a Flux
<code>GitRepository</code>/<code>OCIRepository</code>/<code>Bucket</code>) over inline <code>files</code> for real
repositories, so Flux versions the source and JaaS re-renders on every
commit. Per-environment differences move to <code>spec.externalVariables</code> and
<code>spec.tlas</code>, so two environments sharing one library become two snippets that
differ only in those fields.</p>
</li>
<li>
<p><strong>Replace apply with GitOps.</strong> <code>tk apply</code> goes away. A Flux <code>Kustomization</code>
points its <code>sourceRef</code> at the snippet&rsquo;s <code>ExternalArtifact</code>; Flux applies it
and reconciles it continuously. The
<a href="/tutorials/deploying-manifests/">Deploying manifests</a>
 tutorial walks this
end to end.</p>
</li>
</ol>
<h2 id="what-changes">What changes</h2>
<p>There is no single <code>tk diff</code> preview — use Flux&rsquo;s drift detection and
stageset&rsquo;s between-stage verification instead. Namespace and label injection moves from
<code>spec.json</code> to the consuming Flux <code>Kustomization</code>, where both are first-class
fields. Your Jsonnet and libraries come across unchanged.</p>
]]></content><category scheme="https://jaas.projects.metio.wtf/tags/comparison" term="comparison" label="comparison"/><category scheme="https://jaas.projects.metio.wtf/tags/tanka" term="tanka" label="tanka"/></entry><entry><title type="html">JaaS vs the jsonnet CLI</title><link href="https://jaas.projects.metio.wtf/comparisons/jsonnet-cli/?utm_source=atom_feed" rel="alternate" type="text/html"/><link href="https://jaas.projects.metio.wtf/comparisons/grafana-operator/?utm_source=atom_feed" rel="related" type="text/html" title="JaaS and grafana-operator"/><link href="https://jaas.projects.metio.wtf/comparisons/tanka/?utm_source=atom_feed" rel="related" type="text/html" title="JaaS vs Tanka"/><link href="https://jaas.projects.metio.wtf/comparisons/jsonnet-controller/?utm_source=atom_feed" rel="related" type="text/html" title="JaaS vs jsonnet-controller"/><id>https://jaas.projects.metio.wtf/comparisons/jsonnet-cli/</id><published>0001-01-01T00:00:00+00:00</published><updated>2026-06-16T20:48:39+02:00</updated><content type="html"><![CDATA[<blockquote>What the JaaS service adds over running the jsonnet and jb command-line tools yourself.</blockquote><p>The <a href="https://jsonnet.org/"><code>jsonnet</code></a>
 command-line tool — usually paired with
<a href="https://github.com/jsonnet-bundler/jsonnet-bundler"><code>jsonnet-bundler</code></a>
 (<code>jb</code>)
for vendoring libraries — evaluates Jsonnet to JSON on your machine. JaaS runs
the <strong>same go-jsonnet core</strong> as a service. This is not a question of which
implementation is correct; it is a question of <em>where the evaluation runs and
what surrounds it</em>.</p>
<h2 id="what-the-service-adds">What the service adds</h2>
<p>Over a local binary invocation, JaaS adds:</p>
<ul>
<li><strong>An HTTP endpoint other systems can call.</strong> <code>GET /jsonnet/&lt;snippet&gt;</code> returns
the evaluated JSON, with Top Level Arguments supplied as query parameters and
external variables configured on the service. Anything that speaks HTTP can
request a render without installing the toolchain or the vendor tree. See the
<a href="/usage/rendering-endpoint/">rendering endpoint</a>
 usage page.</li>
<li><strong>An operator that turns a snippet into a revisioned Flux artifact.</strong> With
<code>--enable-flux-integration</code>, a <code>JsonnetSnippet</code> is evaluated continuously and
published as a content-addressed <code>ExternalArtifact</code> that Flux consumers apply
in-cluster — re-rendered automatically when its source changes. See
<a href="/usage/operator-mode/">operator mode</a>
.</li>
<li><strong>Import resolution that matches <code>jsonnet -J vendor</code>.</strong> JaaS resolves imports
with the same semantics as the CLI&rsquo;s JPATH/vendor search, so the JSON a
snippet produces under the service matches what the CLI produces locally.</li>
<li><strong>Evaluation caps.</strong> <code>--evaluation-timeout</code> bounds wall-clock time per render,
<code>--max-stack</code> bounds call-stack depth, and <code>--max-concurrent-evals</code> bounds how
many evaluations run at once — so one expensive snippet cannot exhaust a
shared server. The CLI imposes none of these on its own.</li>
<li><strong>Read-scope sandboxing.</strong> Snippet-name resolution goes through Go&rsquo;s
<code>os.Root</code>, which rejects <code>..</code> traversal and symlinks that escape the
configured snippet directory, so a crafted request cannot read arbitrary
files. The <a href="/usage/evaluation-and-security/">evaluation and security</a>
 page
details the caps and the boundaries.</li>
</ul>
<h2 id="when-the-plain-cli-is-the-right-tool">When the plain CLI is the right tool</h2>
<p>The CLI is the better choice for work that is local and one-off:</p>
<ul>
<li><strong>One-off local renders</strong> — inspecting what a snippet produces, debugging a
library, iterating on a dashboard before committing it.</li>
<li><strong>CI scripts</strong> — a build step that renders Jsonnet to JSON and hands it to
another tool, where standing up a service would add a moving part for no gain.</li>
<li><strong>Anywhere a service is unwanted</strong> — no HTTP endpoint to call, no cluster, no
artifact to consume.</li>
</ul>
<p>Because JaaS runs the same go-jsonnet core, these are not mutually exclusive:
you can keep <code>jsonnet</code> and <code>jb</code> on your workstation and in CI, and run JaaS
in-cluster for the server-side and GitOps paths, with both producing the same
JSON for the same input. The <a href="/tutorials/local-rendering/">local rendering</a>

tutorial shows JaaS used purely as a renderer, which keeps the local and
in-cluster output aligned.</p>
]]></content><category scheme="https://jaas.projects.metio.wtf/tags/comparison" term="comparison" label="comparison"/><category scheme="https://jaas.projects.metio.wtf/tags/jsonnet-cli" term="jsonnet-cli" label="jsonnet-cli"/></entry><entry><title type="html">JOI images</title><link href="https://jaas.projects.metio.wtf/usage/joi-images/?utm_source=atom_feed" rel="alternate" type="text/html"/><link href="https://jaas.projects.metio.wtf/installation/configuration/?utm_source=atom_feed" rel="related" type="text/html" title="Configuration reference"/><link href="https://jaas.projects.metio.wtf/usage/creating-sources/?utm_source=atom_feed" rel="related" type="text/html" title="Creating source artifacts"/><link href="https://jaas.projects.metio.wtf/installation/helm-values/?utm_source=atom_feed" rel="related" type="text/html" title="Helm chart values"/><link href="https://jaas.projects.metio.wtf/usage/jsonnet-libraries/?utm_source=atom_feed" rel="related" type="text/html" title="Jsonnet libraries"/><link href="https://jaas.projects.metio.wtf/api/jsonnetlibrary/?utm_source=atom_feed" rel="related" type="text/html" title="JsonnetLibrary"/><id>https://jaas.projects.metio.wtf/usage/joi-images/</id><published>0001-01-01T00:00:00+00:00</published><updated>2026-06-16T20:48:39+02:00</updated><content type="html"><![CDATA[<blockquote>The catalog of prebuilt Jsonnet OCI Images (JOI) — every published library, its image reference, upstream source, and description — ready to import into snippets.</blockquote><p><a href="https://github.com/metio/jsonnet-oci-images">Jsonnet OCI Images</a>
 (JOI) package
popular Jsonnet libraries as single-layer OCI images, one per upstream library,
published at <code>ghcr.io/metio/joi-&lt;org&gt;-&lt;repo&gt;</code>. Because each image is a single
layer, the same artifact serves two roles: a container <strong>image volume</strong> mounted
into jaas, and a Flux <strong><code>OCIRepository</code></strong> source the operator fetches — so a
snippet imports a vendored library without bundling it.</p>
<p>Deploy them with the <a href="/installation/helm-values/#joi-library-chart">joi Helm chart</a>
,
which renders a <code>JsonnetLibrary</code> + <code>OCIRepository</code> pair for each enabled library.
A snippet then imports a library by its alias, choosing the version in the import
path:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-jsonnet" data-lang="jsonnet"><span class="line"><span class="cl"><span class="k">import</span><span class="w"> </span><span class="s">&#39;github.com/jsonnet-libs/k8s-libsonnet/1.34/main.libsonnet&#39;</span><span class="w">
</span></span></span></code></pre></div><p>The catalog below is generated from the
<a href="https://github.com/metio/jsonnet-oci-images/blob/main/libraries.json">jsonnet-oci-images manifest</a>
,
so it always reflects the currently published set. Pin an image with the moving
<code>:latest</code> tag or an immutable dated <code>:&lt;YYYY.M.D&gt;</code> snapshot.</p>
<table>
  <thead>
    <tr><th>Library</th><th>Image</th><th>Upstream</th><th>Description</th></tr>
  </thead>
  <tbody>
    <tr>
      <td><code>actions-runner-controller-libsonnet</code></td>
      <td><code>ghcr.io/metio/joi-jsonnet-libs-actions-runner-controller-libsonnet</code></td>
      <td><a href="https://github.com/jsonnet-libs/actions-runner-controller-libsonnet" rel="noopener noreferrer">jsonnet-libs/actions-runner-controller-libsonnet</a></td>
      <td>actions-runner-controller jsonnet library</td>
    </tr>
    <tr>
      <td><code>aiven-libsonnet</code></td>
      <td><code>ghcr.io/metio/joi-jsonnet-libs-aiven-libsonnet</code></td>
      <td><a href="https://github.com/jsonnet-libs/aiven-libsonnet" rel="noopener noreferrer">jsonnet-libs/aiven-libsonnet</a></td>
      <td>aiven jsonnet library</td>
    </tr>
    <tr>
      <td><code>amazon-vpc-resource-controller-libsonnet</code></td>
      <td><code>ghcr.io/metio/joi-jsonnet-libs-amazon-vpc-resource-controller-libsonnet</code></td>
      <td><a href="https://github.com/jsonnet-libs/amazon-vpc-resource-controller-libsonnet" rel="noopener noreferrer">jsonnet-libs/amazon-vpc-resource-controller-libsonnet</a></td>
      <td>amazon-vpc-resource-controller jsonnet library</td>
    </tr>
    <tr>
      <td><code>argo-cd-libsonnet</code></td>
      <td><code>ghcr.io/metio/joi-jsonnet-libs-argo-cd-libsonnet</code></td>
      <td><a href="https://github.com/jsonnet-libs/argo-cd-libsonnet" rel="noopener noreferrer">jsonnet-libs/argo-cd-libsonnet</a></td>
      <td>argo-cd jsonnet library</td>
    </tr>
    <tr>
      <td><code>argo-rollouts-libsonnet</code></td>
      <td><code>ghcr.io/metio/joi-jsonnet-libs-argo-rollouts-libsonnet</code></td>
      <td><a href="https://github.com/jsonnet-libs/argo-rollouts-libsonnet" rel="noopener noreferrer">jsonnet-libs/argo-rollouts-libsonnet</a></td>
      <td>argo-rollouts jsonnet library</td>
    </tr>
    <tr>
      <td><code>argo-workflows-libsonnet</code></td>
      <td><code>ghcr.io/metio/joi-jsonnet-libs-argo-workflows-libsonnet</code></td>
      <td><a href="https://github.com/jsonnet-libs/argo-workflows-libsonnet" rel="noopener noreferrer">jsonnet-libs/argo-workflows-libsonnet</a></td>
      <td>argo-workflows jsonnet library</td>
    </tr>
    <tr>
      <td><code>argocd-operator-libsonnet</code></td>
      <td><code>ghcr.io/metio/joi-jsonnet-libs-argocd-operator-libsonnet</code></td>
      <td><a href="https://github.com/jsonnet-libs/argocd-operator-libsonnet" rel="noopener noreferrer">jsonnet-libs/argocd-operator-libsonnet</a></td>
      <td>argocd-operator jsonnet library</td>
    </tr>
    <tr>
      <td><code>aws-load-balancer-controller-libsonnet</code></td>
      <td><code>ghcr.io/metio/joi-jsonnet-libs-aws-load-balancer-controller-libsonnet</code></td>
      <td><a href="https://github.com/jsonnet-libs/aws-load-balancer-controller-libsonnet" rel="noopener noreferrer">jsonnet-libs/aws-load-balancer-controller-libsonnet</a></td>
      <td>aws-load-balancer-controller jsonnet library</td>
    </tr>
    <tr>
      <td><code>aws-rds-controller-libsonnet</code></td>
      <td><code>ghcr.io/metio/joi-jsonnet-libs-aws-rds-controller-libsonnet</code></td>
      <td><a href="https://github.com/jsonnet-libs/aws-rds-controller-libsonnet" rel="noopener noreferrer">jsonnet-libs/aws-rds-controller-libsonnet</a></td>
      <td>aws-rds-controller jsonnet library</td>
    </tr>
    <tr>
      <td><code>azure-load-balancer-controller-libsonnet</code></td>
      <td><code>ghcr.io/metio/joi-jsonnet-libs-azure-load-balancer-controller-libsonnet</code></td>
      <td><a href="https://github.com/jsonnet-libs/azure-load-balancer-controller-libsonnet" rel="noopener noreferrer">jsonnet-libs/azure-load-balancer-controller-libsonnet</a></td>
      <td>azure-load-balancer-controller jsonnet library</td>
    </tr>
    <tr>
      <td><code>banzai-logging-libsonnet</code></td>
      <td><code>ghcr.io/metio/joi-jsonnet-libs-banzai-logging-libsonnet</code></td>
      <td><a href="https://github.com/jsonnet-libs/banzai-logging-libsonnet" rel="noopener noreferrer">jsonnet-libs/banzai-logging-libsonnet</a></td>
      <td>banzai-logging jsonnet library</td>
    </tr>
    <tr>
      <td><code>banzaicloud-bank-vaults-libsonnet</code></td>
      <td><code>ghcr.io/metio/joi-jsonnet-libs-banzaicloud-bank-vaults-libsonnet</code></td>
      <td><a href="https://github.com/jsonnet-libs/banzaicloud-bank-vaults-libsonnet" rel="noopener noreferrer">jsonnet-libs/banzaicloud-bank-vaults-libsonnet</a></td>
      <td>banzaicloud-bank-vaults jsonnet library</td>
    </tr>
    <tr>
      <td><code>calico-libsonnet</code></td>
      <td><code>ghcr.io/metio/joi-jsonnet-libs-calico-libsonnet</code></td>
      <td><a href="https://github.com/jsonnet-libs/calico-libsonnet" rel="noopener noreferrer">jsonnet-libs/calico-libsonnet</a></td>
      <td>calico jsonnet library</td>
    </tr>
    <tr>
      <td><code>cert-manager-libsonnet</code></td>
      <td><code>ghcr.io/metio/joi-jsonnet-libs-cert-manager-libsonnet</code></td>
      <td><a href="https://github.com/jsonnet-libs/cert-manager-libsonnet" rel="noopener noreferrer">jsonnet-libs/cert-manager-libsonnet</a></td>
      <td>cert-manager jsonnet library</td>
    </tr>
    <tr>
      <td><code>cilium-libsonnet</code></td>
      <td><code>ghcr.io/metio/joi-jsonnet-libs-cilium-libsonnet</code></td>
      <td><a href="https://github.com/jsonnet-libs/cilium-libsonnet" rel="noopener noreferrer">jsonnet-libs/cilium-libsonnet</a></td>
      <td>cilium jsonnet library</td>
    </tr>
    <tr>
      <td><code>clickhouse-operator-libsonnet</code></td>
      <td><code>ghcr.io/metio/joi-jsonnet-libs-clickhouse-operator-libsonnet</code></td>
      <td><a href="https://github.com/jsonnet-libs/clickhouse-operator-libsonnet" rel="noopener noreferrer">jsonnet-libs/clickhouse-operator-libsonnet</a></td>
      <td>clickhouse-operator jsonnet library</td>
    </tr>
    <tr>
      <td><code>cloudnative-pg-barman-cloud-plugin-libsonnet</code></td>
      <td><code>ghcr.io/metio/joi-jsonnet-libs-cloudnative-pg-barman-cloud-plugin-libsonnet</code></td>
      <td><a href="https://github.com/jsonnet-libs/cloudnative-pg-barman-cloud-plugin-libsonnet" rel="noopener noreferrer">jsonnet-libs/cloudnative-pg-barman-cloud-plugin-libsonnet</a></td>
      <td>cloudnative-pg-barman-cloud-plugin jsonnet library</td>
    </tr>
    <tr>
      <td><code>cloudnative-pg-libsonnet</code></td>
      <td><code>ghcr.io/metio/joi-jsonnet-libs-cloudnative-pg-libsonnet</code></td>
      <td><a href="https://github.com/jsonnet-libs/cloudnative-pg-libsonnet" rel="noopener noreferrer">jsonnet-libs/cloudnative-pg-libsonnet</a></td>
      <td>cloudnative-pg jsonnet library</td>
    </tr>
    <tr>
      <td><code>cluster-api-libsonnet</code></td>
      <td><code>ghcr.io/metio/joi-jsonnet-libs-cluster-api-libsonnet</code></td>
      <td><a href="https://github.com/jsonnet-libs/cluster-api-libsonnet" rel="noopener noreferrer">jsonnet-libs/cluster-api-libsonnet</a></td>
      <td>cluster-api jsonnet library</td>
    </tr>
    <tr>
      <td><code>cluster-api-operator-libsonnet</code></td>
      <td><code>ghcr.io/metio/joi-jsonnet-libs-cluster-api-operator-libsonnet</code></td>
      <td><a href="https://github.com/jsonnet-libs/cluster-api-operator-libsonnet" rel="noopener noreferrer">jsonnet-libs/cluster-api-operator-libsonnet</a></td>
      <td>cluster-api-operator jsonnet library</td>
    </tr>
    <tr>
      <td><code>cluster-api-provider-aws-libsonnet</code></td>
      <td><code>ghcr.io/metio/joi-jsonnet-libs-cluster-api-provider-aws-libsonnet</code></td>
      <td><a href="https://github.com/jsonnet-libs/cluster-api-provider-aws-libsonnet" rel="noopener noreferrer">jsonnet-libs/cluster-api-provider-aws-libsonnet</a></td>
      <td>cluster-api-provider-aws jsonnet library</td>
    </tr>
    <tr>
      <td><code>cluster-api-provider-tinkerbell-libsonnet</code></td>
      <td><code>ghcr.io/metio/joi-jsonnet-libs-cluster-api-provider-tinkerbell-libsonnet</code></td>
      <td><a href="https://github.com/jsonnet-libs/cluster-api-provider-tinkerbell-libsonnet" rel="noopener noreferrer">jsonnet-libs/cluster-api-provider-tinkerbell-libsonnet</a></td>
      <td>cluster-api-provider-tinkerbell jsonnet library</td>
    </tr>
    <tr>
      <td><code>cnrm-libsonnet</code></td>
      <td><code>ghcr.io/metio/joi-jsonnet-libs-cnrm-libsonnet</code></td>
      <td><a href="https://github.com/jsonnet-libs/cnrm-libsonnet" rel="noopener noreferrer">jsonnet-libs/cnrm-libsonnet</a></td>
      <td>cnrm jsonnet library</td>
    </tr>
    <tr>
      <td><code>composable-libsonnet</code></td>
      <td><code>ghcr.io/metio/joi-jsonnet-libs-composable-libsonnet</code></td>
      <td><a href="https://github.com/jsonnet-libs/composable-libsonnet" rel="noopener noreferrer">jsonnet-libs/composable-libsonnet</a></td>
      <td>composable jsonnet library</td>
    </tr>
    <tr>
      <td><code>consul-libsonnet</code></td>
      <td><code>ghcr.io/metio/joi-jsonnet-libs-consul-libsonnet</code></td>
      <td><a href="https://github.com/jsonnet-libs/consul-libsonnet" rel="noopener noreferrer">jsonnet-libs/consul-libsonnet</a></td>
      <td>consul jsonnet library</td>
    </tr>
    <tr>
      <td><code>contour-libsonnet</code></td>
      <td><code>ghcr.io/metio/joi-jsonnet-libs-contour-libsonnet</code></td>
      <td><a href="https://github.com/jsonnet-libs/contour-libsonnet" rel="noopener noreferrer">jsonnet-libs/contour-libsonnet</a></td>
      <td>contour jsonnet library</td>
    </tr>
    <tr>
      <td><code>crossplane-core-libsonnet</code></td>
      <td><code>ghcr.io/metio/joi-jsonnet-libs-crossplane-core-libsonnet</code></td>
      <td><a href="https://github.com/jsonnet-libs/crossplane-core-libsonnet" rel="noopener noreferrer">jsonnet-libs/crossplane-core-libsonnet</a></td>
      <td>crossplane-core jsonnet library</td>
    </tr>
    <tr>
      <td><code>crossplane-libsonnet</code></td>
      <td><code>ghcr.io/metio/joi-jsonnet-libs-crossplane-libsonnet</code></td>
      <td><a href="https://github.com/jsonnet-libs/crossplane-libsonnet" rel="noopener noreferrer">jsonnet-libs/crossplane-libsonnet</a></td>
      <td>crossplane jsonnet library</td>
    </tr>
    <tr>
      <td><code>crossplane-provider-grafana-libsonnet</code></td>
      <td><code>ghcr.io/metio/joi-jsonnet-libs-crossplane-provider-grafana-libsonnet</code></td>
      <td><a href="https://github.com/jsonnet-libs/crossplane-provider-grafana-libsonnet" rel="noopener noreferrer">jsonnet-libs/crossplane-provider-grafana-libsonnet</a></td>
      <td>crossplane-provider-grafana jsonnet library</td>
    </tr>
    <tr>
      <td><code>crossplane-provider-upjet-aws-libsonnet</code></td>
      <td><code>ghcr.io/metio/joi-jsonnet-libs-crossplane-provider-upjet-aws-libsonnet</code></td>
      <td><a href="https://github.com/jsonnet-libs/crossplane-provider-upjet-aws-libsonnet" rel="noopener noreferrer">jsonnet-libs/crossplane-provider-upjet-aws-libsonnet</a></td>
      <td>crossplane-provider-upjet-aws jsonnet library</td>
    </tr>
    <tr>
      <td><code>crossplane-provider-upjet-azure-libsonnet</code></td>
      <td><code>ghcr.io/metio/joi-jsonnet-libs-crossplane-provider-upjet-azure-libsonnet</code></td>
      <td><a href="https://github.com/jsonnet-libs/crossplane-provider-upjet-azure-libsonnet" rel="noopener noreferrer">jsonnet-libs/crossplane-provider-upjet-azure-libsonnet</a></td>
      <td>crossplane-provider-upjet-azure jsonnet library</td>
    </tr>
    <tr>
      <td><code>crossplane-provider-upjet-azuread-libsonnet</code></td>
      <td><code>ghcr.io/metio/joi-jsonnet-libs-crossplane-provider-upjet-azuread-libsonnet</code></td>
      <td><a href="https://github.com/jsonnet-libs/crossplane-provider-upjet-azuread-libsonnet" rel="noopener noreferrer">jsonnet-libs/crossplane-provider-upjet-azuread-libsonnet</a></td>
      <td>crossplane-provider-upjet-azuread jsonnet library</td>
    </tr>
    <tr>
      <td><code>crossplane-provider-upjet-gcp-libsonnet</code></td>
      <td><code>ghcr.io/metio/joi-jsonnet-libs-crossplane-provider-upjet-gcp-libsonnet</code></td>
      <td><a href="https://github.com/jsonnet-libs/crossplane-provider-upjet-gcp-libsonnet" rel="noopener noreferrer">jsonnet-libs/crossplane-provider-upjet-gcp-libsonnet</a></td>
      <td>crossplane-provider-upjet-gcp jsonnet library</td>
    </tr>
    <tr>
      <td><code>crossplane-provider-upjet-github-libsonnet</code></td>
      <td><code>ghcr.io/metio/joi-jsonnet-libs-crossplane-provider-upjet-github-libsonnet</code></td>
      <td><a href="https://github.com/jsonnet-libs/crossplane-provider-upjet-github-libsonnet" rel="noopener noreferrer">jsonnet-libs/crossplane-provider-upjet-github-libsonnet</a></td>
      <td>crossplane-provider-upjet-github jsonnet library</td>
    </tr>
    <tr>
      <td><code>dapr-libsonnet</code></td>
      <td><code>ghcr.io/metio/joi-jsonnet-libs-dapr-libsonnet</code></td>
      <td><a href="https://github.com/jsonnet-libs/dapr-libsonnet" rel="noopener noreferrer">jsonnet-libs/dapr-libsonnet</a></td>
      <td>dapr jsonnet library</td>
    </tr>
    <tr>
      <td><code>datadog-operator-libsonnet</code></td>
      <td><code>ghcr.io/metio/joi-jsonnet-libs-datadog-operator-libsonnet</code></td>
      <td><a href="https://github.com/jsonnet-libs/datadog-operator-libsonnet" rel="noopener noreferrer">jsonnet-libs/datadog-operator-libsonnet</a></td>
      <td>datadog-operator jsonnet library</td>
    </tr>
    <tr>
      <td><code>docsonnet</code></td>
      <td><code>ghcr.io/metio/joi-jsonnet-libs-docsonnet</code></td>
      <td><a href="https://github.com/jsonnet-libs/docsonnet" rel="noopener noreferrer">jsonnet-libs/docsonnet</a></td>
      <td>Experimental Jsonnet docs generator</td>
    </tr>
    <tr>
      <td><code>eck-operator-libsonnet</code></td>
      <td><code>ghcr.io/metio/joi-jsonnet-libs-eck-operator-libsonnet</code></td>
      <td><a href="https://github.com/jsonnet-libs/eck-operator-libsonnet" rel="noopener noreferrer">jsonnet-libs/eck-operator-libsonnet</a></td>
      <td>eck-operator jsonnet library</td>
    </tr>
    <tr>
      <td><code>edp-keycloak-operator-libsonnet</code></td>
      <td><code>ghcr.io/metio/joi-jsonnet-libs-edp-keycloak-operator-libsonnet</code></td>
      <td><a href="https://github.com/jsonnet-libs/edp-keycloak-operator-libsonnet" rel="noopener noreferrer">jsonnet-libs/edp-keycloak-operator-libsonnet</a></td>
      <td>edp-keycloak-operator jsonnet library</td>
    </tr>
    <tr>
      <td><code>emissary-libsonnet</code></td>
      <td><code>ghcr.io/metio/joi-jsonnet-libs-emissary-libsonnet</code></td>
      <td><a href="https://github.com/jsonnet-libs/emissary-libsonnet" rel="noopener noreferrer">jsonnet-libs/emissary-libsonnet</a></td>
      <td>emissary jsonnet library</td>
    </tr>
    <tr>
      <td><code>envoy-gateway-libsonnet</code></td>
      <td><code>ghcr.io/metio/joi-jsonnet-libs-envoy-gateway-libsonnet</code></td>
      <td><a href="https://github.com/jsonnet-libs/envoy-gateway-libsonnet" rel="noopener noreferrer">jsonnet-libs/envoy-gateway-libsonnet</a></td>
      <td>envoy-gateway jsonnet library</td>
    </tr>
    <tr>
      <td><code>etcd-operator-libsonnet</code></td>
      <td><code>ghcr.io/metio/joi-jsonnet-libs-etcd-operator-libsonnet</code></td>
      <td><a href="https://github.com/jsonnet-libs/etcd-operator-libsonnet" rel="noopener noreferrer">jsonnet-libs/etcd-operator-libsonnet</a></td>
      <td>etcd-operator jsonnet library</td>
    </tr>
    <tr>
      <td><code>external-dns-libsonnet</code></td>
      <td><code>ghcr.io/metio/joi-jsonnet-libs-external-dns-libsonnet</code></td>
      <td><a href="https://github.com/jsonnet-libs/external-dns-libsonnet" rel="noopener noreferrer">jsonnet-libs/external-dns-libsonnet</a></td>
      <td>external-dns jsonnet library</td>
    </tr>
    <tr>
      <td><code>external-secrets-libsonnet</code></td>
      <td><code>ghcr.io/metio/joi-jsonnet-libs-external-secrets-libsonnet</code></td>
      <td><a href="https://github.com/jsonnet-libs/external-secrets-libsonnet" rel="noopener noreferrer">jsonnet-libs/external-secrets-libsonnet</a></td>
      <td>external-secrets jsonnet library</td>
    </tr>
    <tr>
      <td><code>flagger-libsonnet</code></td>
      <td><code>ghcr.io/metio/joi-jsonnet-libs-flagger-libsonnet</code></td>
      <td><a href="https://github.com/jsonnet-libs/flagger-libsonnet" rel="noopener noreferrer">jsonnet-libs/flagger-libsonnet</a></td>
      <td>flagger jsonnet library</td>
    </tr>
    <tr>
      <td><code>fluxcd-libsonnet</code></td>
      <td><code>ghcr.io/metio/joi-jsonnet-libs-fluxcd-libsonnet</code></td>
      <td><a href="https://github.com/jsonnet-libs/fluxcd-libsonnet" rel="noopener noreferrer">jsonnet-libs/fluxcd-libsonnet</a></td>
      <td>fluxcd jsonnet library</td>
    </tr>
    <tr>
      <td><code>gatekeeper-libsonnet</code></td>
      <td><code>ghcr.io/metio/joi-jsonnet-libs-gatekeeper-libsonnet</code></td>
      <td><a href="https://github.com/jsonnet-libs/gatekeeper-libsonnet" rel="noopener noreferrer">jsonnet-libs/gatekeeper-libsonnet</a></td>
      <td>gatekeeper jsonnet library</td>
    </tr>
    <tr>
      <td><code>gateway-api-libsonnet</code></td>
      <td><code>ghcr.io/metio/joi-jsonnet-libs-gateway-api-libsonnet</code></td>
      <td><a href="https://github.com/jsonnet-libs/gateway-api-libsonnet" rel="noopener noreferrer">jsonnet-libs/gateway-api-libsonnet</a></td>
      <td>gateway-api jsonnet library</td>
    </tr>
    <tr>
      <td><code>google-cloud-sql-proxy-operator-libsonnet</code></td>
      <td><code>ghcr.io/metio/joi-jsonnet-libs-google-cloud-sql-proxy-operator-libsonnet</code></td>
      <td><a href="https://github.com/jsonnet-libs/google-cloud-sql-proxy-operator-libsonnet" rel="noopener noreferrer">jsonnet-libs/google-cloud-sql-proxy-operator-libsonnet</a></td>
      <td>google-cloud-sql-proxy-operator jsonnet library</td>
    </tr>
    <tr>
      <td><code>grafana-agent-libsonnet</code></td>
      <td><code>ghcr.io/metio/joi-jsonnet-libs-grafana-agent-libsonnet</code></td>
      <td><a href="https://github.com/jsonnet-libs/grafana-agent-libsonnet" rel="noopener noreferrer">jsonnet-libs/grafana-agent-libsonnet</a></td>
      <td>grafana-agent jsonnet library</td>
    </tr>
    <tr>
      <td><code>grafana-alloy-libsonnet</code></td>
      <td><code>ghcr.io/metio/joi-jsonnet-libs-grafana-alloy-libsonnet</code></td>
      <td><a href="https://github.com/jsonnet-libs/grafana-alloy-libsonnet" rel="noopener noreferrer">jsonnet-libs/grafana-alloy-libsonnet</a></td>
      <td>grafana-alloy jsonnet library</td>
    </tr>
    <tr>
      <td><code>grafana-operator-libsonnet</code></td>
      <td><code>ghcr.io/metio/joi-jsonnet-libs-grafana-operator-libsonnet</code></td>
      <td><a href="https://github.com/jsonnet-libs/grafana-operator-libsonnet" rel="noopener noreferrer">jsonnet-libs/grafana-operator-libsonnet</a></td>
      <td>grafana-operator jsonnet library</td>
    </tr>
    <tr>
      <td><code>grafonnet</code></td>
      <td><code>ghcr.io/metio/joi-grafana-grafonnet</code></td>
      <td><a href="https://github.com/grafana/grafonnet" rel="noopener noreferrer">grafana/grafonnet</a></td>
      <td>Jsonnet library for generating Grafana dashboards.</td>
    </tr>
    <tr>
      <td><code>harbor-operator-libsonnet</code></td>
      <td><code>ghcr.io/metio/joi-jsonnet-libs-harbor-operator-libsonnet</code></td>
      <td><a href="https://github.com/jsonnet-libs/harbor-operator-libsonnet" rel="noopener noreferrer">jsonnet-libs/harbor-operator-libsonnet</a></td>
      <td>harbor-operator jsonnet library</td>
    </tr>
    <tr>
      <td><code>hcp-terraform-operator-libsonnet</code></td>
      <td><code>ghcr.io/metio/joi-jsonnet-libs-hcp-terraform-operator-libsonnet</code></td>
      <td><a href="https://github.com/jsonnet-libs/hcp-terraform-operator-libsonnet" rel="noopener noreferrer">jsonnet-libs/hcp-terraform-operator-libsonnet</a></td>
      <td>hcp-terraform-operator jsonnet library</td>
    </tr>
    <tr>
      <td><code>istio-libsonnet</code></td>
      <td><code>ghcr.io/metio/joi-jsonnet-libs-istio-libsonnet</code></td>
      <td><a href="https://github.com/jsonnet-libs/istio-libsonnet" rel="noopener noreferrer">jsonnet-libs/istio-libsonnet</a></td>
      <td>istio jsonnet library</td>
    </tr>
    <tr>
      <td><code>k8s-libsonnet</code></td>
      <td><code>ghcr.io/metio/joi-jsonnet-libs-k8s-libsonnet</code></td>
      <td><a href="https://github.com/jsonnet-libs/k8s-libsonnet" rel="noopener noreferrer">jsonnet-libs/k8s-libsonnet</a></td>
      <td>k8s jsonnet library</td>
    </tr>
    <tr>
      <td><code>kargo-libsonnet</code></td>
      <td><code>ghcr.io/metio/joi-jsonnet-libs-kargo-libsonnet</code></td>
      <td><a href="https://github.com/jsonnet-libs/kargo-libsonnet" rel="noopener noreferrer">jsonnet-libs/kargo-libsonnet</a></td>
      <td>kargo jsonnet library</td>
    </tr>
    <tr>
      <td><code>karpenter-libsonnet</code></td>
      <td><code>ghcr.io/metio/joi-jsonnet-libs-karpenter-libsonnet</code></td>
      <td><a href="https://github.com/jsonnet-libs/karpenter-libsonnet" rel="noopener noreferrer">jsonnet-libs/karpenter-libsonnet</a></td>
      <td>karpenter jsonnet library</td>
    </tr>
    <tr>
      <td><code>keda-libsonnet</code></td>
      <td><code>ghcr.io/metio/joi-jsonnet-libs-keda-libsonnet</code></td>
      <td><a href="https://github.com/jsonnet-libs/keda-libsonnet" rel="noopener noreferrer">jsonnet-libs/keda-libsonnet</a></td>
      <td>keda jsonnet library</td>
    </tr>
    <tr>
      <td><code>knative-eventing-libsonnet</code></td>
      <td><code>ghcr.io/metio/joi-jsonnet-libs-knative-eventing-libsonnet</code></td>
      <td><a href="https://github.com/jsonnet-libs/knative-eventing-libsonnet" rel="noopener noreferrer">jsonnet-libs/knative-eventing-libsonnet</a></td>
      <td>knative-eventing jsonnet library</td>
    </tr>
    <tr>
      <td><code>knative-serving-libsonnet</code></td>
      <td><code>ghcr.io/metio/joi-jsonnet-libs-knative-serving-libsonnet</code></td>
      <td><a href="https://github.com/jsonnet-libs/knative-serving-libsonnet" rel="noopener noreferrer">jsonnet-libs/knative-serving-libsonnet</a></td>
      <td>knative-serving jsonnet library</td>
    </tr>
    <tr>
      <td><code>kro-libsonnet</code></td>
      <td><code>ghcr.io/metio/joi-jsonnet-libs-kro-libsonnet</code></td>
      <td><a href="https://github.com/jsonnet-libs/kro-libsonnet" rel="noopener noreferrer">jsonnet-libs/kro-libsonnet</a></td>
      <td>kro jsonnet library</td>
    </tr>
    <tr>
      <td><code>kube-prometheus-libsonnet</code></td>
      <td><code>ghcr.io/metio/joi-jsonnet-libs-kube-prometheus-libsonnet</code></td>
      <td><a href="https://github.com/jsonnet-libs/kube-prometheus-libsonnet" rel="noopener noreferrer">jsonnet-libs/kube-prometheus-libsonnet</a></td>
      <td>kube-prometheus jsonnet library</td>
    </tr>
    <tr>
      <td><code>kubernetes-nmstate-libsonnet</code></td>
      <td><code>ghcr.io/metio/joi-jsonnet-libs-kubernetes-nmstate-libsonnet</code></td>
      <td><a href="https://github.com/jsonnet-libs/kubernetes-nmstate-libsonnet" rel="noopener noreferrer">jsonnet-libs/kubernetes-nmstate-libsonnet</a></td>
      <td>kubernetes-nmstate jsonnet library</td>
    </tr>
    <tr>
      <td><code>kubernetes-secret-generator-libsonnet</code></td>
      <td><code>ghcr.io/metio/joi-jsonnet-libs-kubernetes-secret-generator-libsonnet</code></td>
      <td><a href="https://github.com/jsonnet-libs/kubernetes-secret-generator-libsonnet" rel="noopener noreferrer">jsonnet-libs/kubernetes-secret-generator-libsonnet</a></td>
      <td>kubernetes-secret-generator jsonnet library</td>
    </tr>
    <tr>
      <td><code>kubevela-libsonnet</code></td>
      <td><code>ghcr.io/metio/joi-jsonnet-libs-kubevela-libsonnet</code></td>
      <td><a href="https://github.com/jsonnet-libs/kubevela-libsonnet" rel="noopener noreferrer">jsonnet-libs/kubevela-libsonnet</a></td>
      <td>kubevela jsonnet library</td>
    </tr>
    <tr>
      <td><code>kueue-libsonnet</code></td>
      <td><code>ghcr.io/metio/joi-jsonnet-libs-kueue-libsonnet</code></td>
      <td><a href="https://github.com/jsonnet-libs/kueue-libsonnet" rel="noopener noreferrer">jsonnet-libs/kueue-libsonnet</a></td>
      <td>kueue jsonnet library</td>
    </tr>
    <tr>
      <td><code>kyverno-libsonnet</code></td>
      <td><code>ghcr.io/metio/joi-jsonnet-libs-kyverno-libsonnet</code></td>
      <td><a href="https://github.com/jsonnet-libs/kyverno-libsonnet" rel="noopener noreferrer">jsonnet-libs/kyverno-libsonnet</a></td>
      <td>kyverno jsonnet library</td>
    </tr>
    <tr>
      <td><code>litmus-chaos-libsonnet</code></td>
      <td><code>ghcr.io/metio/joi-jsonnet-libs-litmus-chaos-libsonnet</code></td>
      <td><a href="https://github.com/jsonnet-libs/litmus-chaos-libsonnet" rel="noopener noreferrer">jsonnet-libs/litmus-chaos-libsonnet</a></td>
      <td>litmus-chaos jsonnet library</td>
    </tr>
    <tr>
      <td><code>mariadb-operator-libsonnet</code></td>
      <td><code>ghcr.io/metio/joi-jsonnet-libs-mariadb-operator-libsonnet</code></td>
      <td><a href="https://github.com/jsonnet-libs/mariadb-operator-libsonnet" rel="noopener noreferrer">jsonnet-libs/mariadb-operator-libsonnet</a></td>
      <td>mariadb-operator jsonnet library</td>
    </tr>
    <tr>
      <td><code>metacontroller-libsonnet</code></td>
      <td><code>ghcr.io/metio/joi-jsonnet-libs-metacontroller-libsonnet</code></td>
      <td><a href="https://github.com/jsonnet-libs/metacontroller-libsonnet" rel="noopener noreferrer">jsonnet-libs/metacontroller-libsonnet</a></td>
      <td>metacontroller jsonnet library</td>
    </tr>
    <tr>
      <td><code>metallb-libsonnet</code></td>
      <td><code>ghcr.io/metio/joi-jsonnet-libs-metallb-libsonnet</code></td>
      <td><a href="https://github.com/jsonnet-libs/metallb-libsonnet" rel="noopener noreferrer">jsonnet-libs/metallb-libsonnet</a></td>
      <td>metallb jsonnet library</td>
    </tr>
    <tr>
      <td><code>milvus-operator-libsonnet</code></td>
      <td><code>ghcr.io/metio/joi-jsonnet-libs-milvus-operator-libsonnet</code></td>
      <td><a href="https://github.com/jsonnet-libs/milvus-operator-libsonnet" rel="noopener noreferrer">jsonnet-libs/milvus-operator-libsonnet</a></td>
      <td>milvus-operator jsonnet library</td>
    </tr>
    <tr>
      <td><code>minio-operator-libsonnet</code></td>
      <td><code>ghcr.io/metio/joi-jsonnet-libs-minio-operator-libsonnet</code></td>
      <td><a href="https://github.com/jsonnet-libs/minio-operator-libsonnet" rel="noopener noreferrer">jsonnet-libs/minio-operator-libsonnet</a></td>
      <td>minio-operator jsonnet library</td>
    </tr>
    <tr>
      <td><code>mysql-operator-libsonnet</code></td>
      <td><code>ghcr.io/metio/joi-jsonnet-libs-mysql-operator-libsonnet</code></td>
      <td><a href="https://github.com/jsonnet-libs/mysql-operator-libsonnet" rel="noopener noreferrer">jsonnet-libs/mysql-operator-libsonnet</a></td>
      <td>mysql-operator jsonnet library</td>
    </tr>
    <tr>
      <td><code>nats-libsonnet</code></td>
      <td><code>ghcr.io/metio/joi-jsonnet-libs-nats-libsonnet</code></td>
      <td><a href="https://github.com/jsonnet-libs/nats-libsonnet" rel="noopener noreferrer">jsonnet-libs/nats-libsonnet</a></td>
      <td>nats jsonnet library</td>
    </tr>
    <tr>
      <td><code>node-feature-discovery-libsonnet</code></td>
      <td><code>ghcr.io/metio/joi-jsonnet-libs-node-feature-discovery-libsonnet</code></td>
      <td><a href="https://github.com/jsonnet-libs/node-feature-discovery-libsonnet" rel="noopener noreferrer">jsonnet-libs/node-feature-discovery-libsonnet</a></td>
      <td>node-feature-discovery jsonnet library</td>
    </tr>
    <tr>
      <td><code>openshift-libsonnet</code></td>
      <td><code>ghcr.io/metio/joi-jsonnet-libs-openshift-libsonnet</code></td>
      <td><a href="https://github.com/jsonnet-libs/openshift-libsonnet" rel="noopener noreferrer">jsonnet-libs/openshift-libsonnet</a></td>
      <td>openshift jsonnet library</td>
    </tr>
    <tr>
      <td><code>prometheus-operator-libsonnet</code></td>
      <td><code>ghcr.io/metio/joi-jsonnet-libs-prometheus-operator-libsonnet</code></td>
      <td><a href="https://github.com/jsonnet-libs/prometheus-operator-libsonnet" rel="noopener noreferrer">jsonnet-libs/prometheus-operator-libsonnet</a></td>
      <td>prometheus-operator jsonnet library</td>
    </tr>
    <tr>
      <td><code>pyrra-libsonnet</code></td>
      <td><code>ghcr.io/metio/joi-jsonnet-libs-pyrra-libsonnet</code></td>
      <td><a href="https://github.com/jsonnet-libs/pyrra-libsonnet" rel="noopener noreferrer">jsonnet-libs/pyrra-libsonnet</a></td>
      <td>pyrra jsonnet library</td>
    </tr>
    <tr>
      <td><code>rabbitmq-libsonnet</code></td>
      <td><code>ghcr.io/metio/joi-jsonnet-libs-rabbitmq-libsonnet</code></td>
      <td><a href="https://github.com/jsonnet-libs/rabbitmq-libsonnet" rel="noopener noreferrer">jsonnet-libs/rabbitmq-libsonnet</a></td>
      <td>rabbitmq jsonnet library</td>
    </tr>
    <tr>
      <td><code>rabbitmq-messaging-topology-operator-libsonnet</code></td>
      <td><code>ghcr.io/metio/joi-jsonnet-libs-rabbitmq-messaging-topology-operator-libsonnet</code></td>
      <td><a href="https://github.com/jsonnet-libs/rabbitmq-messaging-topology-operator-libsonnet" rel="noopener noreferrer">jsonnet-libs/rabbitmq-messaging-topology-operator-libsonnet</a></td>
      <td>rabbitmq-messaging-topology-operator jsonnet library</td>
    </tr>
    <tr>
      <td><code>secrets-store-csi-driver-libsonnet</code></td>
      <td><code>ghcr.io/metio/joi-jsonnet-libs-secrets-store-csi-driver-libsonnet</code></td>
      <td><a href="https://github.com/jsonnet-libs/secrets-store-csi-driver-libsonnet" rel="noopener noreferrer">jsonnet-libs/secrets-store-csi-driver-libsonnet</a></td>
      <td>secrets-store-csi-driver jsonnet library</td>
    </tr>
    <tr>
      <td><code>securecodebox-libsonnet</code></td>
      <td><code>ghcr.io/metio/joi-jsonnet-libs-securecodebox-libsonnet</code></td>
      <td><a href="https://github.com/jsonnet-libs/securecodebox-libsonnet" rel="noopener noreferrer">jsonnet-libs/securecodebox-libsonnet</a></td>
      <td>securecodebox jsonnet library</td>
    </tr>
    <tr>
      <td><code>spicedb-operator-libsonnet</code></td>
      <td><code>ghcr.io/metio/joi-jsonnet-libs-spicedb-operator-libsonnet</code></td>
      <td><a href="https://github.com/jsonnet-libs/spicedb-operator-libsonnet" rel="noopener noreferrer">jsonnet-libs/spicedb-operator-libsonnet</a></td>
      <td>spicedb-operator jsonnet library</td>
    </tr>
    <tr>
      <td><code>strimzi-libsonnet</code></td>
      <td><code>ghcr.io/metio/joi-jsonnet-libs-strimzi-libsonnet</code></td>
      <td><a href="https://github.com/jsonnet-libs/strimzi-libsonnet" rel="noopener noreferrer">jsonnet-libs/strimzi-libsonnet</a></td>
      <td>strimzi jsonnet library</td>
    </tr>
    <tr>
      <td><code>tailscale-operator-libsonnet</code></td>
      <td><code>ghcr.io/metio/joi-jsonnet-libs-tailscale-operator-libsonnet</code></td>
      <td><a href="https://github.com/jsonnet-libs/tailscale-operator-libsonnet" rel="noopener noreferrer">jsonnet-libs/tailscale-operator-libsonnet</a></td>
      <td>tailscale-operator jsonnet library</td>
    </tr>
    <tr>
      <td><code>tektoncd-libsonnet</code></td>
      <td><code>ghcr.io/metio/joi-jsonnet-libs-tektoncd-libsonnet</code></td>
      <td><a href="https://github.com/jsonnet-libs/tektoncd-libsonnet" rel="noopener noreferrer">jsonnet-libs/tektoncd-libsonnet</a></td>
      <td>tektoncd jsonnet library</td>
    </tr>
    <tr>
      <td><code>teleport-operator-libsonnet</code></td>
      <td><code>ghcr.io/metio/joi-jsonnet-libs-teleport-operator-libsonnet</code></td>
      <td><a href="https://github.com/jsonnet-libs/teleport-operator-libsonnet" rel="noopener noreferrer">jsonnet-libs/teleport-operator-libsonnet</a></td>
      <td>teleport-operator jsonnet library</td>
    </tr>
    <tr>
      <td><code>testonnet</code></td>
      <td><code>ghcr.io/metio/joi-jsonnet-libs-testonnet</code></td>
      <td><a href="https://github.com/jsonnet-libs/testonnet" rel="noopener noreferrer">jsonnet-libs/testonnet</a></td>
      <td>A unit testing framework for Jsonnet.</td>
    </tr>
    <tr>
      <td><code>tinkerbell-libsonnet</code></td>
      <td><code>ghcr.io/metio/joi-jsonnet-libs-tinkerbell-libsonnet</code></td>
      <td><a href="https://github.com/jsonnet-libs/tinkerbell-libsonnet" rel="noopener noreferrer">jsonnet-libs/tinkerbell-libsonnet</a></td>
      <td>tinkerbell jsonnet library</td>
    </tr>
    <tr>
      <td><code>traefik-libsonnet</code></td>
      <td><code>ghcr.io/metio/joi-jsonnet-libs-traefik-libsonnet</code></td>
      <td><a href="https://github.com/jsonnet-libs/traefik-libsonnet" rel="noopener noreferrer">jsonnet-libs/traefik-libsonnet</a></td>
      <td>traefik jsonnet library</td>
    </tr>
    <tr>
      <td><code>triggermesh-libsonnet</code></td>
      <td><code>ghcr.io/metio/joi-jsonnet-libs-triggermesh-libsonnet</code></td>
      <td><a href="https://github.com/jsonnet-libs/triggermesh-libsonnet" rel="noopener noreferrer">jsonnet-libs/triggermesh-libsonnet</a></td>
      <td>triggermesh jsonnet library</td>
    </tr>
    <tr>
      <td><code>trust-manager-libsonnet</code></td>
      <td><code>ghcr.io/metio/joi-jsonnet-libs-trust-manager-libsonnet</code></td>
      <td><a href="https://github.com/jsonnet-libs/trust-manager-libsonnet" rel="noopener noreferrer">jsonnet-libs/trust-manager-libsonnet</a></td>
      <td>trust-manager jsonnet library</td>
    </tr>
    <tr>
      <td><code>upbound-provider-opentofu-libsonnet</code></td>
      <td><code>ghcr.io/metio/joi-jsonnet-libs-upbound-provider-opentofu-libsonnet</code></td>
      <td><a href="https://github.com/jsonnet-libs/upbound-provider-opentofu-libsonnet" rel="noopener noreferrer">jsonnet-libs/upbound-provider-opentofu-libsonnet</a></td>
      <td>upbound-provider-opentofu jsonnet library</td>
    </tr>
    <tr>
      <td><code>vault-secrets-operator-libsonnet</code></td>
      <td><code>ghcr.io/metio/joi-jsonnet-libs-vault-secrets-operator-libsonnet</code></td>
      <td><a href="https://github.com/jsonnet-libs/vault-secrets-operator-libsonnet" rel="noopener noreferrer">jsonnet-libs/vault-secrets-operator-libsonnet</a></td>
      <td>vault-secrets-operator jsonnet library</td>
    </tr>
    <tr>
      <td><code>vertical-pod-autoscaler-libsonnet</code></td>
      <td><code>ghcr.io/metio/joi-jsonnet-libs-vertical-pod-autoscaler-libsonnet</code></td>
      <td><a href="https://github.com/jsonnet-libs/vertical-pod-autoscaler-libsonnet" rel="noopener noreferrer">jsonnet-libs/vertical-pod-autoscaler-libsonnet</a></td>
      <td>vertical-pod-autoscaler jsonnet library</td>
    </tr>
    <tr>
      <td><code>victoria-metrics-operator-libsonnet</code></td>
      <td><code>ghcr.io/metio/joi-jsonnet-libs-victoria-metrics-operator-libsonnet</code></td>
      <td><a href="https://github.com/jsonnet-libs/victoria-metrics-operator-libsonnet" rel="noopener noreferrer">jsonnet-libs/victoria-metrics-operator-libsonnet</a></td>
      <td>victoria-metrics-operator jsonnet library</td>
    </tr>
    <tr>
      <td><code>xtd</code></td>
      <td><code>ghcr.io/metio/joi-jsonnet-libs-xtd</code></td>
      <td><a href="https://github.com/jsonnet-libs/xtd" rel="noopener noreferrer">jsonnet-libs/xtd</a></td>
      <td>Extended Jsonnet standard library</td>
    </tr>
    <tr>
      <td><code>zalando-postgres-operator-libsonnet</code></td>
      <td><code>ghcr.io/metio/joi-jsonnet-libs-zalando-postgres-operator-libsonnet</code></td>
      <td><a href="https://github.com/jsonnet-libs/zalando-postgres-operator-libsonnet" rel="noopener noreferrer">jsonnet-libs/zalando-postgres-operator-libsonnet</a></td>
      <td>zalando-postgres-operator jsonnet library</td>
    </tr>
  </tbody>
</table>
]]></content><category scheme="https://jaas.projects.metio.wtf/tags/jsonnet" term="jsonnet" label="jsonnet"/><category scheme="https://jaas.projects.metio.wtf/tags/libraries" term="libraries" label="libraries"/><category scheme="https://jaas.projects.metio.wtf/tags/oci" term="oci" label="oci"/><category scheme="https://jaas.projects.metio.wtf/tags/joi" term="joi" label="joi"/><category scheme="https://jaas.projects.metio.wtf/tags/reference" term="reference" label="reference"/></entry><entry><title type="html">Jsonnet libraries</title><link href="https://jaas.projects.metio.wtf/usage/jsonnet-libraries/?utm_source=atom_feed" rel="alternate" type="text/html"/><link href="https://jaas.projects.metio.wtf/api/jsonnetlibrary/?utm_source=atom_feed" rel="related" type="text/html" title="JsonnetLibrary"/><link href="https://jaas.projects.metio.wtf/usage/snippets-and-libraries/?utm_source=atom_feed" rel="related" type="text/html" title="Snippets and libraries"/><link href="https://jaas.projects.metio.wtf/usage/admission-webhook/?utm_source=atom_feed" rel="related" type="text/html" title="Admission webhook"/><link href="https://jaas.projects.metio.wtf/usage/alerting/?utm_source=atom_feed" rel="related" type="text/html" title="Alerting"/><link href="https://jaas.projects.metio.wtf/usage/creating-sources/?utm_source=atom_feed" rel="related" type="text/html" title="Creating source artifacts"/><id>https://jaas.projects.metio.wtf/usage/jsonnet-libraries/</id><published>0001-01-01T00:00:00+00:00</published><updated>2026-06-16T20:48:39+02:00</updated><content type="html"><![CDATA[<blockquote>Reusable .libsonnet files for snippets via the JsonnetLibrary CRD and OCI-mounted shared libraries, and how imports resolve.</blockquote><p>Snippets import reusable Jsonnet from two places: namespaced <code>JsonnetLibrary</code>
custom resources and OCI-mounted shared libraries the operator carries on disk.
Both feed the same import-alias namespace, so a snippet&rsquo;s <code>import</code> statements
look identical regardless of where the library comes from.</p>
<h2 id="the-jsonnetlibrary-crd">The JsonnetLibrary CRD</h2>
<p>A <code>JsonnetLibrary</code> is a namespaced bundle of <code>.libsonnet</code> files. Like a snippet,
it declares exactly one source — inline <code>spec.files</code> or a <code>spec.sourceRef</code> to a
Flux source (<code>GitRepository</code>, <code>OCIRepository</code>, <code>Bucket</code>, <code>ExternalArtifact</code>).
The library carries no registration name of its own; the import alias is chosen
on the snippet side.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nt">apiVersion</span><span class="p">:</span><span class="w"> </span><span class="l">jaas.metio.wtf/v1</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">kind</span><span class="p">:</span><span class="w"> </span><span class="l">JsonnetLibrary</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">metadata</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">grafana-helpers</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">namespace</span><span class="p">:</span><span class="w"> </span><span class="l">default</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">spec</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">files</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">dashboard.libsonnet</span><span class="p">:</span><span class="w"> </span><span class="p">|</span><span class="sd">
</span></span></span><span class="line"><span class="cl"><span class="sd">      {
</span></span></span><span class="line"><span class="cl"><span class="sd">        new(title): {
</span></span></span><span class="line"><span class="cl"><span class="sd">          title: title,
</span></span></span><span class="line"><span class="cl"><span class="sd">          panels: [],
</span></span></span><span class="line"><span class="cl"><span class="sd">          schemaVersion: 38,
</span></span></span><span class="line"><span class="cl"><span class="sd">        },
</span></span></span><span class="line"><span class="cl"><span class="sd">      }</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">panel.libsonnet</span><span class="p">:</span><span class="w"> </span><span class="p">|</span><span class="sd">
</span></span></span><span class="line"><span class="cl"><span class="sd">      {
</span></span></span><span class="line"><span class="cl"><span class="sd">        graph(title): { type: &#39;graph&#39;, title: title },
</span></span></span><span class="line"><span class="cl"><span class="sd">        stat(title): { type: &#39;stat&#39;, title: title },
</span></span></span><span class="line"><span class="cl"><span class="sd">      }</span><span class="w">
</span></span></span></code></pre></div><p>A <code>JsonnetLibrary</code> whose <code>spec.sourceRef</code> points at an <code>OCIRepository</code> lets you
ship a jb-vendored library tree (grafonnet, docsonnet, and similar) as an OCI
artifact and import it from snippets without inlining every file.</p>
<h2 id="referencing-a-library-from-a-snippet">Referencing a library from a snippet</h2>
<p>A snippet enumerates the libraries it can import in <code>spec.libraries[]</code>. Each
entry is a <code>LibraryRef</code>:</p>
<ul>
<li><code>kind</code> — <code>JsonnetLibrary</code> (the only library kind).</li>
<li><code>name</code> — the <code>JsonnetLibrary</code> resource&rsquo;s name.</li>
<li><code>importPath</code> — the alias the snippet&rsquo;s <code>import</code> statements use. Defaults to the
library&rsquo;s <code>name</code>.</li>
</ul>
<p>A library not listed in <code>spec.libraries</code> is invisible to the snippet even when it
exists in the same namespace — the enumeration is the allowlist.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nt">apiVersion</span><span class="p">:</span><span class="w"> </span><span class="l">jaas.metio.wtf/v1</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">kind</span><span class="p">:</span><span class="w"> </span><span class="l">JsonnetSnippet</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">metadata</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">my-dashboard</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">namespace</span><span class="p">:</span><span class="w"> </span><span class="l">default</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">spec</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">serviceAccountName</span><span class="p">:</span><span class="w"> </span><span class="l">grafana-tenant</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">entryFile</span><span class="p">:</span><span class="w"> </span><span class="l">main.jsonnet</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">files</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">main.jsonnet</span><span class="p">:</span><span class="w"> </span><span class="p">|</span><span class="sd">
</span></span></span><span class="line"><span class="cl"><span class="sd">      local dashboard = import &#39;grafana/dashboard.libsonnet&#39;;
</span></span></span><span class="line"><span class="cl"><span class="sd">      local panel = import &#39;grafana/panel.libsonnet&#39;;
</span></span></span><span class="line"><span class="cl"><span class="sd">      dashboard.new(&#39;API Latency&#39;) + {
</span></span></span><span class="line"><span class="cl"><span class="sd">        panels: [
</span></span></span><span class="line"><span class="cl"><span class="sd">          panel.graph(&#39;p99 by route&#39;),
</span></span></span><span class="line"><span class="cl"><span class="sd">          panel.stat(&#39;error rate&#39;),
</span></span></span><span class="line"><span class="cl"><span class="sd">        ],
</span></span></span><span class="line"><span class="cl"><span class="sd">      }</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">libraries</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span>- <span class="nt">kind</span><span class="p">:</span><span class="w"> </span><span class="l">JsonnetLibrary</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">grafana-helpers</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">importPath</span><span class="p">:</span><span class="w"> </span><span class="l">grafana</span><span class="w">
</span></span></span></code></pre></div><p>The snippet imports <code>grafana/dashboard.libsonnet</code> because the <code>LibraryRef</code> sets
<code>importPath: grafana</code>. Drop <code>importPath</code> and the alias defaults to the library&rsquo;s
name, <code>grafana-helpers</code>. The operator reads the <code>JsonnetLibrary</code> through the
tenant&rsquo;s impersonating client, so the snippet&rsquo;s ServiceAccount needs <code>get</code> on
<code>jsonnetlibraries.jaas.metio.wtf</code> — see
<a href="/usage/tenancy-and-rbac/">Tenancy and RBAC</a>
.</p>
<h2 id="oci-mounted-shared-libraries">OCI-mounted shared libraries</h2>
<p>Cluster-wide shared libraries are mounted into the operator pod&rsquo;s filesystem
rather than expressed as CRs. The operator scans every <code>--library-path</code>
directory at startup, reads every <code>.libsonnet</code> / <code>.jsonnet</code> / <code>.json</code> file into
memory, and folds those entries into every snippet&rsquo;s import namespace
additively — after the snippet&rsquo;s own <code>LibraryRef</code> resolution. A snippet imports
an OCI-mounted library by alias with no <code>LibraryRef</code> at all:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-jsonnet" data-lang="jsonnet"><span class="line"><span class="cl"><span class="k">local</span><span class="w"> </span><span class="nv">grafonnet</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="k">import</span><span class="w"> </span><span class="s">&#39;grafonnet/main.libsonnet&#39;</span><span class="p">;</span><span class="w">
</span></span></span></code></pre></div><p>With the Helm chart this is the <code>additionalLibraries</code> value, which mounts each
configured OCI artifact under a <code>--library-path</code> directory. There is deliberately
no cluster-scoped library CRD: a snippet produces a namespaced
<code>ExternalArtifact</code>, so producers stay namespaced, while genuinely cluster-wide
shared libraries take the OCI-mount path. This is also the path the cluster-free
local renderer uses, so the same library tree renders identically on a
workstation and in the cluster.</p>
<h3 id="library-alias-safety">Library-alias safety</h3>
<p>An OCI-mounted alias and a <code>LibraryRef</code> alias must not collide. When the operator
starts with <code>--library-path</code> flags it records every mounted alias, and both
admission and the reconciler reject any <code>LibraryRef</code> whose <code>importPath</code> (or, when
<code>importPath</code> is omitted, the library <code>name</code>) shadows one of those names. This
catches the case where grafonnet is mounted via OCI but a <code>JsonnetLibrary</code>
<code>LibraryRef</code> is also aliased <code>grafonnet</code> — the additive merge would otherwise
resolve the collision silently in favor of the CR. Rename the import alias or
remove the <code>LibraryRef</code> to resolve the rejection.</p>
<h2 id="jsonnetlibrary-vs-a-source-output-snippet">JsonnetLibrary vs a source-output snippet</h2>
<p>A <code>JsonnetLibrary</code> and a <code>JsonnetSnippet</code> rendered in <code>source</code> output mode both
hand Jsonnet to another snippet, so they can look interchangeable. They differ
on one axis — whether they publish an artifact:</p>
<table>
	<thead>
			<tr>
					<th></th>
					<th><code>JsonnetLibrary</code></th>
					<th><code>source</code>-output <code>JsonnetSnippet</code></th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td>Role</td>
					<td>Passive dependency</td>
					<td>Active producer</td>
			</tr>
			<tr>
					<td>Reached by</td>
					<td><code>import</code> by alias</td>
					<td><code>spec.sourceRef</code> to its <code>ExternalArtifact</code></td>
			</tr>
			<tr>
					<td>When it loads</td>
					<td>In-process during a snippet&rsquo;s evaluation</td>
					<td>Fetched as a tarball before evaluation</td>
			</tr>
			<tr>
					<td>Scope</td>
					<td>Same namespace as the importing snippet</td>
					<td>Cross-namespace via the <code>ExternalArtifact</code></td>
			</tr>
			<tr>
					<td>Publishes an artifact</td>
					<td>No</td>
					<td>Yes — content-addressed and revisioned</td>
			</tr>
	</tbody>
</table>
<p>A <code>JsonnetLibrary</code> is a passive dependency: a snippet lists it in
<code>spec.libraries</code>, imports it by alias, and the operator folds its files into the
import namespace in-process while evaluating. It publishes no artifact and is
visible only within its own namespace.</p>
<p>A <code>source</code>-output <code>JsonnetSnippet</code> is an active producer: it publishes an
<code>ExternalArtifact</code> carrying its raw Jsonnet. That artifact is content-addressed,
revisioned, and consumable across namespaces, so a downstream snippet pins it
with <code>spec.sourceRef</code> and re-evaluates the Jsonnet itself.</p>
<p>Use a <code>JsonnetLibrary</code> for shared helpers your snippets import by alias. Use
<code>output: source</code> chaining when one snippet&rsquo;s Jsonnet should feed another as a
pinned Flux artifact — see
<a href="/usage/snippet-sources/#chaining-snippets">Chaining snippets</a>
.</p>
<h2 id="import-resolution">Import resolution</h2>
<p>The operator&rsquo;s in-memory importer resolves <code>import</code> and <code>importstr</code> statements
with the same semantics as <code>jsonnet -J vendor</code>. A jb-vendored library tree
renders identically on the operator path and locally — this parity is the reason
the same Jsonnet works on a workstation and in the cluster without change. For
an import path, resolution proceeds:</p>
<ol>
<li><strong>Sibling-relative</strong> — relative to the importing file within its own root, so
a bare <code>import 'dashboard.libsonnet'</code>, <code>./x</code>, or <code>../x</code> resolves against the
importing file&rsquo;s directory first.</li>
<li><strong>Bare alias</strong> — a registered alias on its own resolves to that library&rsquo;s
<code>main.libsonnet</code>.</li>
<li><strong>Alias plus file</strong> — <code>alias/file</code> resolves <code>file</code> within the registered
alias&rsquo;s tree; the alias head is authoritative.</li>
<li><strong>JPATH / vendor search</strong> — the import path is searched across the snippet&rsquo;s
own files and then every library, which is what lets an absolute
<code>import 'github.com/grafana/grafonnet/gen/...'</code> resolve against a library
whose tree carries the full vendor path.</li>
</ol>
<p>Sibling files win over a library&rsquo;s default entry, matching <code>jsonnet -J vendor</code>.
A slash-prefixed path whose head is not a registered alias is not an error — it
falls through to the vendor search.</p>
<h2 id="related-pages">Related pages</h2>
<ul>
<li><a href="/usage/snippet-sources/">Snippet sources</a>
 — where a snippet&rsquo;s own Jsonnet
comes from, including the same <code>sourceRef</code> mechanism libraries use.</li>
<li><a href="/usage/snippets-and-libraries/">Snippets and libraries</a>
 — the on-disk
equivalent for the HTTP renderer, including <code>--library-path</code> precedence.</li>
</ul>
]]></content><category scheme="https://jaas.projects.metio.wtf/tags/operator" term="operator" label="operator"/><category scheme="https://jaas.projects.metio.wtf/tags/libraries" term="libraries" label="libraries"/><category scheme="https://jaas.projects.metio.wtf/tags/imports" term="imports" label="imports"/></entry><entry><title type="html">JsonnetLibrary</title><link href="https://jaas.projects.metio.wtf/api/jsonnetlibrary/?utm_source=atom_feed" rel="alternate" type="text/html"/><link href="https://jaas.projects.metio.wtf/api/externalartifact/?utm_source=atom_feed" rel="related" type="text/html" title="ExternalArtifact output contract"/><link href="https://jaas.projects.metio.wtf/usage/jsonnet-libraries/?utm_source=atom_feed" rel="related" type="text/html" title="Jsonnet libraries"/><link href="https://jaas.projects.metio.wtf/api/jsonnetsnippet/?utm_source=atom_feed" rel="related" type="text/html" title="JsonnetSnippet"/><link href="https://jaas.projects.metio.wtf/usage/admission-webhook/?utm_source=atom_feed" rel="related" type="text/html" title="Admission webhook"/><link href="https://jaas.projects.metio.wtf/usage/alerting/?utm_source=atom_feed" rel="related" type="text/html" title="Alerting"/><id>https://jaas.projects.metio.wtf/api/jsonnetlibrary/</id><published>0001-01-01T00:00:00+00:00</published><updated>2026-06-16T20:48:39+02:00</updated><content type="html"><![CDATA[<blockquote>Field-by-field reference for the JsonnetLibrary custom resource at apiVersion jaas.metio.wtf/v1.</blockquote><p><code>JsonnetLibrary</code> (<code>jlib</code>) is a namespaced bundle of <code>.libsonnet</code> files that
<code>JsonnetSnippet</code> CRs in the same namespace can import. The library carries no
artifact of its own and has no controller reconciling it today — it exists purely
as a supply-side source for snippets. The import alias is set on the snippet side
via <code>LibraryRef.importPath</code> (defaulting to the library&rsquo;s <code>metadata.name</code>); the
library itself carries no registration name. Task-oriented guidance lives in
<a href="/usage/jsonnet-libraries/">Jsonnet libraries</a>
.</p>
<h2 id="example">Example</h2>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nt">apiVersion</span><span class="p">:</span><span class="w"> </span><span class="l">jaas.metio.wtf/v1</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">kind</span><span class="p">:</span><span class="w"> </span><span class="l">JsonnetLibrary</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">metadata</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">mylib</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">namespace</span><span class="p">:</span><span class="w"> </span><span class="l">default</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">spec</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">files</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">main.libsonnet</span><span class="p">:</span><span class="w"> </span><span class="p">|</span><span class="sd">
</span></span></span><span class="line"><span class="cl"><span class="sd">      {
</span></span></span><span class="line"><span class="cl"><span class="sd">        dashboard(env, cluster):: {
</span></span></span><span class="line"><span class="cl"><span class="sd">          title: &#39;%s / %s&#39; % [env, cluster],
</span></span></span><span class="line"><span class="cl"><span class="sd">        },
</span></span></span><span class="line"><span class="cl"><span class="sd">      }</span><span class="w">
</span></span></span></code></pre></div><p>Exactly one of <code>spec.files</code> or <code>spec.sourceRef</code> must be set. Admission rejects
CRs that set neither or both.</p>
<h2 id="spec-fields">Spec fields</h2>
<p><code>JsonnetLibrarySpec</code> embeds <code>SnippetSource</code> directly (the same source shape used
by <code>JsonnetSnippetSpec</code>).</p>
<table>
	<thead>
			<tr>
					<th>Field</th>
					<th>Type</th>
					<th>Default</th>
					<th>Description</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td><code>files</code></td>
					<td>map[string]string</td>
					<td>—</td>
					<td>Inline map of filename to Jsonnet/libsonnet source. Exactly one of <code>files</code> or <code>sourceRef</code> must be set.</td>
			</tr>
			<tr>
					<td><code>sourceRef.apiVersion</code></td>
					<td>string</td>
					<td><code>source.toolkit.fluxcd.io/v1</code></td>
					<td>APIVersion of the referenced Flux source CR.</td>
			</tr>
			<tr>
					<td><code>sourceRef.kind</code></td>
					<td>string</td>
					<td>—</td>
					<td>Kind of the referenced source. One of: <code>GitRepository</code>, <code>OCIRepository</code>, <code>Bucket</code>, <code>ExternalArtifact</code>. Required when <code>sourceRef</code> is set.</td>
			</tr>
			<tr>
					<td><code>sourceRef.name</code></td>
					<td>string</td>
					<td>—</td>
					<td>Name of the referenced source CR. Required when <code>sourceRef</code> is set. Minimum length 1.</td>
			</tr>
			<tr>
					<td><code>sourceRef.namespace</code></td>
					<td>string</td>
					<td>library&rsquo;s namespace</td>
					<td>Namespace of the referenced source CR. Cross-namespace references are rejected when the operator is started with <code>--no-cross-namespace-refs</code>.</td>
			</tr>
			<tr>
					<td><code>sourceRef.path</code></td>
					<td>string</td>
					<td>— (artifact root)</td>
					<td>Subdirectory within the fetched tarball to treat as the library root. Empty means the archive root — required for jb-vendored trees (e.g. a <code>sourceRef</code> pointing at a Flux <code>OCIRepository</code> for a JOI image) where the library aliases resolve against the full vendor tree.</td>
			</tr>
	</tbody>
</table>
<h2 id="status">Status</h2>
<p><code>JsonnetLibrary</code> shares the <code>SyncStatus</code> type with <code>JsonnetSnippet</code>, though no
controller currently populates it. The <code>status</code> subresource exists so a future
library reconciler can be added without a schema change.</p>
<table>
	<thead>
			<tr>
					<th>Field</th>
					<th>Type</th>
					<th>Description</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td><code>observedGeneration</code></td>
					<td>int64</td>
					<td><code>.metadata.generation</code> last reconciled. Not currently populated.</td>
			</tr>
			<tr>
					<td><code>conditions</code></td>
					<td>[]Condition</td>
					<td>Standard apimachinery conditions. Not currently populated.</td>
			</tr>
			<tr>
					<td><code>revision</code></td>
					<td>string</td>
					<td>Not currently populated.</td>
			</tr>
			<tr>
					<td><code>artifactURL</code></td>
					<td>string</td>
					<td>Not currently populated.</td>
			</tr>
			<tr>
					<td><code>lastSyncTime</code></td>
					<td>Time</td>
					<td>Not currently populated.</td>
			</tr>
			<tr>
					<td><code>history</code></td>
					<td>[]RevisionEntry</td>
					<td>Not currently populated.</td>
			</tr>
	</tbody>
</table>
<p>For how snippets reference libraries, see <a href="/api/jsonnetsnippet/">/api/jsonnetsnippet/</a>

(<code>spec.libraries</code>) and <a href="/usage/jsonnet-libraries/">Jsonnet libraries</a>
.</p>
]]></content><category scheme="https://jaas.projects.metio.wtf/tags/api" term="api" label="api"/><category scheme="https://jaas.projects.metio.wtf/tags/libraries" term="libraries" label="libraries"/><category scheme="https://jaas.projects.metio.wtf/tags/operator" term="operator" label="operator"/></entry><entry><title type="html">JsonnetSnippet</title><link href="https://jaas.projects.metio.wtf/api/jsonnetsnippet/?utm_source=atom_feed" rel="alternate" type="text/html"/><link href="https://jaas.projects.metio.wtf/api/externalartifact/?utm_source=atom_feed" rel="related" type="text/html" title="ExternalArtifact output contract"/><link href="https://jaas.projects.metio.wtf/usage/creating-sources/?utm_source=atom_feed" rel="related" type="text/html" title="Creating source artifacts"/><link href="https://jaas.projects.metio.wtf/api/jsonnetlibrary/?utm_source=atom_feed" rel="related" type="text/html" title="JsonnetLibrary"/><link href="https://jaas.projects.metio.wtf/usage/operator-mode/?utm_source=atom_feed" rel="related" type="text/html" title="Operator mode"/><link href="https://jaas.projects.metio.wtf/usage/admission-webhook/?utm_source=atom_feed" rel="related" type="text/html" title="Admission webhook"/><id>https://jaas.projects.metio.wtf/api/jsonnetsnippet/</id><published>0001-01-01T00:00:00+00:00</published><updated>2026-06-16T21:40:39+02:00</updated><content type="html"><![CDATA[<blockquote>Field-by-field reference for the JsonnetSnippet custom resource at apiVersion jaas.metio.wtf/v1.</blockquote><p><code>JsonnetSnippet</code> (<code>jsnip</code>) is the published unit of Jsonnet evaluation. The JaaS
operator watches these namespaced CRs, evaluates the Jsonnet they describe, and
upserts a Flux <code>ExternalArtifact</code> whose <code>status.artifact.url</code> points at the
rendered result. Task-oriented guidance lives in
<a href="/usage/operator-mode/">Operator mode</a>
 and <a href="/usage/snippet-sources/">Snippet sources</a>
.</p>
<h2 id="example">Example</h2>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nt">apiVersion</span><span class="p">:</span><span class="w"> </span><span class="l">jaas.metio.wtf/v1</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">kind</span><span class="p">:</span><span class="w"> </span><span class="l">JsonnetSnippet</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">metadata</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">hello-world</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">namespace</span><span class="p">:</span><span class="w"> </span><span class="l">default</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">spec</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">serviceAccountName</span><span class="p">:</span><span class="w"> </span><span class="l">hello-world-tenant</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">entryFile</span><span class="p">:</span><span class="w"> </span><span class="l">main.jsonnet</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">output</span><span class="p">:</span><span class="w"> </span><span class="l">rendered</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">history</span><span class="p">:</span><span class="w"> </span><span class="m">3</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">interval</span><span class="p">:</span><span class="w"> </span><span class="l">10m</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">suspend</span><span class="p">:</span><span class="w"> </span><span class="kc">false</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">files</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">main.jsonnet</span><span class="p">:</span><span class="w"> </span><span class="p">|</span><span class="sd">
</span></span></span><span class="line"><span class="cl"><span class="sd">      local lib = import &#39;mylib/main.libsonnet&#39;;
</span></span></span><span class="line"><span class="cl"><span class="sd">      lib.dashboard(std.extVar(&#39;env&#39;), std.extVar(&#39;cluster&#39;))</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">libraries</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span>- <span class="nt">kind</span><span class="p">:</span><span class="w"> </span><span class="l">JsonnetLibrary</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">mylib</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">importPath</span><span class="p">:</span><span class="w"> </span><span class="l">mylib</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">externalVariables</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">env</span><span class="p">:</span><span class="w"> </span><span class="l">production</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">cluster</span><span class="p">:</span><span class="w"> </span><span class="l">eu-west-1</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">tlas</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">title</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">My Dashboard</span><span class="w">
</span></span></span></code></pre></div><p>Exactly one of <code>spec.files</code> or <code>spec.sourceRef</code> must be set. Admission rejects
CRs that set neither or both.</p>
<h2 id="spec-fields">Spec fields</h2>
<table>
	<thead>
			<tr>
					<th>Field</th>
					<th>Type</th>
					<th>Default</th>
					<th>Description</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td><code>serviceAccountName</code></td>
					<td>string</td>
					<td>—</td>
					<td>ServiceAccount the operator impersonates for every Kubernetes API call made on behalf of this snippet (source fetches, ExternalArtifact upserts). Must exist in the snippet&rsquo;s namespace. When empty, the operator&rsquo;s <code>--default-service-account</code> applies. Reconciliation is denied when neither is set (<code>ReasonServiceAccountMissing</code>).</td>
			</tr>
			<tr>
					<td><code>entryFile</code></td>
					<td>string</td>
					<td><code>main.jsonnet</code></td>
					<td>File (relative to the resolved source root) that go-jsonnet evaluates. Restricted to <code>[A-Za-z0-9._/-]+</code> with no <code>..</code> segments. Maximum 255 characters.</td>
			</tr>
			<tr>
					<td><code>files</code></td>
					<td>map[string]string</td>
					<td>—</td>
					<td>Inline map of filename to Jsonnet source. Exactly one of <code>files</code> or <code>sourceRef</code> must be set.</td>
			</tr>
			<tr>
					<td><code>sourceRef.apiVersion</code></td>
					<td>string</td>
					<td><code>source.toolkit.fluxcd.io/v1</code></td>
					<td>APIVersion of the referenced Flux source CR.</td>
			</tr>
			<tr>
					<td><code>sourceRef.kind</code></td>
					<td>string</td>
					<td>—</td>
					<td>Kind of the referenced source. One of: <code>GitRepository</code>, <code>OCIRepository</code>, <code>Bucket</code>, <code>ExternalArtifact</code>. Required when <code>sourceRef</code> is set.</td>
			</tr>
			<tr>
					<td><code>sourceRef.name</code></td>
					<td>string</td>
					<td>—</td>
					<td>Name of the referenced source CR. Required when <code>sourceRef</code> is set. Minimum length 1.</td>
			</tr>
			<tr>
					<td><code>sourceRef.namespace</code></td>
					<td>string</td>
					<td>snippet&rsquo;s namespace</td>
					<td>Namespace of the referenced source CR. Cross-namespace references are rejected when the operator is started with <code>--no-cross-namespace-refs</code>.</td>
			</tr>
			<tr>
					<td><code>sourceRef.path</code></td>
					<td>string</td>
					<td>— (artifact root)</td>
					<td>Subdirectory within the fetched tarball to treat as the source root. Empty means the archive root.</td>
			</tr>
			<tr>
					<td><code>libraries</code></td>
					<td>[]LibraryRef</td>
					<td>—</td>
					<td><code>JsonnetLibrary</code> CRs importable from this snippet. Libraries not listed here are invisible to the snippet even when present in the cluster. See <a href="/usage/jsonnet-libraries/">Jsonnet libraries</a>
.</td>
			</tr>
			<tr>
					<td><code>libraries[*].apiVersion</code></td>
					<td>string</td>
					<td><code>jaas.metio.wtf/v1</code></td>
					<td>APIVersion of the library CR.</td>
			</tr>
			<tr>
					<td><code>libraries[*].kind</code></td>
					<td>string</td>
					<td>—</td>
					<td>Kind of the library CR. Currently only <code>JsonnetLibrary</code> is accepted. Required.</td>
			</tr>
			<tr>
					<td><code>libraries[*].name</code></td>
					<td>string</td>
					<td>—</td>
					<td>Name of the referenced <code>JsonnetLibrary</code> CR. Required. Minimum length 1.</td>
			</tr>
			<tr>
					<td><code>libraries[*].namespace</code></td>
					<td>string</td>
					<td>snippet&rsquo;s namespace</td>
					<td>Namespace of the referenced library CR. Cross-namespace references are rejected when <code>--no-cross-namespace-refs</code> is set.</td>
			</tr>
			<tr>
					<td><code>libraries[*].importPath</code></td>
					<td>string</td>
					<td>library&rsquo;s <code>metadata.name</code></td>
					<td>Alias used in <code>import</code> statements inside the snippet&rsquo;s Jsonnet source. Collisions with OCI-mounted shared library aliases are rejected at admission.</td>
			</tr>
			<tr>
					<td><code>tlas</code></td>
					<td><code>map[string][]string</code></td>
					<td>—</td>
					<td>Top-level arguments passed to the snippet&rsquo;s outermost function. A single-element value becomes a string TLA; multiple values are passed as a JSON-encoded array, matching the HTTP query-parameter convention.</td>
			</tr>
			<tr>
					<td><code>externalVariables</code></td>
					<td>map[string]string</td>
					<td>—</td>
					<td>Seeds <code>std.extVar</code> lookups for this snippet&rsquo;s evaluation. Keys that conflict with the operator&rsquo;s <code>--ext-var</code> set are rejected at admission; if admission is bypassed, the reconciler refuses the conflicting key with <code>ReasonExternalVariableConflict</code>.</td>
			</tr>
			<tr>
					<td><code>output</code></td>
					<td>string</td>
					<td><code>rendered</code></td>
					<td>What bytes the published ExternalArtifact carries. <code>rendered</code>: the evaluated JSON (a single <code>rendered.json</code> in the tarball). <code>source</code>: the raw <code>.jsonnet</code>/<code>.libsonnet</code> files, for downstream consumers that re-evaluate themselves.</td>
			</tr>
			<tr>
					<td><code>suspend</code></td>
					<td>bool</td>
					<td><code>false</code></td>
					<td>When <code>true</code>, the operator skips the evaluation pipeline, leaves the existing ExternalArtifact in place, and reports <code>Ready=False</code> with reason <code>Suspended</code>. Setting back to <code>false</code> resumes reconciliation. Mirrors Flux&rsquo;s <code>spec.suspend</code> convention.</td>
			</tr>
			<tr>
					<td><code>history</code></td>
					<td>int32</td>
					<td><code>1</code></td>
					<td>Number of past revisions retained in storage. Minimum 1, maximum 50. Setting to N &gt; 1 lets downstream consumers pin to an older revision via its sha256 for rollback or blue-green flows. The keep-set is tracked in <code>status.history</code>.</td>
			</tr>
			<tr>
					<td><code>interval</code></td>
					<td>Duration</td>
					<td>— (watch-only)</td>
					<td>Period between successful reconciles regardless of watch events. Picks up state outside the watched graph (environment drift, OCI library refreshes, etc.). Bounded at admission to between <code>30s</code> and <code>24h</code>. Failed reconciles use controller-runtime&rsquo;s exponential backoff; <code>interval</code> governs only the steady-state cadence.</td>
			</tr>
	</tbody>
</table>
<h2 id="status">Status</h2>
<p><code>status</code> follows the <code>SyncStatus</code> shape shared by all JaaS CRs.</p>
<table>
	<thead>
			<tr>
					<th>Field</th>
					<th>Type</th>
					<th>Description</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td><code>observedGeneration</code></td>
					<td>int64</td>
					<td><code>.metadata.generation</code> of the spec the controller last reconciled. Lets clients distinguish stale status from up-to-date.</td>
			</tr>
			<tr>
					<td><code>conditions</code></td>
					<td>[]Condition</td>
					<td>Standard apimachinery conditions. The <code>Ready</code> condition summarises whether the most recent reconcile succeeded; <code>reason</code> and <code>message</code> carry per-stage failure detail. See Ready condition reasons below.</td>
			</tr>
			<tr>
					<td><code>revision</code></td>
					<td>string</td>
					<td><code>sha256:&lt;hex&gt;</code> content hash of the last successfully reconciled source. Empty until the first successful reconcile.</td>
			</tr>
			<tr>
					<td><code>artifactURL</code></td>
					<td>string</td>
					<td>HTTP URL of the last successfully published artifact tarball. Preserved across subsequent failures so the last-known-good URL stays observable. Empty until the first successful publish.</td>
			</tr>
			<tr>
					<td><code>lastSyncTime</code></td>
					<td>Time</td>
					<td>Timestamp of the most recent successful reconcile.</td>
			</tr>
			<tr>
					<td><code>history</code></td>
					<td>[]RevisionEntry</td>
					<td>Most-recent N revisions retained in storage (<code>N</code> = <code>spec.history</code>). Index 0 is the most recent (matches <code>revision</code>). Each entry carries <code>revision</code> (sha256:hex) and <code>time</code> (publish time).</td>
			</tr>
	</tbody>
</table>
<h3 id="ready-condition-reasons">Ready condition reasons</h3>
<p>Every reason string is wire-stable — runbooks key off these values.</p>
<table>
	<thead>
			<tr>
					<th>Reason</th>
					<th>Status</th>
					<th>Description</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td><code>Synced</code></td>
					<td>True</td>
					<td>Most recent reconcile completed end-to-end and produced a publishable artifact.</td>
			</tr>
			<tr>
					<td><code>Pending</code></td>
					<td>False</td>
					<td>Snippet observed but not yet reconciled (transient).</td>
			</tr>
			<tr>
					<td><code>Suspended</code></td>
					<td>False</td>
					<td><code>spec.suspend</code> is <code>true</code>; evaluation is paused.</td>
			</tr>
			<tr>
					<td><code>InvalidSpec</code></td>
					<td>False</td>
					<td>Spec-level validation failure (missing <code>main.jsonnet</code>, invalid source combination, etc.).</td>
			</tr>
			<tr>
					<td><code>LibraryNotFound</code></td>
					<td>False</td>
					<td>A <code>spec.libraries</code> entry references a <code>JsonnetLibrary</code> CR that cannot be found.</td>
			</tr>
			<tr>
					<td><code>CrossNamespaceRefRejected</code></td>
					<td>False</td>
					<td><code>--no-cross-namespace-refs</code> is enabled and a library or source reference is outside the snippet&rsquo;s namespace.</td>
			</tr>
			<tr>
					<td><code>ExternalVariableConflict</code></td>
					<td>False</td>
					<td><code>spec.externalVariables</code> names a key already owned by the operator&rsquo;s <code>--ext-var</code> set.</td>
			</tr>
			<tr>
					<td><code>ServiceAccountMissing</code></td>
					<td>False</td>
					<td>Neither <code>spec.serviceAccountName</code> nor <code>--default-service-account</code> is set.</td>
			</tr>
			<tr>
					<td><code>EvaluationFailed</code></td>
					<td>False</td>
					<td>go-jsonnet returned a diagnostic (syntax error, runtime error, etc.).</td>
			</tr>
			<tr>
					<td><code>EvaluationTimeout</code></td>
					<td>False</td>
					<td>The eval deadline fired before the snippet finished.</td>
			</tr>
			<tr>
					<td><code>SourceNotReady</code></td>
					<td>False</td>
					<td>The referenced Flux source CR exists but is not yet <code>Ready</code> or has no <code>status.artifact</code>.</td>
			</tr>
			<tr>
					<td><code>SourceFetchFailed</code></td>
					<td>False</td>
					<td>Fetching or verifying the source artifact failed (HTTP error, digest mismatch, tar corruption).</td>
			</tr>
			<tr>
					<td><code>SourceRefNotYetSupported</code></td>
					<td>False</td>
					<td><code>spec.sourceRef</code> is set but the operator is running without <code>--enable-flux-integration</code>. Start the operator with that flag, or remove <code>spec.sourceRef</code> from the snippet.</td>
			</tr>
			<tr>
					<td><code>DependencyCycle</code></td>
					<td>False</td>
					<td>The snippet&rsquo;s dependency chain (via <code>spec.sourceRef</code> or <code>spec.libraries</code>) transitively points back at itself.</td>
			</tr>
			<tr>
					<td><code>ArtifactTooLarge</code></td>
					<td>False</td>
					<td>Rendered content exceeds the operator&rsquo;s <code>--max-artifact-bytes</code> limit.</td>
			</tr>
			<tr>
					<td><code>RBACDenied</code></td>
					<td>False</td>
					<td>An apiserver call failed with Forbidden, or the source CR&rsquo;s kind is not registered. Non-transient — backoff is disabled. The message names the verb and resource the cluster operator must grant.</td>
			</tr>
	</tbody>
</table>
<p>A runbook page for each reason lives at <code>/runbooks/&lt;reason-lowercased&gt;/</code> on this site. See <a href="/usage/operator-mode/">Operator mode</a>
 for lifecycle details and <a href="/api/externalartifact/">ExternalArtifact output contract</a>
 for the artifact contract.</p>
]]></content><category scheme="https://jaas.projects.metio.wtf/tags/api" term="api" label="api"/><category scheme="https://jaas.projects.metio.wtf/tags/snippets" term="snippets" label="snippets"/><category scheme="https://jaas.projects.metio.wtf/tags/operator" term="operator" label="operator"/><category scheme="https://jaas.projects.metio.wtf/tags/flux" term="flux" label="flux"/></entry><entry><title type="html">Kubernetes</title><link href="https://jaas.projects.metio.wtf/installation/kubernetes/?utm_source=atom_feed" rel="alternate" type="text/html"/><link href="https://jaas.projects.metio.wtf/installation/helm-values/?utm_source=atom_feed" rel="related" type="text/html" title="Helm chart values"/><link href="https://jaas.projects.metio.wtf/tutorials/quickstart/?utm_source=atom_feed" rel="related" type="text/html" title="Quickstart"/><link href="https://jaas.projects.metio.wtf/usage/admission-webhook/?utm_source=atom_feed" rel="related" type="text/html" title="Admission webhook"/><link href="https://jaas.projects.metio.wtf/usage/alerting/?utm_source=atom_feed" rel="related" type="text/html" title="Alerting"/><link href="https://jaas.projects.metio.wtf/installation/configuration/?utm_source=atom_feed" rel="related" type="text/html" title="Configuration reference"/><id>https://jaas.projects.metio.wtf/installation/kubernetes/</id><published>0001-01-01T00:00:00+00:00</published><updated>2026-06-17T16:04:27+02:00</updated><content type="html"><![CDATA[<blockquote>Install JaaS on Kubernetes with the Helm chart in either of its two modes — OCI volume mounting or Flux CR-based.</blockquote><p>JaaS ships as a container image at <code>ghcr.io/metio/jaas:latest</code> and as a Helm
chart at <code>oci://ghcr.io/metio/helm-charts/jaas</code>. Pre-built binaries for Linux,
macOS, and Windows are attached to each GitHub release for operators who prefer
to run the binary directly.</p>
<h2 id="prerequisites">Prerequisites</h2>
<ul>
<li>A <a href="https://kubernetes.io/">Kubernetes</a>
 cluster, <strong>v1.28 or later</strong>, with
<code>kubectl</code> configured against it.</li>
<li><a href="https://helm.sh/">Helm</a>
 <strong>v3.14 or later</strong> — OCI chart support is required to
pull the chart from <code>ghcr.io</code>.</li>
</ul>
<p>The Flux CR-based mode (below) additionally needs:</p>
<ul>
<li><a href="https://fluxcd.io/">Flux</a>
 <strong>v2.7.0 or later</strong> in the cluster — the
<code>ExternalArtifact</code> CRD that JaaS publishes lands in v2.7.0.</li>
<li><a href="https://cert-manager.io/">cert-manager</a>
 — <strong>only</strong> if you set the admission
webhook to <code>cert-manager</code> mode. The chart defaults to <code>self-signed</code>, which
provisions and rotates the webhook&rsquo;s TLS in-process and needs no cert-manager;
see <a href="/installation/production/#admission-webhook-tls">Production</a>
 for the
trade-off.</li>
</ul>
<p>The OCI volume-mounting mode needs neither Flux nor cert-manager.</p>
<h2 id="install-and-update">Install and update</h2>
<p><code>helm upgrade --install</code> is idempotent: the same command installs the chart the
first time and applies your changes on every subsequent run, so it&rsquo;s the only
deploy command you need. To update later, re-run it with an updated <code>--values</code>
file or <code>--set</code> flags.</p>
<p>The chart runs JaaS in one of two mutually exclusive modes in a single release.
Pick the one that matches your use case; you <strong>cannot</strong> combine them in one
release — the chart&rsquo;s pre-install preflight rejects the combination.</p>
<h3 id="mode-1--oci-volume-mounting-http-renderer">Mode 1 — OCI volume mounting (HTTP renderer)</h3>
<p>JaaS evaluates Jsonnet snippets on demand and returns JSON over HTTP. Snippets and
libraries are mounted into the pod from OCI artifacts as image volumes (the
<code>snippets</code> and <code>additionalLibraries</code> chart values), read straight from a registry.
There are no CRDs, no leader election, and no persistent storage — the pod is
stateless.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">helm upgrade --install jaas oci://ghcr.io/metio/helm-charts/jaas <span class="se">\
</span></span></span><span class="line"><span class="cl">  --namespace jaas-system --create-namespace <span class="se">\
</span></span></span><span class="line"><span class="cl">  --values my-values.yaml <span class="se">\
</span></span></span><span class="line"><span class="cl">  --wait
</span></span></code></pre></div><p>A minimal <code>my-values.yaml</code> — <code>snippets</code> and <code>additionalLibraries</code> are maps of
<code>name: image-reference</code>:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="c"># Snippets to render — a map of name: image. The name becomes the URL path, so</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="c"># this snippet is served at GET /jsonnet/dashboards.</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">snippets</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">dashboards</span><span class="p">:</span><span class="w"> </span><span class="l">ghcr.io/my-org/my-dashboards:latest</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="c"># Well-known libraries have a built-in toggle — enable grafonnet with one flag</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="c"># (the chart already knows its JOI image). docsonnet and xtd work the same way.</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">libraries</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">grafonnet</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">enabled</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="c"># additionalLibraries mounts any OTHER library image — a JOI library without a</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="c"># built-in toggle, or your own private bundle. The map KEY is the directory the</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="c"># image mounts under and that the renderer adds to its import search path</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="c"># (`--library-path /srv/libraries/&lt;key&gt;`); it must be unique. The entry below</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="c"># mounts ghcr.io/acme/jsonnet-acme-lib at /srv/libraries/acme.</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">additionalLibraries</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">acme</span><span class="p">:</span><span class="w"> </span><span class="l">ghcr.io/acme/jsonnet-acme-lib:latest</span><span class="w">
</span></span></span></code></pre></div><p>The chart mounts each image read-only and wires the renderer for you. The
<code>dashboards</code> snippet is then reachable at <code>GET /jsonnet/dashboards</code>. A library is
imported by the path it resolves to under its search directory — for a
jb-vendored image like grafonnet, the full vendor path baked into it:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-jsonnet" data-lang="jsonnet"><span class="line"><span class="cl"><span class="k">import</span><span class="w"> </span><span class="s">&#39;github.com/grafana/grafonnet/gen/grafonnet-latest/main.libsonnet&#39;</span><span class="w">
</span></span></span></code></pre></div><p>The Jsonnet HTTP server listens on port <code>8080</code> (configurable via <code>ports.http</code>).</p>
<h3 id="mode-2--flux-cr-based-operator">Mode 2 — Flux CR-based (operator)</h3>
<p>JaaS watches <code>JsonnetSnippet</code> and <code>JsonnetLibrary</code> CRs, evaluates snippets, and
publishes the results as <code>ExternalArtifact</code> resources. Downstream Flux consumers
(kustomize-controller, helm-controller, stageset-controller) fetch the rendered
JSON from the artifact server.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">helm upgrade --install jaas oci://ghcr.io/metio/helm-charts/jaas <span class="se">\
</span></span></span><span class="line"><span class="cl">  --namespace jaas-system --create-namespace <span class="se">\
</span></span></span><span class="line"><span class="cl">  --set operator.enabled<span class="o">=</span><span class="nb">true</span> <span class="se">\
</span></span></span><span class="line"><span class="cl">  --set operator.storage.persistence.enabled<span class="o">=</span><span class="nb">true</span> <span class="se">\
</span></span></span><span class="line"><span class="cl">  --wait
</span></span></code></pre></div><p>A minimal values snippet for the operator shape:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nt">operator</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">enabled</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">storage</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="c"># local backend with a PVC — enough for a single-replica install. For</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="c"># multi-replica HA, switch to backend: s3 (see /installation/production/).</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">backend</span><span class="p">:</span><span class="w"> </span><span class="l">local</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">persistence</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">enabled</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">size</span><span class="p">:</span><span class="w"> </span><span class="l">10Gi</span><span class="w">
</span></span></span></code></pre></div><p>The operator publishes artifacts at the URL configured via
<code>operator.storage.baseURL</code>. Left empty, it defaults to the in-cluster Service
DNS name (<code>http://jaas-storage.&lt;namespace&gt;.svc.cluster.local:&lt;port&gt;</code>), which is
correct when downstream Flux consumers fetch artifacts from inside the cluster.
Set it explicitly only when consumers dereference the artifacts through an
Ingress or external hostname.</p>
<h3 id="how-crds-are-handled">How CRDs are handled</h3>
<p>The chart ships its CRDs (<code>JsonnetSnippet</code>, <code>JsonnetLibrary</code>) inside the regular
templates — not Helm&rsquo;s special <code>crds/</code> directory — so a <code>helm upgrade --install</code>
applies schema changes like any other resource, governed by <code>crds.create</code>
(default <code>true</code>). The CRDs carry <code>helm.sh/resource-policy: keep</code>, so a
<code>helm uninstall</code> leaves them — and your existing resources — in place; remove them
by hand only if you really mean to.</p>
<p>Check <a href="https://github.com/metio/jaas/blob/main/MIGRATIONS.md">MIGRATIONS.md</a>

before upgrading across a release that changes an immutable field such as a
Deployment&rsquo;s <code>spec.selector.matchLabels</code> — those require a manual
<code>kubectl --namespace jaas-system delete deploy jaas</code> first.</p>
<p>If you manage CRDs out of band, the raw definitions are published in the
repository under <code>config/crd/bases/</code> and can be applied with
<code>kubectl apply --server-side -f</code>.</p>
<h2 id="customize">Customize</h2>
<p>Every setting the chart exposes — the two modes above, storage backend, leader
election, the admission webhook, NetworkPolicy, service mesh, metrics, and the
rest — is a Helm value. Two references cover them:</p>
<ul>
<li><a href="/installation/helm-values/">Helm chart values</a>
 — the full values reference,
generated from the chart&rsquo;s own schema.</li>
<li><a href="/installation/configuration/">Configuration reference</a>
 — every binary flag and
the chart value that drives it.</li>
</ul>
<p>For production sizing — S3 storage, multi-replica HA, observability, and webhook
hardening — see the <a href="/installation/production/">Production guide</a>
.</p>
<h2 id="verify">Verify</h2>
<p>For the operator shape, confirm the Deployment is available and the CRDs are
registered:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">kubectl --namespace jaas-system rollout status deploy/jaas
</span></span><span class="line"><span class="cl">kubectl get crd jsonnetsnippets.jaas.metio.wtf jsonnetlibraries.jaas.metio.wtf
</span></span></code></pre></div><p>For the HTTP renderer, confirm the pod is ready and the endpoint answers:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">kubectl --namespace jaas-system get pods --selector app.kubernetes.io/name<span class="o">=</span>jaas
</span></span><span class="line"><span class="cl">kubectl --namespace jaas-system port-forward svc/jaas 8080:8080 <span class="p">&amp;</span>
</span></span><span class="line"><span class="cl">curl http://localhost:8080/jsonnet/my-dashboard
</span></span></code></pre></div><h2 id="next-steps">Next steps</h2>
<ul>
<li><a href="/tutorials/quickstart/">Quickstart tutorial</a>
 — five steps from a Helm install
to a published artifact.</li>
<li><a href="/installation/production/">Production hardening</a>
 — storage, observability, the
admission webhook, and multi-replica HA.</li>
</ul>
]]></content><category scheme="https://jaas.projects.metio.wtf/tags/installation" term="installation" label="installation"/><category scheme="https://jaas.projects.metio.wtf/tags/helm" term="helm" label="helm"/><category scheme="https://jaas.projects.metio.wtf/tags/kubernetes" term="kubernetes" label="kubernetes"/><category scheme="https://jaas.projects.metio.wtf/tags/operator" term="operator" label="operator"/></entry><entry><title type="html">LibraryNotFound</title><link href="https://jaas.projects.metio.wtf/runbooks/librarynotfound/?utm_source=atom_feed" rel="alternate" type="text/html"/><link href="https://jaas.projects.metio.wtf/runbooks/crossnamespacerefrejected/?utm_source=atom_feed" rel="related" type="text/html" title="CrossNamespaceRefRejected"/><link href="https://jaas.projects.metio.wtf/runbooks/rbacdenied/?utm_source=atom_feed" rel="related" type="text/html" title="RBACDenied"/><link href="https://jaas.projects.metio.wtf/runbooks/serviceaccountmissing/?utm_source=atom_feed" rel="related" type="text/html" title="ServiceAccountMissing"/><link href="https://jaas.projects.metio.wtf/runbooks/operator-watch-silent/?utm_source=atom_feed" rel="related" type="text/html" title="Watch-layer silent failure"/><link href="https://jaas.projects.metio.wtf/runbooks/artifacttoolarge/?utm_source=atom_feed" rel="related" type="text/html" title="ArtifactTooLarge"/><id>https://jaas.projects.metio.wtf/runbooks/librarynotfound/</id><published>0001-01-01T00:00:00+00:00</published><updated>2026-06-17T10:56:50+02:00</updated><content type="html"><![CDATA[<blockquote>A JsonnetLibrary CR referenced in spec.libraries does not exist or is unreachable by the tenant ServiceAccount</blockquote><h2 id="symptom">Symptom</h2>
<p><code>READY=False</code>, <code>REASON=LibraryNotFound</code>. The Message names the missing library.</p>
<h2 id="cause">Cause</h2>
<p>A <code>spec.libraries[*]</code> entry references a <code>JsonnetLibrary</code> CR that the operator cannot Get. Common reasons:</p>
<ul>
<li>the library CR doesn&rsquo;t exist (typo, wrong namespace, not yet applied)</li>
<li>the tenant ServiceAccount doesn&rsquo;t have <code>get</code> on the library kind in the library&rsquo;s namespace</li>
<li>the library is in a different namespace and <code>--no-cross-namespace-refs=true</code></li>
</ul>
<h2 id="diagnosis">Diagnosis</h2>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl"><span class="c1"># Confirm the library exists.</span>
</span></span><span class="line"><span class="cl">kubectl --namespace &lt;ns&gt; get jsonnetlibrary &lt;name&gt;
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Test the tenant&#39;s RBAC.</span>
</span></span><span class="line"><span class="cl">kubectl auth can-i get jsonnetlibrary &lt;name&gt; <span class="se">\
</span></span></span><span class="line"><span class="cl">  --as<span class="o">=</span>system:serviceaccount:&lt;ns&gt;:&lt;tenant-sa&gt; -n &lt;library-ns&gt;
</span></span></code></pre></div><p>If <code>can-i</code> returns <code>no</code>, RBAC is the gap.</p>
<h2 id="remediation">Remediation</h2>
<ul>
<li>create the library CR if it doesn&rsquo;t exist</li>
<li>grant <code>get</code> on <code>jsonnetlibraries.jaas.metio.wtf</code> to the tenant SA via a Role + RoleBinding</li>
<li>if cross-namespace is intended, either move the library into the snippet&rsquo;s namespace or set <code>--no-cross-namespace-refs=false</code> cluster-wide</li>
</ul>
]]></content><category scheme="https://jaas.projects.metio.wtf/tags/runbooks" term="runbooks" label="runbooks"/><category scheme="https://jaas.projects.metio.wtf/tags/troubleshooting" term="troubleshooting" label="troubleshooting"/><category scheme="https://jaas.projects.metio.wtf/tags/rbac" term="rbac" label="rbac"/></entry><entry><title type="html">Local rendering</title><link href="https://jaas.projects.metio.wtf/tutorials/local-rendering/?utm_source=atom_feed" rel="alternate" type="text/html"/><link href="https://jaas.projects.metio.wtf/usage/rendering-endpoint/?utm_source=atom_feed" rel="related" type="text/html" title="Rendering endpoint"/><id>https://jaas.projects.metio.wtf/tutorials/local-rendering/</id><published>0001-01-01T00:00:00+00:00</published><updated>2026-06-16T20:48:39+02:00</updated><content type="html"><![CDATA[<blockquote>Run JaaS as a cluster-free Jsonnet renderer over HTTP, with snippet directories, library paths, TLAs, and external variables.</blockquote><p>JaaS runs as a cluster-free Jsonnet renderer: point it at a directory of
snippets and a directory of libraries, then <code>GET</code> a snippet name to receive the
evaluated JSON. No Kubernetes, no operator mode, no Flux. The evaluation core is
the same one the operator uses, so a snippet that renders correctly here renders
identically in-cluster.</p>
<p>This tutorial runs against this repository&rsquo;s <code>examples/</code> layout, so clone the
repo first:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">git clone https://github.com/metio/jaas
</span></span><span class="line"><span class="cl"><span class="nb">cd</span> jaas
</span></span></code></pre></div><h2 id="step-1--get-the-binary-or-container-image">Step 1 — Get the binary or container image</h2>
<p>Pre-built binaries are attached to each
<a href="https://github.com/metio/jaas/releases">GitHub release</a>
. Download the archive
for your platform, unpack it, and the <code>jaas</code> binary is inside.</p>
<p>A container image is published at <code>ghcr.io/metio/jaas:latest</code>:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">docker pull ghcr.io/metio/jaas:latest
</span></span></code></pre></div><p>The examples below use a <code>jaas</code> binary on your <code>PATH</code>. To run the container
instead, mount <code>examples/</code> and map the port — for example
<code>docker run --rm -p 8080:8080 -v &quot;$PWD/examples:/examples&quot; ghcr.io/metio/jaas:latest</code>
with the flags adjusted to the in-container <code>/examples</code> paths, and
<code>--listen-address 0.0.0.0</code> so the port is reachable from the host.</p>
<h2 id="step-2--run-jaas-over-the-examples-directory">Step 2 — Run JaaS over the examples directory</h2>
<p>Start JaaS with one snippet directory and one library path:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">jaas <span class="se">\
</span></span></span><span class="line"><span class="cl">  --snippet-directory examples/snippets/dashboards <span class="se">\
</span></span></span><span class="line"><span class="cl">  --library-path examples/libraries
</span></span></code></pre></div><p><code>--snippet-directory</code> exposes each subdirectory as a snippet whose name is the
directory name and whose entry file is <code>main.jsonnet</code>. <code>--library-path</code> makes the
libraries under <code>examples/libraries</code> importable by alias. Both flags repeat, so
you can pass several of each. The Jsonnet server binds <code>127.0.0.1:8080</code> by
default.</p>
<p>Confirm it started by hitting the readiness probe on the management server:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">curl -i http://127.0.0.1:8081/ready
</span></span><span class="line"><span class="cl"><span class="c1"># HTTP/1.1 200 OK</span>
</span></span></code></pre></div><h2 id="step-3--render-a-snippet">Step 3 — Render a snippet</h2>
<p><code>examples/snippets/dashboards/inheritance</code> is a self-contained snippet. Request
it by directory name:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">curl http://127.0.0.1:8080/jsonnet/inheritance
</span></span></code></pre></div><p>JaaS returns the evaluated Jsonnet as JSON with <code>Content-Type: application/json</code>. The <code>library-precedence</code> snippet imports the <code>examplonet</code>
library you exposed with <code>--library-path</code>:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">curl http://127.0.0.1:8080/jsonnet/library-precedence
</span></span></code></pre></div><p>A snippet name that resolves to no file returns a <code>404</code> with a JSON error body;
a Jsonnet error returns a <code>400</code> carrying the go-jsonnet diagnostic.</p>
<h2 id="step-4--pass-a-top-level-argument">Step 4 — Pass a top-level argument</h2>
<p>Top-level arguments arrive as URL query parameters. The <code>multi-tla</code> snippet is
<code>function(tags=[&quot;default&quot;])</code> and joins its <code>tags</code> argument. Repeating a query
key passes a list, which becomes a JSON array TLA:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">curl <span class="s1">&#39;http://127.0.0.1:8080/jsonnet/multi-tla?tags=prod&amp;tags=eu-west&#39;</span>
</span></span><span class="line"><span class="cl"><span class="c1"># {</span>
</span></span><span class="line"><span class="cl"><span class="c1">#    &#34;count&#34;: 2,</span>
</span></span><span class="line"><span class="cl"><span class="c1">#    &#34;joined&#34;: &#34;prod, eu-west&#34;,</span>
</span></span><span class="line"><span class="cl"><span class="c1">#    &#34;list&#34;: [ &#34;prod&#34;, &#34;eu-west&#34; ]</span>
</span></span><span class="line"><span class="cl"><span class="c1"># }</span>
</span></span></code></pre></div><p>A single occurrence of a query key (<code>?tags=prod</code>) passes a string instead of a
one-element array.</p>
<h2 id="step-5--set-an-external-variable">Step 5 — Set an external variable</h2>
<p>External variables are supplied through environment variables prefixed
<code>JAAS_EXT_VAR_</code>. The variable after the prefix is the <code>std.extVar</code> key. Restart
JaaS with the variables the <code>example1</code> snippet reads (<code>name</code> and <code>key</code>):</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl"><span class="nv">JAAS_EXT_VAR_name</span><span class="o">=</span>Alice <span class="se">\
</span></span></span><span class="line"><span class="cl"><span class="nv">JAAS_EXT_VAR_key</span><span class="o">=</span>secret-value <span class="se">\
</span></span></span><span class="line"><span class="cl">jaas <span class="se">\
</span></span></span><span class="line"><span class="cl">  --snippet-directory examples/snippets/dashboards <span class="se">\
</span></span></span><span class="line"><span class="cl">  --library-path examples/libraries
</span></span></code></pre></div><p>Then render the snippet:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">curl http://127.0.0.1:8080/jsonnet/example1
</span></span><span class="line"><span class="cl"><span class="c1"># {</span>
</span></span><span class="line"><span class="cl"><span class="c1">#    ...</span>
</span></span><span class="line"><span class="cl"><span class="c1">#    &#34;person1&#34;: {</span>
</span></span><span class="line"><span class="cl"><span class="c1">#       &#34;external&#34;: &#34;secret-value&#34;,</span>
</span></span><span class="line"><span class="cl"><span class="c1">#       &#34;name&#34;: &#34;Alice&#34;,</span>
</span></span><span class="line"><span class="cl"><span class="c1">#       &#34;welcome&#34;: &#34;Hello Alice!&#34;</span>
</span></span><span class="line"><span class="cl"><span class="c1">#    },</span>
</span></span><span class="line"><span class="cl"><span class="c1">#    ...</span>
</span></span><span class="line"><span class="cl"><span class="c1"># }</span>
</span></span></code></pre></div><p><code>std.extVar('name')</code> and <code>std.extVar('key')</code> resolve to the values from the
environment. External variables are read once at startup, not per request.</p>
<h2 id="same-core-as-the-operator">Same core as the operator</h2>
<p>The <code>jaas</code> binary evaluates Jsonnet through the same evaluation core whether it
serves HTTP locally or reconciles a <code>JsonnetSnippet</code> in operator mode. Local
rendering is the fast feedback loop for snippet authoring: a snippet that renders
here — with the same libraries available — renders identically when the operator
publishes it as an <code>ExternalArtifact</code>.</p>
<h2 id="where-to-go-next">Where to go next</h2>
<ul>
<li><a href="/usage/rendering-endpoint/">Rendering endpoint</a>
 — the request shape, snippet
resolution, the management probes, and the stable error contract.</li>
<li><a href="/usage/snippets-and-libraries/">Snippets and libraries</a>
 — declaring snippets
with <code>--snippet</code> and <code>--snippet-directory</code>, and libraries with <code>--library-path</code>.</li>
<li><a href="/usage/external-variables-and-tlas/">External variables and TLAs</a>
 — the full
<code>JAAS_EXT_VAR_*</code> and query-parameter rules.</li>
</ul>
]]></content><category scheme="https://jaas.projects.metio.wtf/tags/local" term="local" label="local"/><category scheme="https://jaas.projects.metio.wtf/tags/renderer" term="renderer" label="renderer"/><category scheme="https://jaas.projects.metio.wtf/tags/http" term="http" label="http"/></entry><entry><title type="html">Logging</title><link href="https://jaas.projects.metio.wtf/usage/logging/?utm_source=atom_feed" rel="alternate" type="text/html"/><link href="https://jaas.projects.metio.wtf/usage/alerting/?utm_source=atom_feed" rel="related" type="text/html" title="Alerting"/><link href="https://jaas.projects.metio.wtf/usage/metrics/?utm_source=atom_feed" rel="related" type="text/html" title="Metrics"/><link href="https://jaas.projects.metio.wtf/usage/observability/?utm_source=atom_feed" rel="related" type="text/html" title="Observability"/><link href="https://jaas.projects.metio.wtf/usage/tracing/?utm_source=atom_feed" rel="related" type="text/html" title="Tracing"/><link href="https://jaas.projects.metio.wtf/usage/admission-webhook/?utm_source=atom_feed" rel="related" type="text/html" title="Admission webhook"/><id>https://jaas.projects.metio.wtf/usage/logging/</id><published>0001-01-01T00:00:00+00:00</published><updated>2026-06-17T22:19:37+02:00</updated><content type="html"><![CDATA[<blockquote>JaaS logs through log/slog with configurable level and format; in operator mode controller-runtime&rsquo;s own logs share the same handler. Reading JSON logs with kubectl and jq, and the Helm chart keys that drive it.</blockquote><p>JaaS logs through Go&rsquo;s <code>log/slog</code>. Every request, reconcile, and lifecycle event
is a structured record you can filter and parse rather than scrape with a regex.
In operator mode, controller-runtime&rsquo;s own output — leader election, cache sync,
manager startup — flows through the <strong>same</strong> slog handler via the logr bridge
(<code>ctrl.SetLogger(logr.FromSlogHandler(...))</code>), so the manager&rsquo;s logs share the
configured level and format instead of emitting controller-runtime&rsquo;s default zap
output.</p>
<h2 id="the-binary">The binary</h2>
<p>Two flags control logging. They apply in every mode JaaS runs in:</p>
<ul>
<li><code>--log-level</code> — <code>debug</code>, <code>info</code>, <code>warn</code>, or <code>error</code>. Default <code>info</code>.</li>
<li><code>--log-format</code> — <code>json</code> or <code>text</code>. Default <code>json</code>.</li>
</ul>
<p><code>json</code> emits one JSON object per line, the right choice for a log pipeline
(Loki, Elasticsearch, Cloud Logging) that indexes structured fields. <code>text</code>
emits human-readable key=value lines, handy when tailing logs at a terminal
during local development.</p>
<p>The full flag list with defaults is on the
<a href="/installation/configuration/">configuration page</a>
.</p>
<h3 id="reading-logs">Reading logs</h3>
<p>With the default JSON format, pipe <code>kubectl logs</code> through <code>jq</code>. Tail the
operator and pretty-print:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">kubectl --namespace jaas logs deployment/jaas --follow <span class="p">|</span> jq .
</span></span></code></pre></div><p>Filter to warnings and errors only:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">kubectl --namespace jaas logs deployment/jaas <span class="p">|</span> jq <span class="s1">&#39;select(.level == &#34;WARN&#34; or .level == &#34;ERROR&#34;)&#39;</span>
</span></span></code></pre></div><p>Follow a single snippet&rsquo;s reconciles by selecting on the logged fields:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">kubectl --namespace jaas logs deployment/jaas <span class="p">|</span> jq <span class="s1">&#39;select(.namespace == &#34;team-a&#34; and .name == &#34;dashboards&#34;)&#39;</span>
</span></span></code></pre></div><p>Turn <code>--log-level=debug</code> on temporarily to see per-request evaluation detail and
the operator&rsquo;s reconcile decisions; leave it at <code>info</code> in production to keep the
volume down.</p>
<h2 id="the-helm-chart">The Helm chart</h2>
<p>The chart exposes both flags under <code>arguments</code>:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nt">arguments</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="c"># debug, info, warn, error</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">logLevel</span><span class="p">:</span><span class="w"> </span><span class="l">info</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="c"># json, text</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">logFormat</span><span class="p">:</span><span class="w"> </span><span class="l">json</span><span class="w">
</span></span></span></code></pre></div><p>These map one-to-one onto <code>--log-level</code> and <code>--log-format</code>. Keep <code>logFormat: json</code> for any cluster whose logs are ingested by a structured pipeline; switch
to <code>text</code> only for ad-hoc local clusters where a human reads the raw stream.</p>
]]></content><category scheme="https://jaas.projects.metio.wtf/tags/operator" term="operator" label="operator"/><category scheme="https://jaas.projects.metio.wtf/tags/logging" term="logging" label="logging"/><category scheme="https://jaas.projects.metio.wtf/tags/observability" term="observability" label="observability"/></entry><entry><title type="html">Metrics</title><link href="https://jaas.projects.metio.wtf/usage/metrics/?utm_source=atom_feed" rel="alternate" type="text/html"/><link href="https://jaas.projects.metio.wtf/usage/alerting/?utm_source=atom_feed" rel="related" type="text/html" title="Alerting"/><link href="https://jaas.projects.metio.wtf/usage/logging/?utm_source=atom_feed" rel="related" type="text/html" title="Logging"/><link href="https://jaas.projects.metio.wtf/usage/observability/?utm_source=atom_feed" rel="related" type="text/html" title="Observability"/><link href="https://jaas.projects.metio.wtf/usage/tracing/?utm_source=atom_feed" rel="related" type="text/html" title="Tracing"/><link href="https://jaas.projects.metio.wtf/usage/admission-webhook/?utm_source=atom_feed" rel="related" type="text/html" title="Admission webhook"/><id>https://jaas.projects.metio.wtf/usage/metrics/</id><published>0001-01-01T00:00:00+00:00</published><updated>2026-06-17T22:19:37+02:00</updated><content type="html"><![CDATA[<blockquote>The controller-runtime Prometheus endpoint, the custom jaas_ metric family, scraping with a ServiceMonitor or a plain scrape config, querying with PromQL, and the Helm chart keys that drive it.</blockquote><p>The JaaS operator exposes a Prometheus metrics endpoint covering controller-runtime&rsquo;s
standard families plus a custom <code>jaas_*</code> family the reconciler registers. Scrape
it for dashboards and feed it into the shipped <a href="/usage/alerting/">alerts</a>
.</p>
<h2 id="the-binary">The binary</h2>
<p>controller-runtime&rsquo;s Prometheus endpoint binds <code>--metrics-bind-address</code> (default
<code>:8083</code>), serving the standard text exposition format at <code>/metrics</code>. Setting it
to <code>0</code> disables the endpoint. The default deliberately avoids controller-runtime&rsquo;s
built-in <code>:8080</code>, which would collide with the Jsonnet HTTP port.</p>
<p>The full flag list with defaults is on the
<a href="/installation/configuration/">configuration page</a>
.</p>
<h3 id="metrics-reference">Metrics reference</h3>
<p>The operator exports these custom <code>jaas_*</code> metrics, registered against
controller-runtime&rsquo;s registry so they ride the same <code>/metrics</code> endpoint:</p>
<table>
	<thead>
			<tr>
					<th>Metric</th>
					<th>Type</th>
					<th>Labels</th>
					<th>Meaning</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td><code>jaas_snippet_reconcile_total</code></td>
					<td>counter</td>
					<td><code>namespace</code>, <code>name</code>, <code>status</code>, <code>reason</code></td>
					<td>One bump per reconcile that touches the Ready condition. <code>status</code> is <code>True</code>/<code>False</code>; <code>reason</code> is the Reason constant from the snippet&rsquo;s condition.</td>
			</tr>
			<tr>
					<td><code>jaas_snippet_rendered_bytes</code></td>
					<td>histogram</td>
					<td><code>namespace</code>, <code>name</code></td>
					<td>Rendered artifact size, observed only on <code>Synced</code> reconciles. Buckets run 256 B…64 MiB.</td>
			</tr>
			<tr>
					<td><code>jaas_snippet_rate_limited_total</code></td>
					<td>counter</td>
					<td><code>namespace</code>, <code>name</code></td>
					<td>Reconciles deferred by the per-snippet token bucket. Paired with the <code>RateLimited</code> Warning event.</td>
			</tr>
			<tr>
					<td><code>jaas_snippet_eval_unavailable_total</code></td>
					<td>counter</td>
					<td><code>namespace</code>, <code>name</code></td>
					<td>Reconciles deferred because the global concurrent-eval cap was full. Paired with the <code>EvalUnavailable</code> Warning event.</td>
			</tr>
			<tr>
					<td><code>jaas_snippet_force_drop_total</code></td>
					<td>counter</td>
					<td><code>namespace</code>, <code>name</code>, <code>reason</code></td>
					<td>Snippets whose finalizer was force-dropped because <code>Publisher.Withdraw</code> kept failing past <code>--max-withdraw-wait</code> or hit a permanent API error. <code>reason</code> names the trigger (<code>withdraw_timed_out</code>, <code>tenant_client_permanent</code>, <code>withdraw_permanent</code>). Sustained non-zero values mean orphaned tarballs are accumulating; see the <a href="/runbooks/storage-recovery/">storage-recovery runbook</a>
.</td>
			</tr>
			<tr>
					<td><code>jaas_eval_in_flight</code></td>
					<td>gauge</td>
					<td>—</td>
					<td>Evaluations currently holding a slot in the global concurrent-eval semaphore. Reads through to the live count on every scrape.</td>
			</tr>
			<tr>
					<td><code>jaas_eval_max_concurrent</code></td>
					<td>gauge</td>
					<td>—</td>
					<td>Configured ceiling of the semaphore (<code>--max-concurrent-evals</code>). Zero means the gate is disabled — any saturation alert must guard on this being non-zero.</td>
			</tr>
			<tr>
					<td><code>jaas_eval_unavailable_total</code></td>
					<td>counter</td>
					<td>—</td>
					<td>Process-global accumulator of evaluations the semaphore rejected, across the HTTP and operator paths. Monotonic; resets on restart.</td>
			</tr>
			<tr>
					<td><code>jaas_eval_outstanding_timed_out</code></td>
					<td>gauge</td>
					<td>—</td>
					<td>Evaluation goroutines whose parent&rsquo;s context fired before the synchronous go-jsonnet call returned. Sustained non-zero readings flag a runaway snippet.</td>
			</tr>
			<tr>
					<td><code>jaas_storage_sweep_failures_total</code></td>
					<td>counter</td>
					<td>—</td>
					<td>Background storage-sweep passes that returned an error. The sweep removes orphaned <code>.tar.gz.tmp</code> residue; failures here don&rsquo;t block reconciles but let stale files accumulate.</td>
			</tr>
			<tr>
					<td><code>jaas_webhook_cert_renewal_failures_total</code></td>
					<td>counter</td>
					<td>—</td>
					<td>Self-signed cert renewal attempts that returned an error. Sustained non-zero values flag RBAC drift or a write-permission loss on <code>--webhook-cert-dir</code>; the existing cert&rsquo;s natural expiry is the deadline before admission breaks cluster-wide.</td>
			</tr>
			<tr>
					<td><code>jaas_tenant_token_mint_failures_total</code></td>
					<td>counter</td>
					<td><code>namespace</code>, <code>serviceAccount</code></td>
					<td><code>TokenRequest</code> mints that returned an error. Sustained non-zero values on a pair indicate revoked <code>serviceaccounts/token: create</code> or a deleted namespace; affected snippets pin Ready=Unknown.</td>
			</tr>
			<tr>
					<td><code>jaas_crd_watch_engagement_failures_total</code></td>
					<td>counter</td>
					<td><code>gvk</code></td>
					<td><code>EngageFluxWatch</code> calls that returned an error. Sustained non-zero values on a GVK mean dependent snippets won&rsquo;t re-render on upstream source events until the watch engages.</td>
			</tr>
	</tbody>
</table>
<p>The eval gauges (<code>jaas_eval_in_flight</code>, <code>jaas_eval_max_concurrent</code>,
<code>jaas_eval_outstanding_timed_out</code>) reflect the global concurrent-eval cap; see
<a href="/usage/evaluation-and-security/">evaluation and security</a>
 for how that cap works
and how to size <code>--max-concurrent-evals</code>.</p>
<p>Alongside these, controller-runtime contributes its standard families for free —
<code>controller_runtime_reconcile_total</code>, <code>controller_runtime_reconcile_time_seconds</code>,
the <code>workqueue_*</code> series (depth, latency, retries), and the Go/process
collectors. The shipped <a href="/usage/alerting/">alerts</a>
 build on both the <code>jaas_*</code>
metrics and these controller-runtime signals.</p>
<h3 id="querying-with-promql">Querying with PromQL</h3>
<p>Once scraped, a few PromQL queries answer the common questions:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-promql" data-lang="promql"><span class="line"><span class="cl"><span class="c1"># Rate of failed reconciles per snippet, excluding healthy/intentional states.</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="k">sum</span><span class="w"> </span><span class="k">by</span><span class="w"> </span><span class="o">(</span><span class="nv">namespace</span><span class="p">,</span><span class="w"> </span><span class="nv">name</span><span class="o">)</span><span class="w"> </span><span class="o">(</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="kr">rate</span><span class="o">(</span><span class="nv">jaas_snippet_reconcile_total</span><span class="p">{</span><span class="nl">status</span><span class="o">=</span><span class="p">&#34;</span><span class="s">False</span><span class="p">&#34;,</span><span class="nl">reason</span><span class="o">!~</span><span class="p">&#34;</span><span class="s">Synced|Suspended|Pending</span><span class="p">&#34;}[</span><span class="s">5m</span><span class="p">]</span><span class="o">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="o">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="c1"># Eval semaphore saturation, guarded on the gate being enabled.</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nv">jaas_eval_in_flight</span><span class="w"> </span><span class="o">/</span><span class="w"> </span><span class="nv">jaas_eval_max_concurrent</span><span class="w"> </span><span class="ow">and</span><span class="w"> </span><span class="nv">jaas_eval_max_concurrent</span><span class="w"> </span><span class="o">&gt;</span><span class="w"> </span><span class="mi">0</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="c1"># p99 rendered artifact size per snippet.</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="kr">histogram_quantile</span><span class="o">(</span><span class="mf">0.99</span><span class="p">,</span><span class="w"> </span><span class="k">sum</span><span class="w"> </span><span class="k">by</span><span class="w"> </span><span class="o">(</span><span class="nv">namespace</span><span class="p">,</span><span class="w"> </span><span class="nv">name</span><span class="p">,</span><span class="w"> </span><span class="nv">le</span><span class="o">)</span><span class="w"> </span><span class="o">(</span><span class="kr">rate</span><span class="o">(</span><span class="nv">jaas_snippet_rendered_bytes_bucket</span><span class="p">[</span><span class="s">30m</span><span class="p">]</span><span class="o">)))</span><span class="w">
</span></span></span></code></pre></div><h2 id="the-helm-chart">The Helm chart</h2>
<p>The metrics port is set under <code>ports</code>, and the chart wires it to a dedicated
<code>jaas-metrics</code> Service whenever the operator is enabled:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nt">ports</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="c"># controller-runtime metrics endpoint; maps to --metrics-bind-address.</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="c"># Set to 0 in operator.metrics.enabled to disable entirely.</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">metrics</span><span class="p">:</span><span class="w"> </span><span class="m">8083</span><span class="w">
</span></span></span></code></pre></div><p>Scraping is configured under <code>operator.metrics</code>. A <code>ServiceMonitor</code> for the
Prometheus Operator is opt-in — it selects the <code>jaas-metrics</code> Service and scrapes
its <code>metrics</code> port at <code>/metrics</code>:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nt">operator</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">enabled</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">metrics</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">enabled</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">serviceMonitor</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">enabled</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">interval</span><span class="p">:</span><span class="w"> </span><span class="l">30s</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">scrapeTimeout</span><span class="p">:</span><span class="w"> </span><span class="l">10s</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="c"># Labels your Prometheus instance selects ServiceMonitors on.</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">labels</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">release</span><span class="p">:</span><span class="w"> </span><span class="l">kube-prometheus</span><span class="w">
</span></span></span></code></pre></div><p>Without the Prometheus Operator, point a plain Prometheus scrape config at the
<code>jaas-metrics</code> Service (port <code>8083</code>, path <code>/metrics</code>), or add the usual
<code>prometheus.io/scrape</code> annotation set to the pod through <code>pod.additionalLabels</code>
and let a kubernetes-pods scrape job discover it.</p>
<p>To turn the alerts on, see <a href="/usage/alerting/">Alerting</a>
.</p>
]]></content><category scheme="https://jaas.projects.metio.wtf/tags/operator" term="operator" label="operator"/><category scheme="https://jaas.projects.metio.wtf/tags/metrics" term="metrics" label="metrics"/><category scheme="https://jaas.projects.metio.wtf/tags/prometheus" term="prometheus" label="prometheus"/><category scheme="https://jaas.projects.metio.wtf/tags/observability" term="observability" label="observability"/></entry><entry><title type="html">Network policy</title><link href="https://jaas.projects.metio.wtf/usage/network-policy/?utm_source=atom_feed" rel="alternate" type="text/html"/><link href="https://jaas.projects.metio.wtf/usage/service-mesh/?utm_source=atom_feed" rel="related" type="text/html" title="Service mesh"/><link href="https://jaas.projects.metio.wtf/usage/evaluation-and-security/?utm_source=atom_feed" rel="related" type="text/html" title="Evaluation and security"/><link href="https://jaas.projects.metio.wtf/installation/production/?utm_source=atom_feed" rel="related" type="text/html" title="Production"/><id>https://jaas.projects.metio.wtf/usage/network-policy/</id><published>0001-01-01T00:00:00+00:00</published><updated>2026-06-17T16:04:27+02:00</updated><content type="html"><![CDATA[<blockquote>The opt-in NetworkPolicy the chart ships — pod-scoped allowlists vs. a namespace-wide default-deny, choosing a policy engine, the ingress and egress traffic JaaS needs, and how to tighten each port.</blockquote><p>The Helm chart ships an opt-in <code>NetworkPolicy</code> for the JaaS pod. It is off by
default and renders only when <code>networkPolicy.enabled</code> is <code>true</code>. Two independent
layers are on offer: pod-scoped allowlists that lock down only JaaS&rsquo;s own pods
(the safe default), and an additional namespace-wide default-deny for a zero-trust
namespace. The ingress and egress tables below describe exactly what traffic JaaS
depends on — in both renderer mode and <a href="/usage/operator-mode/">operator mode</a>
 — so
everything else can be denied.</p>
<h2 id="two-layers-pod-scoped-allowlists-vs-namespace-default-deny">Two layers: pod-scoped allowlists vs. namespace default-deny</h2>
<p><code>networkPolicy.enabled: true</code> renders per-workload, <strong>pod-scoped allowlist</strong>
policies. They select only JaaS&rsquo;s own pods through their <code>app.kubernetes.io/*</code>
labels and lock down just those pods to the required ports. This is the safe
default and is fine in a shared namespace: co-located workloads — including
anything in <code>flux-system</code> if JaaS shares that namespace — are untouched.</p>
<p><code>networkPolicy.defaultDeny.enabled</code> (default <code>false</code>) <strong>additionally</strong> renders a
namespace-wide default-deny so every pod in the namespace is denied by default and
the allowlists become the only exceptions (a zero-trust namespace). The default-deny
sits at a lower precedence than the allowlists, so the allowlists always win for the
JaaS pods while everything else is denied.</p>
<p>Pick the layer that matches namespace ownership:</p>
<ul>
<li><strong><code>defaultDeny.enabled: false</code></strong> (default) — pod-scoped setup. Only JaaS&rsquo;s pods
are locked down; neighbours keep whatever posture their own policies give them.</li>
<li><strong><code>defaultDeny.enabled: true</code></strong> — namespace zero-trust. Enable this <strong>only when
JaaS owns its namespace</strong>, because the deny-all also denies every co-located
workload that does not have its own allowing policy.</li>
</ul>
<p><code>defaultDeny.order</code> (default <code>2000</code>) tunes the Calico <code>order</code> / ClusterNetworkPolicy
<code>priority</code> that keeps the deny-all subordinate to the allowlists. The <code>kubernetes</code>
and <code>cilium</code> engines have no precedence knob — deny and allow combine additively and
allow wins — so the value matters only for the <code>calico</code> and <code>clusterNetworkPolicy</code>
engines.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nt">networkPolicy</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">enabled</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">defaultDeny</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">enabled</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="w">   </span><span class="c"># only when JaaS owns this namespace</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">order</span><span class="p">:</span><span class="w"> </span><span class="m">2000</span><span class="w">
</span></span></span></code></pre></div><h2 id="choosing-a-policy-engine">Choosing a policy engine</h2>
<p><code>networkPolicy.engine</code> selects which policy dialect the chart renders. It is
explicit, not auto-detected: a chart that sniffed the running CNI would render
different objects on different clusters from identical values, which breaks GitOps
determinism. You name the engine, and the rendered manifest is the same everywhere.</p>
<table>
	<thead>
			<tr>
					<th><code>engine</code></th>
					<th>Renders</th>
					<th>API</th>
					<th>FQDN egress</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td><code>kubernetes</code> (default)</td>
					<td><code>NetworkPolicy</code></td>
					<td><code>networking.k8s.io/v1</code></td>
					<td>No</td>
			</tr>
			<tr>
					<td><code>cilium</code></td>
					<td><code>CiliumNetworkPolicy</code></td>
					<td><code>cilium.io/v2</code></td>
					<td>Yes — free <code>toFQDNs</code> egress</td>
			</tr>
			<tr>
					<td><code>calico</code></td>
					<td><code>NetworkPolicy</code></td>
					<td><code>projectcalico.org/v3</code></td>
					<td>No — OSS Calico has no FQDN egress; that is Calico Enterprise only</td>
			</tr>
			<tr>
					<td><code>clusterNetworkPolicy</code></td>
					<td><code>ClusterNetworkPolicy</code></td>
					<td><code>policy.networking.k8s.io/v1alpha2</code></td>
					<td>No</td>
			</tr>
	</tbody>
</table>
<p><code>clusterNetworkPolicy</code> renders the SIG-Network <code>ClusterNetworkPolicy</code> that
consolidates the deprecated <code>AdminNetworkPolicy</code> + <code>BaselineAdminNetworkPolicy</code>
APIs into one resource. It is alpha, cluster-scoped, and rendered in the <code>Baseline</code>
tier so a developer-authored <code>NetworkPolicy</code> still takes precedence over it.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nt">networkPolicy</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">enabled</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">engine</span><span class="p">:</span><span class="w"> </span><span class="l">cilium</span><span class="w">
</span></span></span></code></pre></div><p>The per-port <code>.from</code> knobs documented under <a href="#configuring-ingress">Configuring ingress</a>

apply to the <code>kubernetes</code> engine only. For the other engines the allowlists are
pod-scoped allow-all on the required ports, and you tighten them through that
engine&rsquo;s native passthrough lists — <code>networkPolicy.&lt;engine&gt;.ingress</code> and
<code>networkPolicy.&lt;engine&gt;.egress</code> — which are merged verbatim into the rendered
policy&rsquo;s <code>spec</code>. For example, adding identity-based ingress and a <code>toFQDNs</code> egress
under the Cilium engine:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nt">networkPolicy</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">enabled</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">engine</span><span class="p">:</span><span class="w"> </span><span class="l">cilium</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">cilium</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">ingress</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="nt">fromEndpoints</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span>- <span class="nt">matchLabels</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">              </span><span class="nt">app.kubernetes.io/name</span><span class="p">:</span><span class="w"> </span><span class="l">kustomize-controller</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">egress</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="nt">toFQDNs</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span>- <span class="nt">matchName</span><span class="p">:</span><span class="w"> </span><span class="l">bucket.example.com</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">toPorts</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span>- <span class="nt">ports</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">              </span>- <span class="nt">port</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;443&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">                </span><span class="nt">protocol</span><span class="p">:</span><span class="w"> </span><span class="l">TCP</span><span class="w">
</span></span></span></code></pre></div><h2 id="required-traffic">Required traffic</h2>
<p>The traffic JaaS needs depends on the mode it runs in. The renderer-mode rows apply
to every install; the operator-mode rows apply only when <code>operator.enabled</code> is
<code>true</code>.</p>
<h3 id="ingress">Ingress</h3>
<table>
	<thead>
			<tr>
					<th>Port</th>
					<th>Source</th>
					<th>Mode</th>
					<th>Selectable by label?</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td>Jsonnet HTTP (<code>ports.http</code>, <code>8080</code>)</td>
					<td>Callers of the <code>/jsonnet</code> endpoint, or an Ingress controller fronting the Service</td>
					<td>always</td>
					<td>Yes — or open when an Ingress fronts it</td>
			</tr>
			<tr>
					<td>Management probes (<code>ports.management</code>, <code>8081</code>)</td>
					<td>The kubelet, dialing the readiness, liveness, and startup probes from the node IP</td>
					<td>always</td>
					<td>No — the node IP is not a pod, so it cannot be a <code>podSelector</code></td>
			</tr>
			<tr>
					<td>Storage HTTP (<code>ports.storage</code>, <code>8082</code>)</td>
					<td>The Flux consumers that dereference <code>ExternalArtifact</code> tarballs — kustomize-controller, helm-controller, and custom consumers such as stageset-controller</td>
					<td>operator</td>
					<td>Yes — by consumer namespace</td>
			</tr>
			<tr>
					<td>Webhook (<code>ports.webhook</code>, <code>9443</code>)</td>
					<td>The kube-apiserver, dialing the validating admission webhook</td>
					<td>operator + webhook</td>
					<td>No — the apiserver is not a pod</td>
			</tr>
			<tr>
					<td>Metrics (<code>ports.metrics</code>, <code>8083</code>)</td>
					<td>Prometheus scraping <code>/metrics</code></td>
					<td>operator + metrics</td>
					<td>Yes — by the scraper&rsquo;s pod or namespace</td>
			</tr>
	</tbody>
</table>
<p>The Jsonnet HTTP and management ports always get an ingress rule. The storage,
webhook, and metrics ports each get their own rule when their mode is active —
storage when <code>operator.enabled</code>, webhook when the operator&rsquo;s webhook is enabled,
and metrics when the operator&rsquo;s metrics endpoint is enabled.</p>
<p>The kubelet and the apiserver source traffic from addresses that are not pods, so
their rules cannot be narrowed with a <code>podSelector</code> or <code>namespaceSelector</code>. Leaving
the management and webhook <code>from</code> lists empty keeps those ports reachable, which is
what lets probes succeed and the apiserver reach the webhook. Authenticity on the
webhook port is enforced by TLS and the CA bundle on the
<code>ValidatingWebhookConfiguration</code>, not by the network layer — see the
<a href="/usage/admission-webhook/">admission webhook page</a>
.</p>
<h3 id="egress">Egress</h3>
<p>Egress only matters when you opt into it (<code>networkPolicy.egress.enabled</code>). The JaaS
operator needs the following outbound flows; in renderer mode it needs only DNS, if
that.</p>
<table>
	<thead>
			<tr>
					<th>Destination</th>
					<th>Purpose</th>
					<th>Mode</th>
					<th>Selectable by label?</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td>Cluster DNS</td>
					<td>Name resolution — without it every other egress flow fails</td>
					<td>always</td>
					<td>Yes — by the DNS namespace</td>
			</tr>
			<tr>
					<td>kube-apiserver</td>
					<td>TokenRequest minting, CR reads, <code>ExternalArtifact</code> writes, leader election, and webhook caBundle patching</td>
					<td>operator</td>
					<td>No — <code>ipBlock</code> CIDR only</td>
			</tr>
			<tr>
					<td>source-controller</td>
					<td>Fetching upstream artifacts for snippets that use a <code>sourceRef</code></td>
					<td>operator</td>
					<td>Yes — the <code>flux-system</code> namespace</td>
			</tr>
			<tr>
					<td>S3 endpoint</td>
					<td>Reading and writing tarballs when <code>storage.backend</code> is <code>s3</code></td>
					<td>operator + S3</td>
					<td>Depends — in-cluster MinIO is label-selectable; an external bucket is <code>ipBlock</code> only</td>
			</tr>
			<tr>
					<td>OTLP collector</td>
					<td>Shipping traces when <code>operator.tracing.endpoint</code> is set</td>
					<td>operator + tracing</td>
					<td>Depends — in-cluster collector is label-selectable; an external one is <code>ipBlock</code> only</td>
			</tr>
	</tbody>
</table>
<p>The kube-apiserver is never label-selectable, so its egress rule must be an
<code>ipBlock</code> CIDR. The same applies to any S3 bucket or OTLP collector that lives
outside the cluster.</p>
<h2 id="configuring-ingress">Configuring ingress</h2>
<p>Under the <code>kubernetes</code> engine, enable the policy and tighten each port through its
<code>from</code> knob. An empty <code>from</code> list leaves that port open; a non-empty list restricts
it to the listed peers.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nt">networkPolicy</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">enabled</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="c"># Open by default — typical when an Ingress fronts the Service. Set a</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="c"># from-list to restrict callers of the /jsonnet endpoint.</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">http</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">from</span><span class="p">:</span><span class="w"> </span><span class="p">[]</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="c"># Leave empty — the kubelet probes source from the node IP.</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">management</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">from</span><span class="p">:</span><span class="w"> </span><span class="p">[]</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="c"># Leave empty — the kube-apiserver cannot be expressed as a podSelector.</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">webhook</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">from</span><span class="p">:</span><span class="w"> </span><span class="p">[]</span><span class="w">
</span></span></span></code></pre></div><p>The storage port defaults to allowing any pod in <code>flux-system</code>, the namespace where
the stock Flux consumers run. Add an entry per extra consumer namespace — for
example a stageset-controller running in <code>stageset-system</code>:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nt">networkPolicy</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">enabled</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">storage</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">from</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="nt">namespaceSelector</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">matchLabels</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span><span class="nt">kubernetes.io/metadata.name</span><span class="p">:</span><span class="w"> </span><span class="l">flux-system</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="nt">namespaceSelector</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">matchLabels</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span><span class="nt">kubernetes.io/metadata.name</span><span class="p">:</span><span class="w"> </span><span class="l">stageset-system</span><span class="w">
</span></span></span></code></pre></div><p>The metrics port has its own ingress rule, rendered when both <code>operator.enabled</code> and
<code>operator.metrics.enabled</code> are set. Scope it to your monitoring namespace through
<code>networkPolicy.metrics.from</code>:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nt">networkPolicy</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">enabled</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">metrics</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">from</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="nt">namespaceSelector</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">matchLabels</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span><span class="nt">kubernetes.io/metadata.name</span><span class="p">:</span><span class="w"> </span><span class="l">monitoring</span><span class="w">
</span></span></span></code></pre></div><p>Anything the per-port knobs do not cover goes into <code>additionalIngress</code>, which is
merged verbatim into the policy:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nt">networkPolicy</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">enabled</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">additionalIngress</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span>- <span class="nt">ports</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span>- <span class="nt">protocol</span><span class="p">:</span><span class="w"> </span><span class="l">TCP</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">port</span><span class="p">:</span><span class="w"> </span><span class="m">8080</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">from</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span>- <span class="nt">namespaceSelector</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span><span class="nt">matchLabels</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">              </span><span class="nt">kubernetes.io/metadata.name</span><span class="p">:</span><span class="w"> </span><span class="l">ingress-nginx</span><span class="w">
</span></span></span></code></pre></div><h2 id="opt-in-egress">Opt-in egress</h2>
<p>Egress is off by default, and deliberately so. Adding the <code>Egress</code> policy type
flips the JaaS pod to default-deny for outbound traffic — everything not explicitly
allowed is dropped. Getting the allow-list complete is the cluster operator&rsquo;s risk,
because the two destinations the operator needs most — the kube-apiserver and any
external S3 or OTLP endpoint — are not label-selectable and so depend on <code>ipBlock</code>
CIDRs that vary per cluster. An incomplete list does not fail loudly; it silently
cuts the operator off.</p>
<blockquote>
<p><strong>Warning:</strong> Enabling egress <strong>without</strong> an <code>ipBlock</code> for the kube-apiserver cuts
the operator off from the control plane. It can no longer mint tokens, read CRs,
publish <code>ExternalArtifact</code> resources, hold the leader-election lease, or patch the
webhook caBundle. Always include the apiserver CIDR before turning egress on.</p>
</blockquote>
<p>Find the apiserver&rsquo;s address with:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">kubectl --namespace default get endpoints kubernetes -o <span class="nv">jsonpath</span><span class="o">=</span><span class="s1">&#39;{.subsets[*].addresses[*].ip}&#39;</span>
</span></span></code></pre></div><p>Use that IP as a <code>/32</code> (or your control plane&rsquo;s CIDR for an HA apiserver). A
complete operator egress block — DNS, the apiserver, source-controller, S3, and an
OTLP collector — looks like this:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nt">networkPolicy</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">enabled</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">egress</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">enabled</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="c"># DNS to the cluster DNS namespace. Without this, every flow below</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="c"># fails name resolution.</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">dns</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">dnsNamespace</span><span class="p">:</span><span class="w"> </span><span class="l">kube-system</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">to</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="c"># kube-apiserver — not label-selectable, so an ipBlock CIDR.</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="c"># Replace with the IP(s) from the command above.</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="nt">to</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span>- <span class="nt">ipBlock</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">              </span><span class="nt">cidr</span><span class="p">:</span><span class="w"> </span><span class="m">10.0.0.1</span><span class="l">/32</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">ports</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span>- <span class="nt">protocol</span><span class="p">:</span><span class="w"> </span><span class="l">TCP</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span><span class="nt">port</span><span class="p">:</span><span class="w"> </span><span class="m">443</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="c"># source-controller — fetching upstream artifacts for sourceRef snippets.</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="nt">to</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span>- <span class="nt">namespaceSelector</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">              </span><span class="nt">matchLabels</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">                </span><span class="nt">kubernetes.io/metadata.name</span><span class="p">:</span><span class="w"> </span><span class="l">flux-system</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="c"># S3 bucket — an ipBlock CIDR for an external endpoint. For in-cluster</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="c"># MinIO, use a namespaceSelector instead.</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="nt">to</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span>- <span class="nt">ipBlock</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">              </span><span class="nt">cidr</span><span class="p">:</span><span class="w"> </span><span class="m">203.0.113.0</span><span class="l">/24</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">ports</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span>- <span class="nt">protocol</span><span class="p">:</span><span class="w"> </span><span class="l">TCP</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span><span class="nt">port</span><span class="p">:</span><span class="w"> </span><span class="m">443</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="c"># OTLP collector — an ipBlock CIDR for an external endpoint. For an</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="c"># in-cluster collector, use a namespaceSelector instead.</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="nt">to</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span>- <span class="nt">ipBlock</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">              </span><span class="nt">cidr</span><span class="p">:</span><span class="w"> </span><span class="m">198.51.100.10</span><span class="l">/32</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">ports</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span>- <span class="nt">protocol</span><span class="p">:</span><span class="w"> </span><span class="l">TCP</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span><span class="nt">port</span><span class="p">:</span><span class="w"> </span><span class="m">4317</span><span class="w">
</span></span></span></code></pre></div><p>Trim this to what your install actually uses: drop the S3 block on the local storage
backend, and drop the OTLP block when <a href="/usage/observability/">tracing</a>
 is off. The
apiserver and DNS rules are non-negotiable for the operator. Storage destinations
are covered on the <a href="/usage/storage-and-ha/">storage and high availability page</a>
, and
tenancy on the <a href="/usage/tenancy-and-rbac/">tenancy and RBAC page</a>
.</p>
<p>For the full set of chart values, see the
<a href="https://github.com/metio/helm-charts/tree/main/charts/jaas">chart README</a>
.</p>
]]></content><category scheme="https://jaas.projects.metio.wtf/tags/network-policy" term="network-policy" label="network-policy"/><category scheme="https://jaas.projects.metio.wtf/tags/security" term="security" label="security"/><category scheme="https://jaas.projects.metio.wtf/tags/networking" term="networking" label="networking"/></entry><entry><title type="html">Observability</title><link href="https://jaas.projects.metio.wtf/usage/observability/?utm_source=atom_feed" rel="alternate" type="text/html"/><link href="https://jaas.projects.metio.wtf/usage/alerting/?utm_source=atom_feed" rel="related" type="text/html" title="Alerting"/><link href="https://jaas.projects.metio.wtf/usage/logging/?utm_source=atom_feed" rel="related" type="text/html" title="Logging"/><link href="https://jaas.projects.metio.wtf/usage/metrics/?utm_source=atom_feed" rel="related" type="text/html" title="Metrics"/><link href="https://jaas.projects.metio.wtf/usage/tracing/?utm_source=atom_feed" rel="related" type="text/html" title="Tracing"/><link href="https://jaas.projects.metio.wtf/usage/admission-webhook/?utm_source=atom_feed" rel="related" type="text/html" title="Admission webhook"/><id>https://jaas.projects.metio.wtf/usage/observability/</id><published>0001-01-01T00:00:00+00:00</published><updated>2026-06-17T22:19:37+02:00</updated><content type="html"><![CDATA[<blockquote>How to watch JaaS in production — structured logs, OTLP traces, Prometheus metrics, and the shipped alert catalog with Kubernetes Events and Flux notification routing.</blockquote><p>JaaS gives you four ways to see what it is doing in a cluster. Structured logs
tell you what happened on a single request or reconcile; traces follow one
operation across its spans; metrics aggregate behaviour into time series for
dashboards and alerts; and alerts plus Kubernetes Events turn a sustained
problem into a page or a notification.</p>
<p>Each pillar has its own page covering both the binary&rsquo;s flags and the Helm chart
keys that drive them:</p>
<ul>
<li><a href="/usage/logging/">Logging</a>
 — the <code>log/slog</code> logger, <code>--log-level</code> and
<code>--log-format</code>, and reading JSON logs with <code>kubectl logs</code> and <code>jq</code>.</li>
<li><a href="/usage/tracing/">Tracing</a>
 — OTLP gRPC export to an OpenTelemetry collector,
sampling, and viewing spans.</li>
<li><a href="/usage/metrics/">Metrics</a>
 — the Prometheus endpoint, the custom <code>jaas_*</code>
metric family, scraping with a <code>ServiceMonitor</code>, and querying with PromQL.</li>
<li><a href="/usage/alerting/">Alerting</a>
 — the opt-in <code>PrometheusRule</code> alert catalog with
its runbook links, plus Kubernetes Events routed through Flux&rsquo;s
notification-controller.</li>
</ul>
<p>Logging applies to every mode JaaS runs in. Tracing, metrics, and alerting are
operator-mode concerns and take effect once <code>--enable-flux-integration</code> is set
(<code>operator.enabled</code> in the chart).</p>
]]></content><category scheme="https://jaas.projects.metio.wtf/tags/operator" term="operator" label="operator"/><category scheme="https://jaas.projects.metio.wtf/tags/logging" term="logging" label="logging"/><category scheme="https://jaas.projects.metio.wtf/tags/tracing" term="tracing" label="tracing"/><category scheme="https://jaas.projects.metio.wtf/tags/metrics" term="metrics" label="metrics"/><category scheme="https://jaas.projects.metio.wtf/tags/alerts" term="alerts" label="alerts"/></entry><entry><title type="html">Operations</title><link href="https://jaas.projects.metio.wtf/installation/operations/?utm_source=atom_feed" rel="alternate" type="text/html"/><link href="https://jaas.projects.metio.wtf/installation/configuration/?utm_source=atom_feed" rel="related" type="text/html" title="Configuration reference"/><link href="https://jaas.projects.metio.wtf/installation/helm-values/?utm_source=atom_feed" rel="related" type="text/html" title="Helm chart values"/><link href="https://jaas.projects.metio.wtf/installation/kubernetes/?utm_source=atom_feed" rel="related" type="text/html" title="Kubernetes"/><link href="https://jaas.projects.metio.wtf/installation/production/?utm_source=atom_feed" rel="related" type="text/html" title="Production"/><id>https://jaas.projects.metio.wtf/installation/operations/</id><published>0001-01-01T00:00:00+00:00</published><updated>2026-06-17T16:04:27+02:00</updated><content type="html"><![CDATA[<blockquote>Day-two operational tasks for a running JaaS install — graceful shutdown, leader election, storage GC, and upgrades.</blockquote><p>Day-two operations for a running JaaS install. Initial install and hardening
decisions are in <a href="/installation/kubernetes/">Kubernetes</a>
 and
<a href="/installation/production/">Production</a>
.</p>
<h2 id="graceful-shutdown-and-drain">Graceful shutdown and drain</h2>
<p>When Kubernetes sends <code>SIGTERM</code>, JaaS executes a two-phase shutdown to avoid
dropping in-flight requests:</p>
<ol>
<li>The readiness probe flips to <code>false</code> (<code>503</code> on <code>/ready</code>). Kubernetes
endpoint controllers begin deregistering the pod from Services.</li>
<li>JaaS waits for <code>--shutdown-delay</code> (default <code>5s</code>) before closing its
listeners. This window lets the endpoint propagation complete so no new
traffic arrives after the server closes.</li>
<li>After the delay, the servers shut down gracefully with a 30-second
<code>context.WithTimeout</code>. The operator goroutine is also cancelled and awaited
within the same 30-second window.</li>
</ol>
<p>The distroless runtime image has no <code>sleep</code> binary, so the drain delay is
implemented in the binary rather than via a <code>preStop</code> hook. A second
<code>SIGTERM</code> (or <code>SIGINT</code>) during the drain cuts the wait short.</p>
<p>To disable the drain (zero delay):</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">--shutdown-delay <span class="m">0</span>
</span></span></code></pre></div><p>The chart value is <code>arguments.shutdownDelay</code>.</p>
<h2 id="leader-election-during-rolling-updates">Leader election during rolling updates</h2>
<p>In operator mode, leader election is on by default (<code>--leader-election</code>,
<code>operator.leaderElection.enabled: true</code>). The chart sets
<code>LeaderElectionReleaseOnCancel: true</code>, so when the old pod receives <code>SIGTERM</code> it
releases the lease immediately instead of waiting out the 15-second
<code>LeaseDuration</code>. The new pod picks up the lease within milliseconds.</p>
<p>Snippets that were <code>Ready=True</code> before the restart stay in that condition via
cached state. A new pod that takes over as leader reconciles them on the next
watch event. If snippets remain degraded for more than a few seconds after a
restart, check the <a href="/runbooks/operator-watch-silent/">operator-watch-silent</a>

runbook — it diagnoses the case where the operator&rsquo;s own ClusterRole is missing
a verb so controller-runtime&rsquo;s informer silently fails to start.</p>
<p>To force-restart the operator (e.g. after an upgrade):</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">kubectl rollout restart deployment/jaas --namespace jaas-system
</span></span></code></pre></div><h2 id="artifact-retention-and-storage-gc">Artifact retention and storage GC</h2>
<p>Three independent mechanisms govern how long artifacts stay on disk (or in S3).
Full storage backend configuration is in <a href="/usage/storage-and-ha/">Storage and HA</a>
.</p>
<h3 id="gc-grace-window---artifact-gc-grace-default-5m">GC grace window (<code>--artifact-gc-grace</code>, default <code>5m</code>)</h3>
<p>When a snippet is re-rendered, the superseded revision drops out of the keep-set
but remains fetchable for <code>--artifact-gc-grace</code> after supersession. This closes
the pin→fetch race in which a Flux consumer reads <code>status.artifact.url</code> a moment
before the operator GC-prunes the old tarball. The window survives operator
restarts — supersession time is derived from on-disk storage metadata, not from
in-memory state.</p>
<p>Set <code>0</code> to restore eager pruning (one revision at a time, matching stock Flux
source-controller semantics). Tune lower when storage capacity is tight and all
consumers are in-cluster.</p>
<h3 id="history-retention-spechistory-default-1-max-50">History retention (<code>spec.history</code>, default <code>1</code>, max <code>50</code>)</h3>
<p>Per-snippet deliberate retention for rollback and blue-green flows. A downstream
consumer can pin to a specific <code>sha256</code> digest indefinitely as long as that
revision is within the history keep-set. This is separate from the GC grace
window — it is explicit operator intent, not a race-protection mechanism.</p>
<h3 id="orphaned-tmp-sweep---storage-sweep-interval---storage-sweep-max-tmp-age">Orphaned <code>.tmp</code> sweep (<code>--storage-sweep-interval</code>, <code>--storage-sweep-max-tmp-age</code>)</h3>
<p>A <code>Put</code> that dies between writing the tempfile and the atomic rename leaves a
<code>&lt;rev&gt;.tar.gz.tmp</code> residue. The sweep goroutine runs on a ticker (default every
<code>10m</code>) and removes <code>.tmp</code> files older than <code>--storage-sweep-max-tmp-age</code>
(default <code>30m</code>). The age floor ensures live writers are never raced.</p>
<p>Set <code>--storage-sweep-interval 0</code> to disable the sweep entirely. The
<code>jaas_storage_sweep_failures_total</code> Prometheus counter signals failing sweep
passes.</p>
<h2 id="finalizer-teardown-and-the-withdrawforced-safety-valve">Finalizer teardown and the WithdrawForced safety valve</h2>
<p>Every <code>JsonnetSnippet</code> holds a finalizer (<code>jaas.metio.wtf/finalizer</code>) that
blocks Kubernetes garbage collection until the operator successfully calls
<code>Publisher.Withdraw</code> to remove the artifact from storage. If the backend is
permanently unavailable (S3 down, RBAC revoked, bucket deleted), the finalizer
would otherwise hold the snippet — and by extension its namespace — in
<code>Terminating</code> forever.</p>
<p><code>--max-withdraw-wait</code> (default <code>1h</code>) bounds how long the finalizer can hold.
Once the deadline passes, the operator:</p>
<ol>
<li>Emits a <code>Warning WithdrawForced</code> Kubernetes Event on the snippet.</li>
<li>Drops the finalizer so the snippet can be garbage-collected.</li>
</ol>
<p>The trade-off is a possible orphan tarball in storage. Recover it using the
<a href="/runbooks/storage-recovery/">storage-recovery</a>
 runbook.</p>
<p>Adjust the bound with the chart value <code>operator.maxWithdrawWait</code>. Lower it in
environments where namespace teardown latency is critical; raise it (or remove
the concern by fixing the backend) in environments where artifact-safety is
paramount.</p>
<h2 id="upgrades">Upgrades</h2>
<p>Calendar-based releases ship every Monday. The chart version and the binary
version advance together.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">helm upgrade --install jaas oci://ghcr.io/metio/helm-charts/jaas <span class="se">\
</span></span></span><span class="line"><span class="cl">  --namespace jaas-system <span class="se">\
</span></span></span><span class="line"><span class="cl">  --values my-values.yaml <span class="se">\
</span></span></span><span class="line"><span class="cl">  --wait --timeout 5m
</span></span></code></pre></div><p>The chart ships CRDs under <code>templates/</code> so <code>helm upgrade --install</code> applies schema
changes automatically.</p>
<p><strong>Before each upgrade</strong>, read
<a href="https://github.com/metio/jaas/blob/main/MIGRATIONS.md">MIGRATIONS.md</a>
:</p>
<ul>
<li>Releases that change <code>spec.selector.matchLabels</code> on the Deployment require a
manual <code>kubectl delete deployment/jaas</code> first — that field is immutable and
<code>helm upgrade --install</code> will fail otherwise.</li>
<li>The pre-delete cleanup Job (<code>operator.cleanupOnDelete.enabled: true</code>, the
default) runs on <code>helm uninstall</code> and drops every snippet&rsquo;s finalizer so
<code>ExternalArtifact</code> resources are unwound before the operator pod is removed.
If the cleanup Job hangs, check <code>operator.cleanupOnDelete.kubectlTimeout</code>
(default <code>2m</code>) and the backend health.</li>
</ul>
<h2 id="monitoring-operational-health">Monitoring operational health</h2>
<p>Key signals to watch:</p>
<ul>
<li><code>jaas_storage_sweep_failures_total</code> — non-zero means the sweep goroutine is
erroring; investigate storage backend health.</li>
<li><code>jaas_snippet_reconcile_total{status!=&quot;Synced&quot;}</code> — elevated rate means
snippets are failing to render; cross-reference with the <code>reason</code> label and
the relevant runbook.</li>
<li><code>JaaSControllerWorkqueueDepthHigh</code> PrometheusRule alert — workqueue is backing
up; the operator cannot keep up with the reconcile rate.</li>
<li><code>/ready</code> probe on the management port (default <code>8081</code>) — <code>503</code> after startup
means the manager has not yet been elected or its cache has not synced.</li>
</ul>
<p>All metrics are documented in <a href="/usage/observability/">Observability</a>
. All shipped
alerts link to <a href="/runbooks/">Runbooks</a>
.</p>
<h2 id="next-steps">Next steps</h2>
<ul>
<li><a href="/installation/configuration/">Configuration reference</a>
 — the full flag
list with defaults and chart value equivalents.</li>
<li><a href="/runbooks/">Runbooks</a>
 — incident response procedures keyed to each
<code>Ready</code> condition <code>Reason</code>.</li>
</ul>
]]></content><category scheme="https://jaas.projects.metio.wtf/tags/installation" term="installation" label="installation"/><category scheme="https://jaas.projects.metio.wtf/tags/operations" term="operations" label="operations"/><category scheme="https://jaas.projects.metio.wtf/tags/maintenance" term="maintenance" label="maintenance"/><category scheme="https://jaas.projects.metio.wtf/tags/upgrades" term="upgrades" label="upgrades"/></entry><entry><title type="html">Operator mode</title><link href="https://jaas.projects.metio.wtf/usage/operator-mode/?utm_source=atom_feed" rel="alternate" type="text/html"/><link href="https://jaas.projects.metio.wtf/usage/creating-sources/?utm_source=atom_feed" rel="related" type="text/html" title="Creating source artifacts"/><link href="https://jaas.projects.metio.wtf/api/externalartifact/?utm_source=atom_feed" rel="related" type="text/html" title="ExternalArtifact output contract"/><link href="https://jaas.projects.metio.wtf/api/jsonnetsnippet/?utm_source=atom_feed" rel="related" type="text/html" title="JsonnetSnippet"/><link href="https://jaas.projects.metio.wtf/usage/admission-webhook/?utm_source=atom_feed" rel="related" type="text/html" title="Admission webhook"/><link href="https://jaas.projects.metio.wtf/usage/alerting/?utm_source=atom_feed" rel="related" type="text/html" title="Alerting"/><id>https://jaas.projects.metio.wtf/usage/operator-mode/</id><published>0001-01-01T00:00:00+00:00</published><updated>2026-06-16T20:48:39+02:00</updated><content type="html"><![CDATA[<blockquote>Boot JaaS as a Kubernetes operator that evaluates JsonnetSnippet CRs and publishes the results as Flux ExternalArtifacts.</blockquote><p>JaaS runs as a Kubernetes operator alongside its HTTP renderer. In this mode it
watches custom resources, evaluates the Jsonnet they describe, and publishes the
result as a Flux <a href="https://fluxcd.io/"><code>ExternalArtifact</code></a>
 that downstream
controllers consume. The HTTP renderer keeps running; the operator is an
additional set of goroutines, not a separate binary.</p>
<h2 id="enabling-the-operator">Enabling the operator</h2>
<p>Set <code>--enable-flux-integration</code> on the binary:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">jaas --enable-flux-integration <span class="se">\
</span></span></span><span class="line"><span class="cl">  --storage-path<span class="o">=</span>/var/lib/jaas/artifacts <span class="se">\
</span></span></span><span class="line"><span class="cl">  --storage-base-url<span class="o">=</span>http://jaas-storage.jaas.svc:8082
</span></span></code></pre></div><p><code>--storage-path</code> and <code>--storage-base-url</code> are required in operator mode — they
tell the operator where to write artifact tarballs and the public URL prefix
downstream consumers fetch them from.</p>
<p>With the <a href="/installation/">Helm chart</a>
 set <code>operator.enabled: true</code>:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nt">operator</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">enabled</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="w">
</span></span></span></code></pre></div><p>The chart wires the storage paths, leader election, RBAC, and the metrics
Service for you.</p>
<h2 id="the-two-custom-resources">The two custom resources</h2>
<p>The operator watches two CRDs in the <code>jaas.metio.wtf/v1</code> API group. Both are
namespaced.</p>
<table>
	<thead>
			<tr>
					<th>Kind</th>
					<th>Scope</th>
					<th>Purpose</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td><code>JsonnetSnippet</code></td>
					<td>Namespaced</td>
					<td>A Jsonnet snippet to evaluate and publish as an <code>ExternalArtifact</code>.</td>
			</tr>
			<tr>
					<td><code>JsonnetLibrary</code></td>
					<td>Namespaced</td>
					<td>Reusable <code>.libsonnet</code> files that snippets in the same namespace import.</td>
			</tr>
	</tbody>
</table>
<p>A <code>JsonnetSnippet</code> is the published unit. A <code>JsonnetLibrary</code> carries no artifact
of its own — it exists to be imported by snippets. The full field reference for
each lives at <a href="/api/jsonnetsnippet/">/api/jsonnetsnippet/</a>
; the library CRD is
covered in <a href="/usage/jsonnet-libraries/">Jsonnet libraries</a>
.</p>
<h2 id="what-the-operator-produces">What the operator produces</h2>
<p>Each reconcile of a <code>JsonnetSnippet</code> evaluates the snippet and writes the result
into a tar.gz, then upserts a Flux <code>ExternalArtifact</code> CR whose
<code>status.artifact.url</code> points at the operator&rsquo;s storage HTTP server. In the
default <code>rendered</code> output mode the archive holds a single <code>rendered.json</code> — the
evaluated JSON. The published artifact&rsquo;s URL is also mirrored onto the snippet&rsquo;s
own <code>status.artifactURL</code>, so <code>kubectl describe jsonnetsnippet</code> answers &ldquo;where is
my rendered output?&rdquo; without a second lookup.</p>
<p>Any controller that understands Flux&rsquo;s <code>ExternalArtifact</code> reads the result by
pointing a <code>sourceRef</code> at it:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nt">apiVersion</span><span class="p">:</span><span class="w"> </span><span class="l">kustomize.toolkit.fluxcd.io/v1</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">kind</span><span class="p">:</span><span class="w"> </span><span class="l">Kustomization</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">metadata</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">consume-rendered</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">namespace</span><span class="p">:</span><span class="w"> </span><span class="l">default</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">spec</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">sourceRef</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">kind</span><span class="p">:</span><span class="w"> </span><span class="l">ExternalArtifact</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">hello-world</span><span class="w">
</span></span></span></code></pre></div><p>Real consumers of the published artifact include:</p>
<ul>
<li><a href="https://grafana.github.io/grafana-operator/">grafana-operator</a>
 — renders
Grafana dashboards from the evaluated JSON.</li>
<li><a href="https://stageset.projects.metio.wtf/">stageset-controller</a>
 — drives a staged
rollout of the rendered manifests.</li>
<li>Flux&rsquo;s own <code>kustomize-controller</code> and <code>helm-controller</code>, which apply the
rendered output as part of a GitOps pipeline.</li>
</ul>
<h2 id="a-minimal-snippet">A minimal snippet</h2>
<p>The simplest <code>JsonnetSnippet</code> carries its Jsonnet inline in <code>spec.files</code> and
seeds two external variables:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nt">apiVersion</span><span class="p">:</span><span class="w"> </span><span class="l">v1</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">kind</span><span class="p">:</span><span class="w"> </span><span class="l">ServiceAccount</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">metadata</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">hello-world-tenant</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">namespace</span><span class="p">:</span><span class="w"> </span><span class="l">default</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nn">---</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">apiVersion</span><span class="p">:</span><span class="w"> </span><span class="l">jaas.metio.wtf/v1</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">kind</span><span class="p">:</span><span class="w"> </span><span class="l">JsonnetSnippet</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">metadata</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">hello-world</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">namespace</span><span class="p">:</span><span class="w"> </span><span class="l">default</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">spec</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">serviceAccountName</span><span class="p">:</span><span class="w"> </span><span class="l">hello-world-tenant</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">entryFile</span><span class="p">:</span><span class="w"> </span><span class="l">main.jsonnet</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">files</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">main.jsonnet</span><span class="p">:</span><span class="w"> </span><span class="p">|</span><span class="sd">
</span></span></span><span class="line"><span class="cl"><span class="sd">      {
</span></span></span><span class="line"><span class="cl"><span class="sd">        greeting: &#39;hello&#39;,
</span></span></span><span class="line"><span class="cl"><span class="sd">        recipient: std.extVar(&#39;audience&#39;),
</span></span></span><span class="line"><span class="cl"><span class="sd">        timestamp: std.extVar(&#39;now&#39;),
</span></span></span><span class="line"><span class="cl"><span class="sd">      }</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">externalVariables</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">audience</span><span class="p">:</span><span class="w"> </span><span class="l">world</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">now</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;2026-06-09T12:00:00Z&#34;</span><span class="w">
</span></span></span></code></pre></div><p><code>spec.serviceAccountName</code> names the ServiceAccount the operator impersonates for
every API call this snippet drives — the artifact write, source fetches, library
reads. That ServiceAccount&rsquo;s RBAC, not the operator&rsquo;s, governs what the snippet
can reach. See <a href="/usage/tenancy-and-rbac/">Tenancy and RBAC</a>
 for the verbs the
tenant ServiceAccount needs.</p>
<h2 id="lifecycle-knobs">Lifecycle knobs</h2>
<p>Two <code>spec</code> fields control when and whether the operator reconciles a snippet.
Both mirror Flux&rsquo;s source-controller conventions.</p>
<h3 id="specsuspend"><code>spec.suspend</code></h3>
<p>Set <code>spec.suspend: true</code> to pause reconciliation without deleting the snippet.
The operator skips the evaluation pipeline, leaves the existing
<code>ExternalArtifact</code> in place, and reports <code>Ready=False</code> with reason <code>Suspended</code>.
Setting it back to <code>false</code> resumes reconciliation. The published artifact stays
available the whole time, so downstream consumers keep reading the last rendered
output while the snippet is paused.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nt">spec</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">suspend</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="w">
</span></span></span></code></pre></div><h3 id="specinterval"><code>spec.interval</code></h3>
<p>Set <code>spec.interval</code> to re-render the snippet on a fixed cadence even when no
watch event fires:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nt">spec</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">interval</span><span class="p">:</span><span class="w"> </span><span class="l">10m</span><span class="w">
</span></span></span></code></pre></div><p>A <code>JsonnetSnippet</code> re-renders whenever its source, libraries, or referenced Flux
sources change. <code>spec.interval</code> adds a steady-state cadence on top of that, so
the snippet picks up state outside the watched graph — external-variable
environment drift on the operator pod, OCI library refreshes, and similar. The
interval is bounded at admission to between <code>30s</code> and <code>24h</code>. Failed reconciles
still use controller-runtime&rsquo;s exponential backoff; the interval governs only
the steady-state cadence.</p>
<h2 id="where-to-go-next">Where to go next</h2>
<ul>
<li><a href="/usage/snippet-sources/">Snippet sources</a>
 — inline files, a Flux <code>sourceRef</code>,
multi-snippet trees, and chaining one snippet&rsquo;s output into another.</li>
<li><a href="/usage/jsonnet-libraries/">Jsonnet libraries</a>
 — the <code>JsonnetLibrary</code> CRD,
OCI-mounted shared libraries, and how imports resolve.</li>
<li><a href="/usage/tenancy-and-rbac/">Tenancy and RBAC</a>
 — per-snippet impersonation and
the tenant ServiceAccount&rsquo;s permissions.</li>
<li><a href="/usage/storage-and-ha/">Storage and HA</a>
 — the local and S3 backends, leader
election, and revision retention.</li>
<li><a href="/api/jsonnetsnippet/">/api/jsonnetsnippet/</a>
 — the exhaustive field-by-field
reference.</li>
</ul>
]]></content><category scheme="https://jaas.projects.metio.wtf/tags/operator" term="operator" label="operator"/><category scheme="https://jaas.projects.metio.wtf/tags/flux" term="flux" label="flux"/></entry><entry><title type="html">Operator pod not ready</title><link href="https://jaas.projects.metio.wtf/runbooks/operator-pod-down/?utm_source=atom_feed" rel="alternate" type="text/html"/><link href="https://jaas.projects.metio.wtf/runbooks/crd-watch-engagement/?utm_source=atom_feed" rel="related" type="text/html" title="CRD watch engagement failing"/><link href="https://jaas.projects.metio.wtf/runbooks/invalidspec/?utm_source=atom_feed" rel="related" type="text/html" title="InvalidSpec"/><link href="https://jaas.projects.metio.wtf/runbooks/pending/?utm_source=atom_feed" rel="related" type="text/html" title="Pending"/><link href="https://jaas.projects.metio.wtf/runbooks/suspended/?utm_source=atom_feed" rel="related" type="text/html" title="Suspended"/><link href="https://jaas.projects.metio.wtf/runbooks/synced/?utm_source=atom_feed" rel="related" type="text/html" title="Synced"/><id>https://jaas.projects.metio.wtf/runbooks/operator-pod-down/</id><published>0001-01-01T00:00:00+00:00</published><updated>2026-06-17T10:56:50+02:00</updated><content type="html"><![CDATA[<blockquote>At least one jaas pod has been Ready=False for the configured alert window, so new snippets are not being reconciled</blockquote><p>Linked from the <code>JaaSOperatorPodDown</code> alert. Fires when at least one jaas pod has been <code>Ready=False</code> for the alert window (default 5m).</p>
<h2 id="symptom">Symptom</h2>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">ALERTS{alertname=&#34;JaaSOperatorPodDown&#34;, namespace=&#34;&lt;jaas-ns&gt;&#34;}
</span></span></code></pre></div><ul>
<li>New JsonnetSnippets stay <code>Ready=Unknown</code> indefinitely (no controller reconciling them).</li>
<li>Existing snippets keep serving stale <code>ExternalArtifact</code> content (the storage HTTP server may still respond; reconciliation is the part that stopped).</li>
</ul>
<h2 id="cause">Cause</h2>
<p>One of the chart&rsquo;s two probes is failing:</p>
<ul>
<li><strong>Liveness</strong> (<code>/live</code>) is unconditional 200 — a failure here means the pod&rsquo;s HTTP server itself isn&rsquo;t responding (deadlock, OOM, panic).</li>
<li><strong>Readiness</strong> (<code>/ready</code>) consults <code>HealthState</code>, which goes <code>false</code> during <code>drainBeforeShutdown</code> or before the listeners bind.</li>
<li><strong>Startup</strong> (<code>/start</code>) returns 503 until <code>MarkStarted()</code> is called — bind failures (port already in use, permission denied) keep the pod stuck here forever.</li>
</ul>
<p>Frequent causes:</p>
<ol>
<li><strong>Bind failure</strong> on one of the HTTP servers (jsonnet, management, storage, metrics, webhook). The pod logs a clear &ldquo;listen tcp: address already in use&rdquo; or similar at boot.</li>
<li><strong>OOMKilled</strong> — a pathological snippet allocated a huge object; the kubelet killed the pod. <code>kubectl describe pod</code> shows <code>Last State: Terminated, Reason: OOMKilled</code>.</li>
<li><strong>Image pull failure</strong> — registry rate limit, wrong tag, missing pull secret.</li>
<li><strong>TLS cert missing or unreadable</strong> when <code>operator.webhook.enabled=true</code> and the cert-manager Secret hasn&rsquo;t materialized.</li>
<li><strong>Lease contention</strong> that leaves no replica as leader (every replica reconnecting to renew, never holding the lease).</li>
</ol>
<h2 id="diagnosis">Diagnosis</h2>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl"><span class="c1"># Which probes are failing? Events tell you.</span>
</span></span><span class="line"><span class="cl">kubectl --namespace &lt;jaas-ns&gt; describe pod --selector app.kubernetes.io/name<span class="o">=</span>jaas
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Pod logs — the boot sequence prints every listener it binds.</span>
</span></span><span class="line"><span class="cl">kubectl --namespace &lt;jaas-ns&gt; logs --selector app.kubernetes.io/name<span class="o">=</span>jaas --tail<span class="o">=</span><span class="m">300</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Compare against the expected listener set.</span>
</span></span><span class="line"><span class="cl">kubectl --namespace &lt;jaas-ns&gt; get svc --selector app.kubernetes.io/name<span class="o">=</span>jaas
</span></span></code></pre></div><p>For OOM:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">kubectl --namespace &lt;jaas-ns&gt; top pod --selector app.kubernetes.io/name<span class="o">=</span>jaas
</span></span><span class="line"><span class="cl">kubectl --namespace &lt;jaas-ns&gt; get pod --selector app.kubernetes.io/name<span class="o">=</span>jaas --output yaml <span class="se">\
</span></span></span><span class="line"><span class="cl">  <span class="p">|</span> grep -A3 lastState
</span></span></code></pre></div><p>For lease problems (multi-replica only):</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">kubectl --namespace &lt;jaas-ns&gt; get lease &lt;release-name&gt;-operator --output yaml
</span></span></code></pre></div><p><code>holderIdentity</code> flipping every renewal interval is a sign of network flake or apiserver pressure — the replicas can&rsquo;t keep the lease stable.</p>
<h2 id="remediation">Remediation</h2>
<ul>
<li><strong>Bind failure.</strong> Free the colliding port (often <code>8080</code>, when the controller-runtime metrics endpoint defaults conflict with the jsonnet HTTP port — confirm <code>--metrics-bind-address</code> is <code>:8083</code>).</li>
<li><strong>OOMKilled.</strong> Raise <code>resources.memory</code>, then identify the runaway snippet (the bench in <code>internal/operator/bench_test.go</code> is a regression baseline; the runaway is usually obvious from <code>jaas_snippet_rendered_bytes</code>).</li>
<li><strong>Image pull.</strong> Standard k8s drill: check secrets, registry, tag.</li>
<li><strong>TLS cert.</strong> With <code>certMode=cert-manager</code>, confirm the Issuer / Certificate are ready. With <code>certMode=self-signed</code>, the operator regenerates on boot — a permission error on the cert-dir mount blocks it.</li>
<li><strong>Lease flap.</strong> Try <code>kubectl --namespace &lt;jaas-ns&gt; delete lease &lt;release-name&gt;-operator</code> to force a fresh election. If it keeps flapping, the cluster has bigger problems than JaaS.</li>
</ul>
<h2 id="prevention">Prevention</h2>
<ul>
<li>Pin <code>replicas.max: 1</code> and <code>LeaderElectionReleaseOnCancel: true</code> (chart defaults). Multi-replica is only worth it for storage-backed HA — single-replica is the simpler operational story.</li>
<li>Run the cleanup Job (<code>operator.cleanupOnDelete.enabled: true</code>, chart default) so a <code>helm uninstall</code> of a wedged operator unwinds the finalizers instead of leaving orphaned snippets.</li>
<li>Pair this alert with <code>JaaSControllerWorkqueueDepthHigh</code> (<a href="/runbooks/workqueue-saturation/">workqueue-saturation.md</a>
). A pod-down event almost always coincides with a saturated queue from snippets piling up.</li>
</ul>
]]></content><category scheme="https://jaas.projects.metio.wtf/tags/runbooks" term="runbooks" label="runbooks"/><category scheme="https://jaas.projects.metio.wtf/tags/troubleshooting" term="troubleshooting" label="troubleshooting"/><category scheme="https://jaas.projects.metio.wtf/tags/lifecycle" term="lifecycle" label="lifecycle"/></entry><entry><title type="html">Pending</title><link href="https://jaas.projects.metio.wtf/runbooks/pending/?utm_source=atom_feed" rel="alternate" type="text/html"/><link href="https://jaas.projects.metio.wtf/runbooks/crd-watch-engagement/?utm_source=atom_feed" rel="related" type="text/html" title="CRD watch engagement failing"/><link href="https://jaas.projects.metio.wtf/runbooks/invalidspec/?utm_source=atom_feed" rel="related" type="text/html" title="InvalidSpec"/><link href="https://jaas.projects.metio.wtf/runbooks/operator-pod-down/?utm_source=atom_feed" rel="related" type="text/html" title="Operator pod not ready"/><link href="https://jaas.projects.metio.wtf/runbooks/suspended/?utm_source=atom_feed" rel="related" type="text/html" title="Suspended"/><link href="https://jaas.projects.metio.wtf/runbooks/synced/?utm_source=atom_feed" rel="related" type="text/html" title="Synced"/><id>https://jaas.projects.metio.wtf/runbooks/pending/</id><published>0001-01-01T00:00:00+00:00</published><updated>2026-06-17T10:56:50+02:00</updated><content type="html"><![CDATA[<blockquote>The snippet has been observed by the operator but its first reconcile pass has not yet completed</blockquote><h2 id="symptom">Symptom</h2>
<p><code>kubectl get jsonnetsnippet</code> shows <code>READY=Unknown</code> (or <code>False</code> with <code>REASON=Pending</code>) immediately after the snippet was created or its spec was updated.</p>
<h2 id="cause">Cause</h2>
<p>The operator has observed the CR but hasn&rsquo;t completed its first reconcile pass yet. Transient by design.</p>
<h2 id="diagnosis">Diagnosis</h2>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">kubectl describe jsonnetsnippet &lt;name&gt;
</span></span></code></pre></div><p>If the timestamp on the <code>Pending</code> condition is older than ~30 seconds, the operator is either:</p>
<ul>
<li>not running (<code>kubectl --namespace &lt;jaas-namespace&gt; get pods</code>)</li>
<li>backed up on its work queue (check <code>kubectl logs deploy/jaas</code> and the <code>workqueue_depth</code> metric)</li>
<li>not the leader (multi-replica install, <code>kubectl --namespace &lt;jaas-namespace&gt; get lease</code> shows the holder)</li>
</ul>
<h2 id="remediation">Remediation</h2>
<p>If transient, wait. If persistent:</p>
<ul>
<li>restart the operator: <code>kubectl rollout restart deploy/jaas</code></li>
<li>inspect the operator&rsquo;s logs for errors</li>
</ul>
<p>If the snippet is stuck in <code>Pending</code> because the work queue is saturated, increase replicas (with leader election ON) or raise the rate-limiter budget (<code>--rerender-rate</code>, <code>--rerender-burst</code>).</p>
]]></content><category scheme="https://jaas.projects.metio.wtf/tags/runbooks" term="runbooks" label="runbooks"/><category scheme="https://jaas.projects.metio.wtf/tags/troubleshooting" term="troubleshooting" label="troubleshooting"/><category scheme="https://jaas.projects.metio.wtf/tags/lifecycle" term="lifecycle" label="lifecycle"/></entry><entry><title type="html">Production</title><link href="https://jaas.projects.metio.wtf/installation/production/?utm_source=atom_feed" rel="alternate" type="text/html"/><link href="https://jaas.projects.metio.wtf/usage/alerting/?utm_source=atom_feed" rel="related" type="text/html" title="Alerting"/><link href="https://jaas.projects.metio.wtf/installation/configuration/?utm_source=atom_feed" rel="related" type="text/html" title="Configuration reference"/><link href="https://jaas.projects.metio.wtf/usage/evaluation-and-security/?utm_source=atom_feed" rel="related" type="text/html" title="Evaluation and security"/><link href="https://jaas.projects.metio.wtf/installation/helm-values/?utm_source=atom_feed" rel="related" type="text/html" title="Helm chart values"/><link href="https://jaas.projects.metio.wtf/installation/kubernetes/?utm_source=atom_feed" rel="related" type="text/html" title="Kubernetes"/><id>https://jaas.projects.metio.wtf/installation/production/</id><published>0001-01-01T00:00:00+00:00</published><updated>2026-06-17T16:04:27+02:00</updated><content type="html"><![CDATA[<blockquote>A decision-oriented checklist for hardening a JaaS operator install before serving production traffic.</blockquote><p>The chart&rsquo;s defaults are safe for an initial install but not optimised for
sustained production workloads. Work through these decisions before exposing JaaS
to real traffic. Each links to the detailed guide.</p>
<h2 id="1-pick-a-storage-backend">1. Pick a storage backend</h2>
<p>The single largest decision. Artifacts must survive pod restarts and, for HA,
be readable by every replica simultaneously.</p>
<table>
	<thead>
			<tr>
					<th>Backend</th>
					<th>Persistence</th>
					<th>Multi-replica HA</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td><code>local</code> + emptyDir (chart default)</td>
					<td>No</td>
					<td>No</td>
			</tr>
			<tr>
					<td><code>local</code> + RWO PVC</td>
					<td>Yes</td>
					<td>No — single replica only</td>
			</tr>
			<tr>
					<td><code>local</code> + RWX PVC</td>
					<td>Yes</td>
					<td>Yes — requires RWX storage class</td>
			</tr>
			<tr>
					<td><code>s3</code></td>
					<td>Yes</td>
					<td>Yes — leader writes, all replicas read</td>
			</tr>
	</tbody>
</table>
<p>For cloud installs, <code>s3</code> (AWS S3, MinIO, Ceph RGW, GCS S3-compat API) is the
recommended backend. Pair it with leader election (on by default) so only the
lease-holder writes. For on-prem, a PVC with the access mode your storage class
supports is the practical path.</p>
<p>Full configuration options and artifact retention are covered in
<a href="/usage/storage-and-ha/">Storage and HA</a>
 — including the garbage-collection grace
period (<code>--artifact-gc-grace</code>) that keeps a just-superseded revision fetchable for
a short window, so a consumer that read <code>status.artifact</code> moments before pruning
doesn&rsquo;t 404 on the revision it pinned.</p>
<p>Minimal S3 values (IRSA on EKS):</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nt">operator</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">enabled</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">serviceAccount</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">annotations</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">eks.amazonaws.com/role-arn</span><span class="p">:</span><span class="w"> </span><span class="l">arn:aws:iam::ACCOUNT:role/jaas-operator</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">storage</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">backend</span><span class="p">:</span><span class="w"> </span><span class="l">s3</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">s3</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">endpoint</span><span class="p">:</span><span class="w"> </span><span class="l">s3.amazonaws.com</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">bucket</span><span class="p">:</span><span class="w"> </span><span class="l">my-jaas-artifacts</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">prefix</span><span class="p">:</span><span class="w"> </span><span class="l">prod</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">region</span><span class="p">:</span><span class="w"> </span><span class="l">eu-west-1</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">useSSL</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="c"># Leave accessKey/secretKey empty — IAM role via SA annotation.</span><span class="w">
</span></span></span></code></pre></div><h2 id="2-size-cpu-and-memory">2. Size CPU and memory</h2>
<p>The chart defaults (64 MiB memory, 32m CPU) are fine for a quickstart but will
OOM under sustained snippet rendering. Each in-flight evaluation is essentially
uncancellable mid-flight — go-jsonnet has no mid-evaluation cancellation — so
CPU and memory limits must accommodate the worst-case concurrent eval load.</p>
<p>Set <code>--max-artifact-bytes</code> to cap the rendered output size per snippet so a
runaway template can&rsquo;t allocate unbounded memory before the timeout fires.</p>
<p>See <a href="/usage/evaluation-and-security/">Evaluation and security</a>
 for the
concurrent-eval cap, timeout defaults, and how to tune them.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nt">resources</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">memory</span><span class="p">:</span><span class="w"> </span><span class="l">256Mi</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">cpu</span><span class="p">:</span><span class="w"> </span><span class="l">100m</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">operator</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">storage</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">maxArtifactBytes</span><span class="p">:</span><span class="w"> </span><span class="m">16777216</span><span class="w">  </span><span class="c"># 16 MiB; fails with ReasonArtifactTooLarge</span><span class="w">
</span></span></span></code></pre></div><h2 id="3-enable-observability">3. Enable observability</h2>
<p>The chart ships a metrics endpoint (on by default at port <code>8083</code>), an opt-in
<code>ServiceMonitor</code>, and an opt-in <code>PrometheusRule</code> with a starter alert set. Turn
them on and wire the Prometheus selector labels before deploying:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nt">operator</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">metrics</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">enabled</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">serviceMonitor</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">enabled</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">labels</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">release</span><span class="p">:</span><span class="w"> </span><span class="l">kube-prom         </span><span class="w"> </span><span class="c"># match your Prometheus&#39;s serviceMonitorSelector</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">prometheusRule</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">enabled</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">labels</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">release</span><span class="p">:</span><span class="w"> </span><span class="l">kube-prom         </span><span class="w"> </span><span class="c"># match your Prometheus&#39;s ruleSelector</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">extraAlertLabels</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">team</span><span class="p">:</span><span class="w"> </span><span class="l">platform             </span><span class="w"> </span><span class="c"># Alertmanager routing label</span><span class="w">
</span></span></span></code></pre></div><p><code>serviceMonitor.labels</code>, <code>prometheusRule.labels</code>, and
<code>prometheusRule.extraAlertLabels</code> are three distinct label knobs:
<code>serviceMonitor.labels</code> and <code>prometheusRule.labels</code> control which Prometheus
instance picks up each CRD object; <code>extraAlertLabels</code> adds routing labels to
individual alerts (for Alertmanager), not to the rule object.</p>
<p>The shipped alert set and all custom JaaS metrics are documented in
<a href="/usage/observability/">Observability</a>
.</p>
<h2 id="admission-webhook-tls">4. Enable the admission webhook</h2>
<p>The webhook rejects spec invariant violations — ext-var key collisions, library
alias shadowing, import cycles — at <code>kubectl apply</code> time instead of at
reconcile time. Pick a cert mode:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="c"># Option A: cert-manager (recommended when cert-manager is installed)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">operator</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">webhook</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">enabled</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">certMode</span><span class="p">:</span><span class="w"> </span><span class="l">cert-manager</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">certManager</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">enabled</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">issuerRef</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">kind</span><span class="p">:</span><span class="w"> </span><span class="l">ClusterIssuer</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">letsencrypt-prod</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="c"># Option B: self-signed (no cert-manager required)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">operator</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">webhook</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">enabled</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">certMode</span><span class="p">:</span><span class="w"> </span><span class="l">self-signed</span><span class="w">
</span></span></span></code></pre></div><p>The default <code>failurePolicy: Fail</code> blocks every <code>JsonnetSnippet</code> create/update
cluster-wide when the webhook is unavailable. During a rolling update the window
is typically under five seconds (leader election releases the lease on
SIGTERM). If your GitOps tooling cannot tolerate that, scope the webhook via
<code>operator.webhook.namespaceSelector</code> or <code>operator.webhook.objectSelector</code>, or
switch to <code>failurePolicy: Ignore</code> and rely on the reconciler-side fallback.</p>
<p>Full cert provisioning and failurePolicy trade-offs are covered in
<a href="/usage/admission-webhook/">Admission webhook</a>
.</p>
<h2 id="5-lock-down-tenant-rbac">5. Lock down tenant RBAC</h2>
<p>Every <code>JsonnetSnippet</code> runs impersonated as its <code>spec.serviceAccountName</code> (or
the <code>--default-service-account</code> fallback). The operator&rsquo;s own ServiceAccount
only needs <code>serviceaccounts/token: create</code> — every other API call (library
reads, source fetches, <code>ExternalArtifact</code> writes) is done under the tenant SA&rsquo;s
RBAC, so a compromised snippet can only reach what its SA is allowed to.</p>
<p>Minimum per-tenant <code>Role</code>:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nt">apiVersion</span><span class="p">:</span><span class="w"> </span><span class="l">rbac.authorization.k8s.io/v1</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">kind</span><span class="p">:</span><span class="w"> </span><span class="l">Role</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">metadata</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">namespace</span><span class="p">:</span><span class="w"> </span><span class="l">&lt;tenant-namespace&gt;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">jaas-tenant</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">rules</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span>- <span class="nt">apiGroups</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="l">source.toolkit.fluxcd.io]</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">resources</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="l">externalartifacts]</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">verbs</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="l">get, create, update, patch]</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span>- <span class="nt">apiGroups</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="l">jaas.metio.wtf]</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">resources</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="l">jsonnetlibraries]</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">verbs</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="l">get, list]</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span>- <span class="nt">apiGroups</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="l">source.toolkit.fluxcd.io]</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">resources</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="l">gitrepositories, ocirepositories, buckets, externalartifacts]</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">verbs</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="l">get]</span><span class="w">
</span></span></span></code></pre></div><p>When <code>operator.watchNamespaces</code> is set, the chart automatically switches from a
ClusterRoleBinding to per-namespace RoleBindings. Full RBAC layout and
NetworkPolicy notes for <code>spec.sourceRef</code> fetches are in
<a href="/usage/tenancy-and-rbac/">Tenancy and RBAC</a>
.</p>
<h2 id="6-plan-for-upgrades-and-disaster-recovery">6. Plan for upgrades and disaster recovery</h2>
<p>Calendar-based releases run every Monday. Chart upgrades are <code>helm upgrade --install</code>; the chart ships CRDs under <code>templates/</code> so schema changes apply
automatically. Read
<a href="https://github.com/metio/jaas/blob/main/MIGRATIONS.md">MIGRATIONS.md</a>
 before
each upgrade — releases that change immutable <code>spec.selector.matchLabels</code> fields
require a manual <code>kubectl delete deploy/jaas</code> first.</p>
<p>Three runbooks to bookmark before go-live:</p>
<ul>
<li><a href="/runbooks/storage-recovery/">storage-recovery</a>
 — PVC loss, S3 outages,
disk-full, downstream 404s.</li>
<li><a href="/runbooks/rbacdenied/">rbacdenied</a>
 — tenant SA missing a verb, ExternalArtifact
write forbidden, Flux source CRD not installed.</li>
<li><a href="/runbooks/operator-watch-silent/">operator-watch-silent</a>
 — the one failure
mode JaaS cannot surface in snippet status (operator&rsquo;s own ClusterRole missing
a verb so controller-runtime&rsquo;s informer silently fails).</li>
</ul>
<h2 id="a-complete-production-valuesyaml">A complete production values.yaml</h2>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nt">replicas</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">min</span><span class="p">:</span><span class="w"> </span><span class="m">2</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">max</span><span class="p">:</span><span class="w"> </span><span class="m">5</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">resources</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">memory</span><span class="p">:</span><span class="w"> </span><span class="l">256Mi</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">cpu</span><span class="p">:</span><span class="w"> </span><span class="l">100m</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">image</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">pullPolicy</span><span class="p">:</span><span class="w"> </span><span class="l">IfNotPresent</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">namespace</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">create</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">podSecurity</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">enforce</span><span class="p">:</span><span class="w"> </span><span class="l">restricted</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">operator</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">enabled</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">defaultServiceAccount</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;&#34;</span><span class="w">   </span><span class="c"># force per-snippet spec.serviceAccountName</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">serviceAccount</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">create</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">annotations</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">eks.amazonaws.com/role-arn</span><span class="p">:</span><span class="w"> </span><span class="l">arn:aws:iam::ACCOUNT:role/jaas-operator </span><span class="w"> </span><span class="c"># TODO</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">storage</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">backend</span><span class="p">:</span><span class="w"> </span><span class="l">s3</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">s3</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">endpoint</span><span class="p">:</span><span class="w"> </span><span class="l">s3.amazonaws.com </span><span class="w"> </span><span class="c"># TODO</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">bucket</span><span class="p">:</span><span class="w"> </span><span class="l">my-jaas-artifacts  </span><span class="w"> </span><span class="c"># TODO</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">prefix</span><span class="p">:</span><span class="w"> </span><span class="l">prod</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">region</span><span class="p">:</span><span class="w"> </span><span class="l">eu-west-1          </span><span class="w"> </span><span class="c"># TODO</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">maxArtifactBytes</span><span class="p">:</span><span class="w"> </span><span class="m">16777216</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">metrics</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">enabled</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">serviceMonitor</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">enabled</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">labels</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">release</span><span class="p">:</span><span class="w"> </span><span class="l">kube-prom       </span><span class="w"> </span><span class="c"># TODO</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">prometheusRule</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">enabled</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">labels</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">release</span><span class="p">:</span><span class="w"> </span><span class="l">kube-prom       </span><span class="w"> </span><span class="c"># TODO</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">extraAlertLabels</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">team</span><span class="p">:</span><span class="w"> </span><span class="l">platform           </span><span class="w"> </span><span class="c"># TODO</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">webhook</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">enabled</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">certMode</span><span class="p">:</span><span class="w"> </span><span class="l">cert-manager</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">certManager</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">issuerRef</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">kind</span><span class="p">:</span><span class="w"> </span><span class="l">ClusterIssuer</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">letsencrypt-prod   </span><span class="w"> </span><span class="c"># TODO</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">leaderElection</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">enabled</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">cleanupOnDelete</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">enabled</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="w">
</span></span></span></code></pre></div><p>Apply with:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">helm upgrade --install jaas oci://ghcr.io/metio/helm-charts/jaas <span class="se">\
</span></span></span><span class="line"><span class="cl">  --namespace jaas-system --create-namespace <span class="se">\
</span></span></span><span class="line"><span class="cl">  --values production-values.yaml <span class="se">\
</span></span></span><span class="line"><span class="cl">  --wait --timeout 5m
</span></span></code></pre></div><h2 id="next-steps">Next steps</h2>
<ul>
<li><a href="/installation/operations/">Operations</a>
 — day-two tasks: rolling restarts,
storage sweeping, finalizer teardown.</li>
<li><a href="/installation/configuration/">Configuration reference</a>
 — every flag and default.</li>
<li><a href="/runbooks/">Runbooks</a>
 — incident response.</li>
</ul>
]]></content><category scheme="https://jaas.projects.metio.wtf/tags/installation" term="installation" label="installation"/><category scheme="https://jaas.projects.metio.wtf/tags/production" term="production" label="production"/><category scheme="https://jaas.projects.metio.wtf/tags/ha" term="ha" label="ha"/><category scheme="https://jaas.projects.metio.wtf/tags/security" term="security" label="security"/><category scheme="https://jaas.projects.metio.wtf/tags/observability" term="observability" label="observability"/></entry><entry><title type="html">Quickstart</title><link href="https://jaas.projects.metio.wtf/tutorials/quickstart/?utm_source=atom_feed" rel="alternate" type="text/html"/><link href="https://jaas.projects.metio.wtf/installation/kubernetes/?utm_source=atom_feed" rel="related" type="text/html" title="Kubernetes"/><link href="https://jaas.projects.metio.wtf/usage/admission-webhook/?utm_source=atom_feed" rel="related" type="text/html" title="Admission webhook"/><link href="https://jaas.projects.metio.wtf/usage/alerting/?utm_source=atom_feed" rel="related" type="text/html" title="Alerting"/><link href="https://jaas.projects.metio.wtf/usage/creating-sources/?utm_source=atom_feed" rel="related" type="text/html" title="Creating source artifacts"/><link href="https://jaas.projects.metio.wtf/tutorials/deploying-manifests/?utm_source=atom_feed" rel="related" type="text/html" title="Deploying manifests with StageSet"/><id>https://jaas.projects.metio.wtf/tutorials/quickstart/</id><published>0001-01-01T00:00:00+00:00</published><updated>2026-06-17T16:04:27+02:00</updated><content type="html"><![CDATA[<blockquote>Install the JaaS operator and publish your first ExternalArtifact from an inline-files JsonnetSnippet.</blockquote><p>This tutorial takes you from an empty cluster to one published Flux
<code>ExternalArtifact</code> carrying rendered JSON. The path is operator mode with no
optional knobs — no webhook, no S3, no Flux source CRs — and a single
<code>JsonnetSnippet</code> whose source is inline <code>spec.files</code>.</p>
<h2 id="prerequisites">Prerequisites</h2>
<ul>
<li>
<p>A Kubernetes cluster. <code>kind</code>, <code>minikube</code>, or a managed cluster all work.</p>
</li>
<li>
<p><code>kubectl</code> configured to talk to it.</p>
</li>
<li>
<p><code>helm</code> 3.x.</p>
</li>
<li>
<p>Flux installed, at <strong>v2.7.0 or newer</strong>. A <code>JsonnetSnippet</code> publishes its
result as a Flux <code>ExternalArtifact</code>, and the <code>ExternalArtifact</code> CRD lands in
source-controller v1.7.0 (Flux v2.7.0) — earlier bundles have no such CRD and
the publish path fails. Install all of Flux:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">kubectl apply -f https://github.com/fluxcd/flux2/releases/download/v2.7.0/install.yaml
</span></span></code></pre></div></li>
</ul>
<h2 id="step-1--install-the-chart">Step 1 — Install the chart</h2>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">helm upgrade --install jaas oci://ghcr.io/metio/helm-charts/jaas <span class="se">\
</span></span></span><span class="line"><span class="cl">  --namespace jaas-system --create-namespace <span class="se">\
</span></span></span><span class="line"><span class="cl">  --set operator.enabled<span class="o">=</span><span class="nb">true</span> <span class="se">\
</span></span></span><span class="line"><span class="cl">  --set operator.defaultServiceAccount<span class="o">=</span>default <span class="se">\
</span></span></span><span class="line"><span class="cl">  --wait --timeout 5m
</span></span></code></pre></div><p><code>operator.defaultServiceAccount=default</code> tells the operator which
ServiceAccount to impersonate in a tenant namespace when a snippet does not name
its own. That is fine for this tutorial; production assigns a dedicated SA per
tenant — see <a href="/usage/tenancy-and-rbac/">Tenancy and RBAC</a>
.</p>
<p>Verify the operator is running:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">kubectl --namespace jaas-system get deploy jaas
</span></span><span class="line"><span class="cl"><span class="c1"># NAME   READY   UP-TO-DATE   AVAILABLE   AGE</span>
</span></span><span class="line"><span class="cl"><span class="c1"># jaas   1/1     1            1           30s</span>
</span></span></code></pre></div><h2 id="step-2--grant-the-tenant-serviceaccount-the-minimum-verbs">Step 2 — Grant the tenant ServiceAccount the minimum verbs</h2>
<p>The <code>default</code> ServiceAccount&rsquo;s built-in RBAC does not include the verbs the
operator needs to publish the artifact. In the tenant namespace — here <code>default</code>
— apply a <code>Role</code> and <code>RoleBinding</code>:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">cat <span class="s">&lt;&lt;EOF | kubectl apply -f -
</span></span></span><span class="line"><span class="cl"><span class="s">apiVersion: rbac.authorization.k8s.io/v1
</span></span></span><span class="line"><span class="cl"><span class="s">kind: Role
</span></span></span><span class="line"><span class="cl"><span class="s">metadata:
</span></span></span><span class="line"><span class="cl"><span class="s">  namespace: default
</span></span></span><span class="line"><span class="cl"><span class="s">  name: jaas-tenant
</span></span></span><span class="line"><span class="cl"><span class="s">rules:
</span></span></span><span class="line"><span class="cl"><span class="s">  - apiGroups: [source.toolkit.fluxcd.io]
</span></span></span><span class="line"><span class="cl"><span class="s">    resources: [externalartifacts]
</span></span></span><span class="line"><span class="cl"><span class="s">    verbs: [get, create, update, patch]
</span></span></span><span class="line"><span class="cl"><span class="s">---
</span></span></span><span class="line"><span class="cl"><span class="s">apiVersion: rbac.authorization.k8s.io/v1
</span></span></span><span class="line"><span class="cl"><span class="s">kind: RoleBinding
</span></span></span><span class="line"><span class="cl"><span class="s">metadata:
</span></span></span><span class="line"><span class="cl"><span class="s">  namespace: default
</span></span></span><span class="line"><span class="cl"><span class="s">  name: jaas-tenant
</span></span></span><span class="line"><span class="cl"><span class="s">subjects:
</span></span></span><span class="line"><span class="cl"><span class="s">  - kind: ServiceAccount
</span></span></span><span class="line"><span class="cl"><span class="s">    name: default
</span></span></span><span class="line"><span class="cl"><span class="s">    namespace: default
</span></span></span><span class="line"><span class="cl"><span class="s">roleRef:
</span></span></span><span class="line"><span class="cl"><span class="s">  apiGroup: rbac.authorization.k8s.io
</span></span></span><span class="line"><span class="cl"><span class="s">  kind: Role
</span></span></span><span class="line"><span class="cl"><span class="s">  name: jaas-tenant
</span></span></span><span class="line"><span class="cl"><span class="s">EOF</span>
</span></span></code></pre></div><p>The operator impersonates the tenant ServiceAccount to upsert the
<code>ExternalArtifact</code> it publishes. Without <code>create</code>, <code>update</code>, and <code>patch</code> on
<code>externalartifacts</code>, the first reconcile fails with <code>Reason=RBACDenied</code>.</p>
<p>Verify the binding:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">kubectl --namespace default get rolebinding jaas-tenant
</span></span><span class="line"><span class="cl"><span class="c1"># NAME          ROLE               AGE</span>
</span></span><span class="line"><span class="cl"><span class="c1"># jaas-tenant   Role/jaas-tenant   5s</span>
</span></span></code></pre></div><h2 id="step-3--apply-your-first-snippet">Step 3 — Apply your first snippet</h2>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">cat <span class="s">&lt;&lt;EOF | kubectl apply -f -
</span></span></span><span class="line"><span class="cl"><span class="s">apiVersion: jaas.metio.wtf/v1
</span></span></span><span class="line"><span class="cl"><span class="s">kind: JsonnetSnippet
</span></span></span><span class="line"><span class="cl"><span class="s">metadata:
</span></span></span><span class="line"><span class="cl"><span class="s">  name: hello
</span></span></span><span class="line"><span class="cl"><span class="s">  namespace: default
</span></span></span><span class="line"><span class="cl"><span class="s">spec:
</span></span></span><span class="line"><span class="cl"><span class="s">  serviceAccountName: default
</span></span></span><span class="line"><span class="cl"><span class="s">  files:
</span></span></span><span class="line"><span class="cl"><span class="s">    main.jsonnet: |
</span></span></span><span class="line"><span class="cl"><span class="s">      {
</span></span></span><span class="line"><span class="cl"><span class="s">        greeting: &#34;hello from jaas&#34;,
</span></span></span><span class="line"><span class="cl"><span class="s">        rendered_at: std.extVar(&#34;now&#34;),
</span></span></span><span class="line"><span class="cl"><span class="s">      }
</span></span></span><span class="line"><span class="cl"><span class="s">  externalVariables:
</span></span></span><span class="line"><span class="cl"><span class="s">    now: &#34;quickstart&#34;
</span></span></span><span class="line"><span class="cl"><span class="s">EOF</span>
</span></span></code></pre></div><p>This is a complete <code>JsonnetSnippet</code>. Three fields carry it:</p>
<ul>
<li><code>spec.serviceAccountName</code> — the ServiceAccount the operator impersonates for
this snippet&rsquo;s API calls.</li>
<li><code>spec.files.&lt;filename&gt;</code> — inline Jsonnet source. The default entry file is
<code>main.jsonnet</code>; override it with <code>spec.entryFile</code>.</li>
<li><code>spec.externalVariables</code> — <code>std.extVar()</code> lookups available to the snippet.</li>
</ul>
<p>Verify the resource exists:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">kubectl --namespace default get jsonnetsnippet hello
</span></span></code></pre></div><h2 id="step-4--confirm-it-reconciled">Step 4 — Confirm it reconciled</h2>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">kubectl --namespace default get jsonnetsnippet hello
</span></span><span class="line"><span class="cl"><span class="c1"># NAME    READY   URL                                                                                    AGE</span>
</span></span><span class="line"><span class="cl"><span class="c1"># hello   True    http://jaas-storage.jaas-system.svc.cluster.local:8082/default/hello/&lt;sha256&gt;.tar.gz   5s</span>
</span></span></code></pre></div><p>The <code>URL</code> column is the artifact&rsquo;s address. If <code>READY</code> is <code>False</code>, describe the
resource — the <code>Reason</code> and <code>Message</code> on the Ready condition name the problem
(most commonly an RBAC gap or a Jsonnet syntax error):</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">kubectl --namespace default describe jsonnetsnippet hello
</span></span></code></pre></div><p>The <code>ExternalArtifact</code> is the resource downstream Flux consumers read:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">kubectl --namespace default get externalartifact hello -o yaml
</span></span><span class="line"><span class="cl"><span class="c1"># status:</span>
</span></span><span class="line"><span class="cl"><span class="c1">#   artifact:</span>
</span></span><span class="line"><span class="cl"><span class="c1">#     url: http://jaas-storage.jaas-system.svc.cluster.local:8082/default/hello/&lt;sha256&gt;.tar.gz</span>
</span></span><span class="line"><span class="cl"><span class="c1">#     digest: sha256:&lt;hex&gt;</span>
</span></span><span class="line"><span class="cl"><span class="c1">#     revision: sha256:&lt;hex&gt;</span>
</span></span><span class="line"><span class="cl"><span class="c1">#     size: &lt;bytes&gt;</span>
</span></span></code></pre></div><h2 id="step-5--fetch-the-rendered-bytes">Step 5 — Fetch the rendered bytes</h2>
<p>The URL resolves in-cluster only. Fetch the tarball from a one-shot pod:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl"><span class="nv">URL</span><span class="o">=</span><span class="k">$(</span>kubectl --namespace default get jsonnetsnippet hello -o <span class="nv">jsonpath</span><span class="o">=</span><span class="s1">&#39;{.status.artifactURL}&#39;</span><span class="k">)</span>
</span></span><span class="line"><span class="cl">kubectl run --rm -i --restart<span class="o">=</span>Never --image<span class="o">=</span>docker.io/curlimages/curl:8.10.1 fetch -- <span class="se">\
</span></span></span><span class="line"><span class="cl">    sh -c <span class="s2">&#34;curl -fsSL &#39;</span><span class="nv">$URL</span><span class="s2">&#39; | tar -xzO rendered.json&#34;</span>
</span></span><span class="line"><span class="cl"><span class="c1"># {</span>
</span></span><span class="line"><span class="cl"><span class="c1">#    &#34;greeting&#34;: &#34;hello from jaas&#34;,</span>
</span></span><span class="line"><span class="cl"><span class="c1">#    &#34;rendered_at&#34;: &#34;quickstart&#34;</span>
</span></span><span class="line"><span class="cl"><span class="c1"># }</span>
</span></span></code></pre></div><p>The tarball carries a single <code>rendered.json</code> because <code>spec.output</code> defaults to
<code>rendered</code> (the evaluated JSON). Setting <code>spec.output: source</code> publishes the raw
<code>.jsonnet</code> files instead, for consumers that re-evaluate themselves.</p>
<h2 id="clean-up">Clean up</h2>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">kubectl --namespace default delete jsonnetsnippet hello
</span></span><span class="line"><span class="cl">kubectl --namespace default delete rolebinding jaas-tenant
</span></span><span class="line"><span class="cl">kubectl --namespace default delete role jaas-tenant
</span></span><span class="line"><span class="cl">helm --namespace jaas-system uninstall jaas
</span></span><span class="line"><span class="cl">kubectl delete namespace jaas-system
</span></span></code></pre></div><p>The chart&rsquo;s pre-delete hook waits for the snippet&rsquo;s finalizer to drop — which
removes the <code>ExternalArtifact</code> — before the operator pod is removed, so the
uninstall leaves no orphans.</p>
<h2 id="where-to-go-next">Where to go next</h2>
<ul>
<li><a href="/tutorials/grafana-dashboards/">Grafana dashboards</a>
 — render grafonnet
dashboards and hand them to the grafana-operator.</li>
<li><a href="/tutorials/deploying-manifests/">Deploying manifests with StageSet</a>
 — render
Kubernetes manifests and roll them out with stageset-controller.</li>
<li><a href="/usage/operator-mode/">Operator mode</a>
 — the full operator reference: source
kinds, leader election, the artifact contract.</li>
<li><a href="/usage/">Usage</a>
 — every configuration knob, one page per concern.</li>
</ul>
]]></content><category scheme="https://jaas.projects.metio.wtf/tags/operator" term="operator" label="operator"/><category scheme="https://jaas.projects.metio.wtf/tags/externalartifact" term="externalartifact" label="externalartifact"/><category scheme="https://jaas.projects.metio.wtf/tags/helm" term="helm" label="helm"/></entry><entry><title type="html">RBACDenied</title><link href="https://jaas.projects.metio.wtf/runbooks/rbacdenied/?utm_source=atom_feed" rel="alternate" type="text/html"/><link href="https://jaas.projects.metio.wtf/runbooks/crossnamespacerefrejected/?utm_source=atom_feed" rel="related" type="text/html" title="CrossNamespaceRefRejected"/><link href="https://jaas.projects.metio.wtf/runbooks/librarynotfound/?utm_source=atom_feed" rel="related" type="text/html" title="LibraryNotFound"/><link href="https://jaas.projects.metio.wtf/runbooks/serviceaccountmissing/?utm_source=atom_feed" rel="related" type="text/html" title="ServiceAccountMissing"/><link href="https://jaas.projects.metio.wtf/runbooks/operator-watch-silent/?utm_source=atom_feed" rel="related" type="text/html" title="Watch-layer silent failure"/><link href="https://jaas.projects.metio.wtf/runbooks/artifacttoolarge/?utm_source=atom_feed" rel="related" type="text/html" title="ArtifactTooLarge"/><id>https://jaas.projects.metio.wtf/runbooks/rbacdenied/</id><published>0001-01-01T00:00:00+00:00</published><updated>2026-06-17T10:56:50+02:00</updated><content type="html"><![CDATA[<blockquote>The apiserver returned Forbidden on a call the reconciler made with the tenant ServiceAccount&rsquo;s impersonated identity</blockquote><h2 id="symptom">Symptom</h2>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">kubectl describe jsonnetsnippet &lt;name&gt;
</span></span><span class="line"><span class="cl">...
</span></span><span class="line"><span class="cl">Status:
</span></span><span class="line"><span class="cl">  Conditions:
</span></span><span class="line"><span class="cl">    Reason:  RBACDenied
</span></span><span class="line"><span class="cl">    Status:  False
</span></span><span class="line"><span class="cl">    Type:    Ready
</span></span><span class="line"><span class="cl">    Message: RBAC denied reading the source CR — grant the tenant ServiceAccount get on the source kind ...
</span></span></code></pre></div><p>Or for a missing CRD:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">    Message: source CR&#39;s kind is not registered with the apiserver — install the corresponding CRD ...
</span></span></code></pre></div><p>The reconciler logs at warn level and stops engaging backoff for this snippet. The next reconcile happens only when the snippet&rsquo;s spec changes, a referenced library / source CR&rsquo;s status flips, or <code>spec.interval</code> ticks — so the workqueue isn&rsquo;t burning cycles on a permanently-failing call.</p>
<h2 id="cause">Cause</h2>
<p>The apiserver returned <code>Forbidden</code> on a call the reconciler had to make. Three call sites can surface this:</p>
<ol>
<li><strong>Source-CR read.</strong> The tenant ServiceAccount lacks <code>get</code> on the kind named by <code>spec.sourceRef.kind</code>. The fix is on the tenant&rsquo;s <code>Role</code> / <code>RoleBinding</code>.</li>
<li><strong>Library-CR read.</strong> The tenant SA lacks <code>get</code> (and typically <code>list</code>) on <code>jsonnetlibraries</code> in the snippet&rsquo;s namespace.</li>
<li><strong>ExternalArtifact write.</strong> The tenant SA lacks <code>create</code> / <code>update</code> / <code>patch</code> on <code>externalartifacts</code>. This is the publish step — the rendered bytes are computed but the operator can&rsquo;t write them back as the impersonating client.</li>
</ol>
<p>The <code>NoMatchError</code> variant means the apiserver doesn&rsquo;t know about the resource kind at all — typically because the corresponding CRD (usually Flux&rsquo;s source-controller) isn&rsquo;t installed in the cluster.</p>
<h2 id="diagnosis">Diagnosis</h2>
<p><code>kubectl describe</code> shows the operator&rsquo;s classified message. The verbatim apiserver error (<code>forbidden: ServiceAccount X cannot get resource Y in namespace Z</code>) is appended after the operator&rsquo;s classification, so you can read off:</p>
<ul>
<li>Which SA tried the call (<code>system:serviceaccount:&lt;namespace&gt;:&lt;sa-name&gt;</code>)</li>
<li>Which verb it lacked (<code>cannot get</code>, <code>cannot create</code>, <code>cannot patch</code>)</li>
<li>Which resource (<code>gitrepositories.source.toolkit.fluxcd.io</code>, <code>jsonnetlibraries.jaas.metio.wtf</code>, <code>externalartifacts.source.toolkit.fluxcd.io</code>)</li>
</ul>
<p>Verify the SA exists and inspect its current permissions:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">kubectl --namespace &lt;tenant-namespace&gt; get sa &lt;sa-name&gt;
</span></span><span class="line"><span class="cl">kubectl auth can-i --as<span class="o">=</span>system:serviceaccount:&lt;tenant-namespace&gt;:&lt;sa-name&gt; <span class="se">\
</span></span></span><span class="line"><span class="cl">    --namespace &lt;tenant-namespace&gt; <span class="se">\
</span></span></span><span class="line"><span class="cl">    &lt;verb&gt; &lt;resource&gt;
</span></span></code></pre></div><p>For the <code>NoMatchError</code> variant:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl"><span class="c1"># Verify the CRD is actually installed:</span>
</span></span><span class="line"><span class="cl">kubectl get crd <span class="p">|</span> grep -E <span class="s1">&#39;source.toolkit.fluxcd.io|jaas.metio.wtf&#39;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># If source-controller&#39;s CRDs are missing, install Flux:</span>
</span></span><span class="line"><span class="cl"><span class="c1"># https://fluxcd.io/flux/installation/</span>
</span></span></code></pre></div><h2 id="remediation">Remediation</h2>
<p>Grant the missing verb to the tenant SA. The minimum verbs JaaS expects are documented in the <a href="https://jaas.projects.metio.wtf/usage/tenancy-and-rbac/#the-tenant-role">Tenancy and RBAC</a>
 guide. Typical fix:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nt">apiVersion</span><span class="p">:</span><span class="w"> </span><span class="l">rbac.authorization.k8s.io/v1</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">kind</span><span class="p">:</span><span class="w"> </span><span class="l">Role</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">metadata</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">namespace</span><span class="p">:</span><span class="w"> </span><span class="l">&lt;tenant-namespace&gt;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">jaas-tenant</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">rules</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span>- <span class="nt">apiGroups</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="l">source.toolkit.fluxcd.io]</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">resources</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="l">externalartifacts]</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">verbs</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="l">get, create, update, patch]</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span>- <span class="nt">apiGroups</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="l">source.toolkit.fluxcd.io]</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">resources</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="l">gitrepositories, ocirepositories, buckets, externalartifacts]</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">verbs</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="l">get]</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span>- <span class="nt">apiGroups</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="l">jaas.metio.wtf]</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">resources</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="l">jsonnetlibraries]</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">verbs</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="l">get, list]</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nn">---</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">apiVersion</span><span class="p">:</span><span class="w"> </span><span class="l">rbac.authorization.k8s.io/v1</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">kind</span><span class="p">:</span><span class="w"> </span><span class="l">RoleBinding</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">metadata</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">namespace</span><span class="p">:</span><span class="w"> </span><span class="l">&lt;tenant-namespace&gt;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">jaas-tenant</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">subjects</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span>- <span class="nt">kind</span><span class="p">:</span><span class="w"> </span><span class="l">ServiceAccount</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">&lt;sa-name&gt;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">namespace</span><span class="p">:</span><span class="w"> </span><span class="l">&lt;tenant-namespace&gt;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">roleRef</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">apiGroup</span><span class="p">:</span><span class="w"> </span><span class="l">rbac.authorization.k8s.io</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">kind</span><span class="p">:</span><span class="w"> </span><span class="l">Role</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">jaas-tenant</span><span class="w">
</span></span></span></code></pre></div><p>After the RBAC change, force the next reconcile (the snippet&rsquo;s last spec edit doesn&rsquo;t auto-retrigger because the failure was non-transient):</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">kubectl annotate jsonnetsnippet &lt;name&gt; jaas.metio.wtf/reconcile-at<span class="o">=</span><span class="k">$(</span>date -u +%FT%TZ<span class="k">)</span> --overwrite
</span></span></code></pre></div><p>For the missing-CRD case, installing the CRD fires the operator&rsquo;s <code>crdWatcher</code>, which engages the watch automatically — no manual nudge needed.</p>
<h2 id="why-this-is-non-transient">Why this is non-transient</h2>
<p><code>Forbidden</code> doesn&rsquo;t recover by retry. The cluster operator (or whoever owns the tenant&rsquo;s RBAC) has to grant the verb. Retrying every 16 minutes would pile up wasted API calls and obscure the workqueue&rsquo;s signal. The non-transient classification lets the workqueue depth metric remain meaningful — anything on it is genuinely live work.</p>
<p><code>NoMatchError</code> is the same shape: until the CRD is installed, the kind doesn&rsquo;t exist. Retry can&rsquo;t conjure it.</p>
]]></content><category scheme="https://jaas.projects.metio.wtf/tags/runbooks" term="runbooks" label="runbooks"/><category scheme="https://jaas.projects.metio.wtf/tags/troubleshooting" term="troubleshooting" label="troubleshooting"/><category scheme="https://jaas.projects.metio.wtf/tags/rbac" term="rbac" label="rbac"/></entry><entry><title type="html">Rendering endpoint</title><link href="https://jaas.projects.metio.wtf/usage/rendering-endpoint/?utm_source=atom_feed" rel="alternate" type="text/html"/><link href="https://jaas.projects.metio.wtf/tutorials/local-rendering/?utm_source=atom_feed" rel="related" type="text/html" title="Local rendering"/><id>https://jaas.projects.metio.wtf/usage/rendering-endpoint/</id><published>0001-01-01T00:00:00+00:00</published><updated>2026-06-16T20:48:39+02:00</updated><content type="html"><![CDATA[<blockquote>The GET /jsonnet/{snippet} request, snippet resolution, the management probes, and the stable error contract.</blockquote><p>Send a <code>GET</code> to the rendering endpoint with a snippet name and JaaS returns the
evaluated Jsonnet as JSON:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">curl http://127.0.0.1:8080/jsonnet/example1
</span></span></code></pre></div><p>The Jsonnet server binds <code>127.0.0.1:8080</code> by default (<code>--listen-address</code>,
<code>--port</code>). The URL shape is <code>GET /&lt;jsonnet-endpoint-path&gt;/{snippet...}</code>, where
<code>{snippet...}</code> is a trailing path segment that may contain slashes. A successful
response carries <code>Content-Type: application/json</code> and the rendered document.</p>
<h2 id="the-endpoint-path">The endpoint path</h2>
<p>The leading path segment defaults to <code>jsonnet</code> and is set with
<code>--jsonnet-endpoint-path</code>. Running with <code>--jsonnet-endpoint-path render</code> moves the
endpoint to <code>GET /render/{snippet...}</code>:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">./jaas --jsonnet-endpoint-path render --snippet-directory examples/snippets/dashboards
</span></span><span class="line"><span class="cl">curl http://127.0.0.1:8080/render/example1
</span></span></code></pre></div><h2 id="snippet-resolution">Snippet resolution</h2>
<p>The <code>{snippet...}</code> segment names which file JaaS evaluates. Resolution checks
the <code>--snippet</code> files first, then looks for <code>&lt;name&gt;/main.jsonnet</code> under each
<code>--snippet-directory</code>. See
<a href="/usage/snippets-and-libraries/">Snippets and libraries</a>
 for how to declare
both.</p>
<p>Resolution is sandboxed through Go&rsquo;s <code>os.Root</code>, which rejects <code>..</code> traversal and
symlinks that escape the configured directory. A crafted URL never reaches a
file outside the snippet roots:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">curl -i http://127.0.0.1:8080/jsonnet/../etc/passwd
</span></span><span class="line"><span class="cl"><span class="c1"># HTTP/1.1 404 Not Found</span>
</span></span></code></pre></div><h2 id="management-probes">Management probes</h2>
<p>A second HTTP server — the management server — exposes the Kubernetes lifecycle
probes. It binds <code>127.0.0.1:8081</code> by default (<code>--management-listen-address</code>,
<code>--management-port</code>):</p>
<table>
	<thead>
			<tr>
					<th>Path</th>
					<th>Meaning</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td><code>/live</code></td>
					<td>Liveness. Unconditional <code>200</code>.</td>
			</tr>
			<tr>
					<td><code>/start</code></td>
					<td>Startup. Consults health state; <code>200</code> once started, otherwise <code>503</code> + JSON.</td>
			</tr>
			<tr>
					<td><code>/ready</code></td>
					<td>Readiness. Consults health state; <code>200</code> when ready, otherwise <code>503</code> + JSON.</td>
			</tr>
	</tbody>
</table>
<p>A not-ready probe returns a JSON body naming the state:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">curl -i http://127.0.0.1:8081/ready
</span></span><span class="line"><span class="cl"><span class="c1"># HTTP/1.1 503 Service Unavailable</span>
</span></span><span class="line"><span class="cl"><span class="c1"># {&#34;status&#34;:&#34;not ready&#34;}</span>
</span></span></code></pre></div><h2 id="error-contract">Error contract</h2>
<p>Every non-2xx response carries a JSON body with <code>Content-Type: application/json</code>
so programmatic callers can pick the failure apart:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-json" data-lang="json"><span class="line"><span class="cl"><span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="nt">&#34;error&#34;</span><span class="p">:</span>   <span class="s2">&#34;snippet_not_found&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="nt">&#34;message&#34;</span><span class="p">:</span> <span class="s2">&#34;snippet \&#34;missing\&#34; not found&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="nt">&#34;snippet&#34;</span><span class="p">:</span> <span class="s2">&#34;missing&#34;</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre></div><p>The <code>error</code> field is a stable identifier — callers match on it, and these
strings do not change. The <code>message</code> field carries human-readable detail. The
<code>snippet</code> field echoes the requested name when one was parsed, and is omitted
otherwise.</p>
<table>
	<thead>
			<tr>
					<th><code>error</code></th>
					<th style="text-align: right">HTTP status</th>
					<th>When</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td><code>method_not_allowed</code></td>
					<td style="text-align: right"><code>405</code></td>
					<td>Anything other than <code>GET</code> on the endpoint.</td>
			</tr>
			<tr>
					<td><code>snippet_not_found</code></td>
					<td style="text-align: right"><code>404</code></td>
					<td>The requested snippet name resolves to no file.</td>
			</tr>
			<tr>
					<td><code>evaluation_timeout</code></td>
					<td style="text-align: right"><code>504</code></td>
					<td>Evaluation exceeded <code>--evaluation-timeout</code>.</td>
			</tr>
			<tr>
					<td><code>evaluation_unavailable</code></td>
					<td style="text-align: right"><code>503</code></td>
					<td>The concurrent-eval cap (<code>--max-concurrent-evals</code>) is full.</td>
			</tr>
			<tr>
					<td><code>evaluation_failed</code></td>
					<td style="text-align: right"><code>400</code></td>
					<td>go-jsonnet returned an error (syntax, missing import, stack-limit exceeded).</td>
			</tr>
	</tbody>
</table>
<p>For <code>evaluation_failed</code>, <code>message</code> is the raw go-jsonnet diagnostic, including
the file and line numbers from the snippet on disk. That diagnostic can name
on-disk paths, so treat it as cluster-internal detail.</p>
<p>A client that closes the connection mid-evaluation receives no body and no
status line — the handler detects the cancellation and returns without writing
anything.</p>
<p>The timeout, stack, and concurrency caps that drive <code>evaluation_timeout</code> and
<code>evaluation_unavailable</code> are documented in
<a href="/usage/evaluation-and-security/">Evaluation and security</a>
. To pass values into
a render, see
<a href="/usage/external-variables-and-tlas/">External variables and TLAs</a>
.</p>
]]></content><category scheme="https://jaas.projects.metio.wtf/tags/http" term="http" label="http"/><category scheme="https://jaas.projects.metio.wtf/tags/endpoint" term="endpoint" label="endpoint"/><category scheme="https://jaas.projects.metio.wtf/tags/errors" term="errors" label="errors"/></entry><entry><title type="html">Self-signed webhook cert renewal failing</title><link href="https://jaas.projects.metio.wtf/runbooks/webhook-cert-renewal/?utm_source=atom_feed" rel="alternate" type="text/html"/><link href="https://jaas.projects.metio.wtf/runbooks/artifacttoolarge/?utm_source=atom_feed" rel="related" type="text/html" title="ArtifactTooLarge"/><link href="https://jaas.projects.metio.wtf/runbooks/crd-watch-engagement/?utm_source=atom_feed" rel="related" type="text/html" title="CRD watch engagement failing"/><link href="https://jaas.projects.metio.wtf/runbooks/crossnamespacerefrejected/?utm_source=atom_feed" rel="related" type="text/html" title="CrossNamespaceRefRejected"/><link href="https://jaas.projects.metio.wtf/runbooks/dependencycycle/?utm_source=atom_feed" rel="related" type="text/html" title="DependencyCycle"/><link href="https://jaas.projects.metio.wtf/runbooks/eval-saturation/?utm_source=atom_feed" rel="related" type="text/html" title="Eval-concurrency saturation"/><id>https://jaas.projects.metio.wtf/runbooks/webhook-cert-renewal/</id><published>0001-01-01T00:00:00+00:00</published><updated>2026-06-17T10:56:50+02:00</updated><content type="html"><![CDATA[<blockquote>The Renewer goroutine cannot rotate the webhook TLS material; if uncorrected the cert will expire and every JsonnetSnippet admission will fail</blockquote><p>Fires when <code>jaas_webhook_cert_renewal_failures_total</code> has increased above the configured per-hour threshold. The <code>Renewer</code> background goroutine rotates the self-signed TLS material every <code>Validity / 3</code> (typically every few months for a year-long cert). When it can&rsquo;t, the existing cert keeps working until its natural expiry — at which point the apiserver stops trusting the chain and <strong>every JsonnetSnippet admission fails cluster-wide with <code>x509</code> errors</strong>.</p>
<h2 id="symptom">Symptom</h2>
<ul>
<li><code>JaaSWebhookCertRenewalFailing</code> alert is firing (severity: critical).</li>
<li>Operator pod logs carry repeated <code>Self-signed webhook cert renewal failed</code> warnings at the <code>Renewer.Interval</code> cadence.</li>
<li><code>kubectl describe validatingwebhookconfiguration &lt;name&gt;</code> shows a <code>caBundle</code> that hasn&rsquo;t rotated since the failures started.</li>
<li>The pod stays <code>Ready=True</code> — the renewer&rsquo;s failures don&rsquo;t gate the readiness probe.</li>
</ul>
<h2 id="diagnosis">Diagnosis</h2>
<p>The most common causes, in order of frequency:</p>
<h3 id="cause-a--rbac-drift-on-the-named-vwc">Cause A — RBAC drift on the named VWC</h3>
<p>The operator&rsquo;s ClusterRole pins <code>resourceNames: [&lt;VWCName&gt;]</code> on the <code>validatingwebhookconfigurations</code> patch verb. A chart upgrade that changes <code>operator.webhook.vwcName</code> (or a manual chart edit) leaves the running pod patching a name it no longer has permission for.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">kubectl auth can-i patch validatingwebhookconfiguration/&lt;vwc-name&gt; <span class="se">\
</span></span></span><span class="line"><span class="cl">  --as<span class="o">=</span>system:serviceaccount:&lt;namespace&gt;:&lt;operator-sa&gt;
</span></span></code></pre></div><p>If the answer is &ldquo;no&rdquo;, the chart&rsquo;s <code>operator-cluster</code> ClusterRole needs the current VWC name added to <code>resourceNames</code> (or the running pod restarted to pick up the new name).</p>
<h3 id="cause-b--vwc-renamed-out-from-under-the-operator">Cause B — VWC renamed out from under the operator</h3>
<p>A separate controller (admission policy automation, GitOps drift correction) renamed the VWC. The operator is patching a stale name.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">kubectl get validatingwebhookconfigurations <span class="se">\
</span></span></span><span class="line"><span class="cl">  --selector <span class="s1">&#39;app.kubernetes.io/instance=&lt;release-name&gt;&#39;</span>
</span></span></code></pre></div><p>If the live name differs from the operator&rsquo;s <code>--webhook-validating-config-name</code> flag, redeploy the operator with the correct flag or rename the VWC back.</p>
<h3 id="cause-c--certdir-gone-read-only">Cause C — <code>CertDir</code> gone read-only</h3>
<p>The chart mounts <code>CertDir</code> as an <code>emptyDir</code> by default. A <code>kubectl apply</code> that adds a <code>readOnlyRootFilesystem: true</code> security context or a sidecar that re-mounts the volume can break writes.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">kubectl --namespace &lt;ns&gt; <span class="nb">exec</span> &lt;operator-pod&gt; -- ls -l /tmp/k8s-webhook-server/serving-certs/
</span></span><span class="line"><span class="cl">kubectl --namespace &lt;ns&gt; <span class="nb">exec</span> &lt;operator-pod&gt; -- touch /tmp/k8s-webhook-server/serving-certs/.write-probe
</span></span></code></pre></div><p>If the touch fails, the security-context or volume mount needs fixing.</p>
<h2 id="remediation">Remediation</h2>
<ol>
<li>
<p><strong>Fix the root cause</strong> (RBAC, name, or mount).</p>
</li>
<li>
<p><strong>Roll the operator pod</strong> to force a fresh bootstrap of the cert and a re-patch of the VWC:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">kubectl --namespace &lt;ns&gt; rollout restart deployment &lt;operator-deployment&gt;
</span></span></code></pre></div><p>The new pod&rsquo;s bootstrap path goes through the dual-CA union (DD8), so existing replicas stay trusted across the rotation.</p>
</li>
<li>
<p><strong>Verify renewal is healthy</strong> after the bounce — the <code>jaas_webhook_cert_renewal_failures_total</code> counter should stop increasing, and the alert clears once the <code>for:</code> window passes.</p>
</li>
</ol>
<h2 id="when-to-consider-switching-to-cert-manager">When to consider switching to cert-manager</h2>
<p>If the self-signed renewer keeps tripping over your environment&rsquo;s RBAC story or pod-security policies, the chart supports <code>operator.webhook.certMode: cert-manager</code> — cert-manager handles the rotation and the operator mounts the resulting secret. Trade-off: requires cert-manager installed and an Issuer configured.</p>
]]></content><category scheme="https://jaas.projects.metio.wtf/tags/runbooks" term="runbooks" label="runbooks"/><category scheme="https://jaas.projects.metio.wtf/tags/troubleshooting" term="troubleshooting" label="troubleshooting"/><category scheme="https://jaas.projects.metio.wtf/tags/webhook" term="webhook" label="webhook"/></entry><entry><title type="html">Service mesh</title><link href="https://jaas.projects.metio.wtf/usage/service-mesh/?utm_source=atom_feed" rel="alternate" type="text/html"/><link href="https://jaas.projects.metio.wtf/usage/network-policy/?utm_source=atom_feed" rel="related" type="text/html" title="Network policy"/><link href="https://jaas.projects.metio.wtf/usage/evaluation-and-security/?utm_source=atom_feed" rel="related" type="text/html" title="Evaluation and security"/><link href="https://jaas.projects.metio.wtf/installation/production/?utm_source=atom_feed" rel="related" type="text/html" title="Production"/><id>https://jaas.projects.metio.wtf/usage/service-mesh/</id><published>0001-01-01T00:00:00+00:00</published><updated>2026-06-16T21:40:39+02:00</updated><content type="html"><![CDATA[<blockquote>The opt-in service-mesh authorization the chart ships — Istio or Linkerd identity-based authorization and mTLS layered over networkPolicy, per-port allowed mesh identities, the non-mesh carve-outs for the apiserver and kubelet, and native passthrough.</blockquote><p>The Helm chart ships an opt-in service-mesh authorization layer for the JaaS pod.
It is off by default and renders only when <code>serviceMesh.enabled</code> is <code>true</code>. Where
the <a href="/usage/network-policy/">network policy</a>
 operates at L3/L4 — which pods and IP
ranges may reach which ports — the service mesh operates at L7 with <strong>identity-based
authorization</strong> and <strong>mTLS</strong>: it asks <em>which mesh identity</em> is calling, proven by a
cryptographic workload certificate, not merely which IP the packet came from.</p>
<p><code>serviceMesh</code> and <code>networkPolicy</code> are separate fields and compose additively. They
solve different problems and are best enabled together: the network policy draws the
L3/L4 perimeter, and the mesh authorizes meshed callers by SPIFFE identity on top of
it. Enabling one does not require the other, and neither weakens the other.</p>
<h2 id="opt-in-and-explicit-engine">Opt-in and explicit engine</h2>
<p><code>serviceMesh.engine</code> selects which mesh dialect the chart renders. It is explicit,
not auto-detected: a chart that sniffed the running mesh would render different
objects on different clusters from identical values, which breaks GitOps determinism.
You name the engine, and the rendered manifest is the same everywhere.</p>
<table>
	<thead>
			<tr>
					<th><code>engine</code></th>
					<th>Renders</th>
					<th>API</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td><code>istio</code> (default)</td>
					<td><code>AuthorizationPolicy</code> + (optional) <code>PeerAuthentication</code></td>
					<td><code>security.istio.io/v1</code></td>
			</tr>
			<tr>
					<td><code>linkerd</code></td>
					<td><code>Server</code> + <code>AuthorizationPolicy</code> + <code>MeshTLSAuthentication</code></td>
					<td><code>policy.linkerd.io</code></td>
			</tr>
	</tbody>
</table>
<p>The rendered objects are inert unless the named mesh is actually installed and the
JaaS pod is injected into it. Enabling <code>serviceMesh</code> on an un-meshed pod renders the
manifests but changes nothing about how traffic flows.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nt">serviceMesh</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">enabled</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">engine</span><span class="p">:</span><span class="w"> </span><span class="l">istio</span><span class="w">
</span></span></span></code></pre></div><h2 id="per-port-authorization">Per-port authorization</h2>
<p>Each mesh-reachable port carries a <code>from</code> list naming the mesh identities allowed to
call it. An <strong>empty <code>from</code> list leaves that port open</strong> to any meshed caller,
mirroring <code>networkPolicy</code>&rsquo;s empty-<code>from</code>-is-open semantics; a non-empty list
restricts the port to the listed identities.</p>
<p>The mesh-reachable ports are the Jsonnet HTTP port, the storage port, and the metrics
port:</p>
<table>
	<thead>
			<tr>
					<th>Port</th>
					<th>Mode</th>
					<th><code>from</code> restricts</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td>Jsonnet HTTP (<code>ports.http</code>, <code>8080</code>)</td>
					<td>always</td>
					<td>Callers of the <code>/jsonnet</code> endpoint</td>
			</tr>
			<tr>
					<td>Storage HTTP (<code>ports.storage</code>, <code>8082</code>)</td>
					<td>operator</td>
					<td>Flux consumers that dereference <code>ExternalArtifact</code> tarballs</td>
			</tr>
			<tr>
					<td>Metrics (<code>ports.metrics</code>, <code>8083</code>)</td>
					<td>operator + metrics</td>
					<td>Prometheus scraping <code>/metrics</code></td>
			</tr>
	</tbody>
</table>
<p>A <code>from</code> entry is a source matcher with two fields:</p>
<ul>
<li><strong><code>principals</code></strong> — SPIFFE/mesh identities. On Istio these are
<code>source.principals</code> (<code>cluster.local/ns/&lt;ns&gt;/sa/&lt;sa&gt;</code>). On Linkerd they map to
<code>MeshTLSAuthentication</code> identities (<code>&lt;sa&gt;.&lt;ns&gt;.serviceaccount.identity.linkerd.cluster.local</code>,
or <code>*</code>).</li>
<li><strong><code>namespaces</code></strong> — source namespaces (Istio <code>source.namespaces</code>). <strong>Istio-only</strong> —
Linkerd authenticates by workload identity, not by namespace, so this field is
ignored under the <code>linkerd</code> engine.</li>
</ul>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nt">serviceMesh</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">enabled</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">engine</span><span class="p">:</span><span class="w"> </span><span class="l">istio</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="c"># Restrict the storage port to the kustomize-controller&#39;s identity.</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">storage</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">from</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="nt">principals</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span>- <span class="l">cluster.local/ns/flux-system/sa/kustomize-controller</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span>- <span class="l">cluster.local/ns/flux-system/sa/helm-controller</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="c"># Scope metrics scraping to the monitoring namespace.</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">metrics</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">from</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="nt">namespaces</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span>- <span class="l">monitoring</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="c"># Leave http open so any meshed caller can reach the renderer.</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">http</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">from</span><span class="p">:</span><span class="w"> </span><span class="p">[]</span><span class="w">
</span></span></span></code></pre></div><h2 id="non-mesh-clients-keep-working">Non-mesh clients keep working</h2>
<p>The kube-apiserver (which dials the admission webhook) and the kubelet (which dials
the readiness, liveness, and startup probes) are <strong>not part of the mesh</strong>. They carry
no mesh identity and present no workload certificate, so any authorization rule that
demanded one would reject them — admission would break and probes would fail.</p>
<p>The chart deliberately leaves the <strong>webhook and management ports open</strong> so these
non-mesh clients always connect:</p>
<ul>
<li><strong>Istio</strong> adds an allow-any rule covering the webhook (<code>9443</code>) and management
(<code>8081</code>) ports, so no identity is required on them.</li>
<li>With <strong><code>mtls: strict</code></strong>, the chart additionally sets <code>portLevelMtls: PERMISSIVE</code> on
those two ports, so a plaintext connection from the apiserver or kubelet is still
accepted even while every other port enforces mTLS.</li>
<li><strong>Linkerd</strong> renders no <code>Server</code> for those ports, leaving them outside the mesh&rsquo;s
authorization scope entirely.</li>
</ul>
<p>This is why admission and probes keep working with the mesh fully enabled.
Authenticity on the webhook port is enforced by TLS and the CA bundle on the
<code>ValidatingWebhookConfiguration</code>, not by the mesh — see the
<a href="/usage/admission-webhook/">admission webhook page</a>
.</p>
<h2 id="mtls">mTLS</h2>
<p><code>serviceMesh.mtls</code> sets the mTLS posture and applies to the <strong>Istio engine only</strong>;
Linkerd negotiates mTLS automatically between meshed pods, so the knob is ignored
there.</p>
<table>
	<thead>
			<tr>
					<th><code>mtls</code></th>
					<th>Effect (Istio)</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td><code>&quot;&quot;</code> (default)</td>
					<td>Defers to the mesh&rsquo;s own default (mesh-wide <code>PeerAuthentication</code> / <code>MeshConfig</code>) — no <code>PeerAuthentication</code> is rendered</td>
			</tr>
			<tr>
					<td><code>permissive</code></td>
					<td>Renders a <code>PeerAuthentication</code> accepting both mTLS and plaintext</td>
			</tr>
			<tr>
					<td><code>strict</code></td>
					<td>Requires mTLS on the workload&rsquo;s ports, <strong>except</strong> the webhook and management ports, which get a port-level <code>PERMISSIVE</code> carve-out</td>
			</tr>
	</tbody>
</table>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nt">serviceMesh</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">enabled</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">engine</span><span class="p">:</span><span class="w"> </span><span class="l">istio</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">mtls</span><span class="p">:</span><span class="w"> </span><span class="l">strict</span><span class="w">
</span></span></span></code></pre></div><p>Under <code>strict</code>, every mesh-reachable port enforces mTLS while the apiserver and
kubelet still reach the webhook and probes over plaintext via the carve-out above.</p>
<h2 id="default-deny">Default-deny</h2>
<p><code>serviceMesh.defaultDeny.enabled</code> (default <code>false</code>) additionally renders a
namespace-wide default-deny so every pod in the install namespace rejects
unauthorized mesh traffic and the per-workload allows become the only exceptions (a
zero-trust namespace).</p>
<ul>
<li><strong>Istio</strong> renders an empty-spec <code>AuthorizationPolicy</code> (deny-all) scoped to the
whole namespace; it sits at lower precedence than the workload <code>ALLOW</code>, so the
per-port allows always win for the JaaS pod while everything else is denied.</li>
<li><strong>Linkerd</strong> has no per-object deny-all; the namespace default is set via the
<code>config.linkerd.io/default-inbound-policy</code> annotation, stamped onto the
chart-managed Namespace (requires <code>namespace.create=true</code>) — otherwise annotate the
namespace out of band.</li>
</ul>
<p>Enable this <strong>only when JaaS owns its namespace</strong>, because the deny-all also denies
every co-located workload that does not have its own allowing authorization.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nt">serviceMesh</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">enabled</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">engine</span><span class="p">:</span><span class="w"> </span><span class="l">istio</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">defaultDeny</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">enabled</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="w">   </span><span class="c"># only when JaaS owns this namespace</span><span class="w">
</span></span></span></code></pre></div><h2 id="native-passthrough">Native passthrough</h2>
<p>Anything the per-port <code>from</code> knobs cannot express goes into the engine&rsquo;s native
passthrough list, merged verbatim into the rendered objects.</p>
<p>Under the <code>istio</code> engine, <code>serviceMesh.istio.rules</code> are merged into the
<code>AuthorizationPolicy</code>&rsquo;s <code>spec.rules</code> (the <code>security.istio.io/v1</code> rule schema) — use it
for path/method matchers, <code>when</code> JWT-claim conditions, <code>ipBlocks</code>, and similar:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nt">serviceMesh</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">enabled</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">engine</span><span class="p">:</span><span class="w"> </span><span class="l">istio</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">istio</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">rules</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="nt">from</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span>- <span class="nt">source</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">              </span><span class="nt">requestPrincipals</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">                </span>- <span class="s2">&#34;https://accounts.example.com/*&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">to</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span>- <span class="nt">operation</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">              </span><span class="nt">paths</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">                </span>- <span class="l">/jsonnet/*</span><span class="w">
</span></span></span></code></pre></div><p>Under the <code>linkerd</code> engine, <code>serviceMesh.linkerd.authorizations</code> are appended verbatim
as additional documents after the rendered <code>Server</code> / <code>AuthorizationPolicy</code> /
<code>MeshTLSAuthentication</code> set — each entry must be a complete <code>policy.linkerd.io</code> object:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nt">serviceMesh</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">enabled</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">engine</span><span class="p">:</span><span class="w"> </span><span class="l">linkerd</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">linkerd</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">authorizations</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="nt">apiVersion</span><span class="p">:</span><span class="w"> </span><span class="l">policy.linkerd.io/v1beta3</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">kind</span><span class="p">:</span><span class="w"> </span><span class="l">AuthorizationPolicy</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">metadata</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">jaas-extra</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">spec</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">targetRef</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span><span class="nt">group</span><span class="p">:</span><span class="w"> </span><span class="l">policy.linkerd.io</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span><span class="nt">kind</span><span class="p">:</span><span class="w"> </span><span class="l">Server</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">jaas-http</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">requiredAuthenticationRefs</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span>- <span class="nt">kind</span><span class="p">:</span><span class="w"> </span><span class="l">ServiceAccount</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">              </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">custom-caller</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">              </span><span class="nt">namespace</span><span class="p">:</span><span class="w"> </span><span class="l">tenant-a</span><span class="w">
</span></span></span></code></pre></div><h2 id="which-traffic-gets-authorized">Which traffic gets authorized</h2>
<p>The mesh authorizes only meshed callers on the mesh-reachable ports; the non-mesh
ports stay open by design so the control plane keeps working.</p>
<table>
	<thead>
			<tr>
					<th>Port</th>
					<th>Authorized by the mesh?</th>
					<th>Why</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td>Jsonnet HTTP (<code>8080</code>)</td>
					<td>Yes — <code>serviceMesh.http.from</code></td>
					<td>Meshed callers of the renderer</td>
			</tr>
			<tr>
					<td>Storage HTTP (<code>8082</code>)</td>
					<td>Yes — <code>serviceMesh.storage.from</code></td>
					<td>Meshed Flux consumers</td>
			</tr>
			<tr>
					<td>Metrics (<code>8083</code>)</td>
					<td>Yes — <code>serviceMesh.metrics.from</code></td>
					<td>Meshed Prometheus scrapers</td>
			</tr>
			<tr>
					<td>Webhook (<code>9443</code>)</td>
					<td>No — open carve-out</td>
					<td>The kube-apiserver is not in the mesh</td>
			</tr>
			<tr>
					<td>Management probes (<code>8081</code>)</td>
					<td>No — open carve-out</td>
					<td>The kubelet is not in the mesh</td>
			</tr>
	</tbody>
</table>
<p>For the full set of chart values, see
<a href="/installation/helm-values/">Helm chart values</a>
.</p>
]]></content><category scheme="https://jaas.projects.metio.wtf/tags/service-mesh" term="service-mesh" label="service-mesh"/><category scheme="https://jaas.projects.metio.wtf/tags/istio" term="istio" label="istio"/><category scheme="https://jaas.projects.metio.wtf/tags/linkerd" term="linkerd" label="linkerd"/><category scheme="https://jaas.projects.metio.wtf/tags/security" term="security" label="security"/><category scheme="https://jaas.projects.metio.wtf/tags/networking" term="networking" label="networking"/></entry><entry><title type="html">ServiceAccountMissing</title><link href="https://jaas.projects.metio.wtf/runbooks/serviceaccountmissing/?utm_source=atom_feed" rel="alternate" type="text/html"/><link href="https://jaas.projects.metio.wtf/runbooks/crossnamespacerefrejected/?utm_source=atom_feed" rel="related" type="text/html" title="CrossNamespaceRefRejected"/><link href="https://jaas.projects.metio.wtf/runbooks/librarynotfound/?utm_source=atom_feed" rel="related" type="text/html" title="LibraryNotFound"/><link href="https://jaas.projects.metio.wtf/runbooks/rbacdenied/?utm_source=atom_feed" rel="related" type="text/html" title="RBACDenied"/><link href="https://jaas.projects.metio.wtf/runbooks/operator-watch-silent/?utm_source=atom_feed" rel="related" type="text/html" title="Watch-layer silent failure"/><link href="https://jaas.projects.metio.wtf/runbooks/artifacttoolarge/?utm_source=atom_feed" rel="related" type="text/html" title="ArtifactTooLarge"/><id>https://jaas.projects.metio.wtf/runbooks/serviceaccountmissing/</id><published>0001-01-01T00:00:00+00:00</published><updated>2026-06-17T10:56:50+02:00</updated><content type="html"><![CDATA[<blockquote>The snippet specifies no ServiceAccount and the operator has no &ndash;default-service-account configured</blockquote><h2 id="symptom">Symptom</h2>
<p><code>READY=False</code>, <code>REASON=ServiceAccountMissing</code>.</p>
<h2 id="cause">Cause</h2>
<p>The snippet omitted <code>spec.serviceAccountName</code> AND the operator was started without <code>--default-service-account</code>. The operator refuses to reconcile a snippet with no effective ServiceAccount because every reconcile mints a tenant token from that SA — without one, there&rsquo;s nothing to impersonate.</p>
<h2 id="diagnosis">Diagnosis</h2>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">kubectl get jsonnetsnippet &lt;name&gt; --output <span class="nv">jsonpath</span><span class="o">=</span><span class="s1">&#39;{.spec.serviceAccountName}&#39;</span>
</span></span></code></pre></div><p>Empty? Either the snippet must set it, or the cluster operator must configure a default.</p>
<h2 id="remediation">Remediation</h2>
<p>Pick one:</p>
<ol>
<li><strong>Snippet-side (preferred for multi-tenant setups):</strong> set <code>spec.serviceAccountName: &lt;existing-sa&gt;</code> on every snippet. Each tenant uses its own SA → least-privilege impersonation.</li>
<li><strong>Cluster-side (single-tenant clusters):</strong> start the operator with <code>--default-service-account=&lt;sa-name&gt;</code>. Every snippet without an explicit SA impersonates this one. The default SA must exist in <strong>every snippet&rsquo;s namespace</strong> — the operator looks it up per-reconcile.</li>
</ol>
]]></content><category scheme="https://jaas.projects.metio.wtf/tags/runbooks" term="runbooks" label="runbooks"/><category scheme="https://jaas.projects.metio.wtf/tags/troubleshooting" term="troubleshooting" label="troubleshooting"/><category scheme="https://jaas.projects.metio.wtf/tags/rbac" term="rbac" label="rbac"/></entry><entry><title type="html">Snippet sources</title><link href="https://jaas.projects.metio.wtf/usage/snippet-sources/?utm_source=atom_feed" rel="alternate" type="text/html"/><link href="https://jaas.projects.metio.wtf/usage/creating-sources/?utm_source=atom_feed" rel="related" type="text/html" title="Creating source artifacts"/><link href="https://jaas.projects.metio.wtf/usage/admission-webhook/?utm_source=atom_feed" rel="related" type="text/html" title="Admission webhook"/><link href="https://jaas.projects.metio.wtf/usage/alerting/?utm_source=atom_feed" rel="related" type="text/html" title="Alerting"/><link href="https://jaas.projects.metio.wtf/runbooks/dependencycycle/?utm_source=atom_feed" rel="related" type="text/html" title="DependencyCycle"/><link href="https://jaas.projects.metio.wtf/api/externalartifact/?utm_source=atom_feed" rel="related" type="text/html" title="ExternalArtifact output contract"/><id>https://jaas.projects.metio.wtf/usage/snippet-sources/</id><published>0001-01-01T00:00:00+00:00</published><updated>2026-06-16T20:48:39+02:00</updated><content type="html"><![CDATA[<blockquote>Where a JsonnetSnippet&rsquo;s Jsonnet comes from — inline files, a Flux source, a multi-snippet tree, and chained snippet output.</blockquote><p>A <code>JsonnetSnippet</code> declares exactly one source for its Jsonnet bytes: either
inline <code>spec.files</code> or a <code>spec.sourceRef</code> pointing at a Flux source. Admission
rejects a snippet that sets both or neither. The operator resolves the source
into an in-memory file tree, evaluates <code>spec.entryFile</code> within it, and publishes
the result.</p>
<h2 id="inline-files">Inline files</h2>
<p><code>spec.files</code> is a map of filename to Jsonnet source. The operator evaluates the
entry file (<code>spec.entryFile</code>, default <code>main.jsonnet</code>) against the rest of the
map. This is the simplest source — the snippet is self-contained, with no
external dependency to fetch:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nt">apiVersion</span><span class="p">:</span><span class="w"> </span><span class="l">jaas.metio.wtf/v1</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">kind</span><span class="p">:</span><span class="w"> </span><span class="l">JsonnetSnippet</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">metadata</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">hello-world</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">namespace</span><span class="p">:</span><span class="w"> </span><span class="l">default</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">spec</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">serviceAccountName</span><span class="p">:</span><span class="w"> </span><span class="l">hello-world-tenant</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">entryFile</span><span class="p">:</span><span class="w"> </span><span class="l">main.jsonnet</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">files</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">main.jsonnet</span><span class="p">:</span><span class="w"> </span><span class="p">|</span><span class="sd">
</span></span></span><span class="line"><span class="cl"><span class="sd">      {
</span></span></span><span class="line"><span class="cl"><span class="sd">        greeting: &#39;hello&#39;,
</span></span></span><span class="line"><span class="cl"><span class="sd">        recipient: std.extVar(&#39;audience&#39;),
</span></span></span><span class="line"><span class="cl"><span class="sd">      }</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">externalVariables</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">audience</span><span class="p">:</span><span class="w"> </span><span class="l">world</span><span class="w">
</span></span></span></code></pre></div><h2 id="a-flux-source">A Flux source</h2>
<p><code>spec.sourceRef</code> points at a Flux source CR whose artifact tarball the operator
fetches and extracts into the snippet&rsquo;s file tree. The <code>kind</code> is one of
<code>GitRepository</code>, <code>OCIRepository</code>, <code>Bucket</code>, or <code>ExternalArtifact</code> — see Flux&rsquo;s
<a href="https://fluxcd.io/">source-controller documentation</a>
 for how each source CR
publishes its artifact.</p>
<p>When the referenced source republishes — a new commit lands on the
<code>GitRepository</code>, a new tag pushes to the <code>OCIRepository</code> — the operator&rsquo;s watch
on Flux source kinds re-queues the snippet and re-renders it. No <code>spec.interval</code>
is required for this; the watch is event-driven.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nt">apiVersion</span><span class="p">:</span><span class="w"> </span><span class="l">source.toolkit.fluxcd.io/v1</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">kind</span><span class="p">:</span><span class="w"> </span><span class="l">GitRepository</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">metadata</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">dashboards-source</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">namespace</span><span class="p">:</span><span class="w"> </span><span class="l">default</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">spec</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">interval</span><span class="p">:</span><span class="w"> </span><span class="l">5m</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">url</span><span class="p">:</span><span class="w"> </span><span class="l">https://github.com/example-org/grafana-dashboards</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">ref</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">branch</span><span class="p">:</span><span class="w"> </span><span class="l">main</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nn">---</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">apiVersion</span><span class="p">:</span><span class="w"> </span><span class="l">jaas.metio.wtf/v1</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">kind</span><span class="p">:</span><span class="w"> </span><span class="l">JsonnetSnippet</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">metadata</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">api-latency-dashboard</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">namespace</span><span class="p">:</span><span class="w"> </span><span class="l">default</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">spec</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">serviceAccountName</span><span class="p">:</span><span class="w"> </span><span class="l">dashboards-tenant</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">entryFile</span><span class="p">:</span><span class="w"> </span><span class="l">dashboards/api-latency.jsonnet</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">sourceRef</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">kind</span><span class="p">:</span><span class="w"> </span><span class="l">GitRepository</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">dashboards-source</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">path</span><span class="p">:</span><span class="w"> </span><span class="l">dashboards/</span><span class="w">
</span></span></span></code></pre></div><p><code>spec.sourceRef.path</code> narrows extraction to a subdirectory of the artifact&rsquo;s
tarball. Empty means the whole tree. The tenant ServiceAccount needs <code>get</code> on
the referenced source kind — see <a href="/usage/tenancy-and-rbac/">Tenancy and RBAC</a>
.</p>
<h3 id="the-entry-file-and-multi-snippet-trees">The entry file and multi-snippet trees</h3>
<p><code>spec.entryFile</code> names the file — relative to the resolved source root — that
go-jsonnet evaluates. It defaults to <code>main.jsonnet</code>. The field is restricted to
relative <code>[A-Za-z0-9._/-]+</code> paths with no <code>..</code> segments, so it cannot traverse
out of the extracted tree.</p>
<p>One Flux source often carries many snippets. A shared dashboards repository, for
example, holds one <code>.jsonnet</code> file per dashboard. Rather than one source per
dashboard, point several <code>JsonnetSnippet</code> resources at the same <code>GitRepository</code>
and give each a different <code>entryFile</code>:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nt">apiVersion</span><span class="p">:</span><span class="w"> </span><span class="l">jaas.metio.wtf/v1</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">kind</span><span class="p">:</span><span class="w"> </span><span class="l">JsonnetSnippet</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">metadata</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">api-latency-dashboard</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">namespace</span><span class="p">:</span><span class="w"> </span><span class="l">default</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">spec</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">serviceAccountName</span><span class="p">:</span><span class="w"> </span><span class="l">dashboards-tenant</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">entryFile</span><span class="p">:</span><span class="w"> </span><span class="l">dashboards/api-latency.jsonnet</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">sourceRef</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">kind</span><span class="p">:</span><span class="w"> </span><span class="l">GitRepository</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">dashboards-source</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nn">---</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">apiVersion</span><span class="p">:</span><span class="w"> </span><span class="l">jaas.metio.wtf/v1</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">kind</span><span class="p">:</span><span class="w"> </span><span class="l">JsonnetSnippet</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">metadata</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">error-budget-dashboard</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">namespace</span><span class="p">:</span><span class="w"> </span><span class="l">default</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">spec</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">serviceAccountName</span><span class="p">:</span><span class="w"> </span><span class="l">dashboards-tenant</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">entryFile</span><span class="p">:</span><span class="w"> </span><span class="l">dashboards/error-budget.jsonnet</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">sourceRef</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">kind</span><span class="p">:</span><span class="w"> </span><span class="l">GitRepository</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">dashboards-source</span><span class="w">
</span></span></span></code></pre></div><p>Both snippets share the source fetch and re-render together when the repository
republishes, but each publishes its own <code>ExternalArtifact</code> from its own entry
file.</p>
<h2 id="chaining-snippets">Chaining snippets</h2>
<p>A <code>JsonnetSnippet</code> can source from the <code>ExternalArtifact</code> another snippet
publishes. This composes a pipeline of renders: snippet A evaluates and
publishes its JSON, and snippet B takes that JSON as its input, transforms it,
and publishes a second artifact. A downstream consumer deploys only the final
artifact.</p>
<p>Chaining works because the <code>ExternalArtifact</code> is a Flux source like any other.
Snippet B sets <code>spec.sourceRef</code> with <code>kind: ExternalArtifact</code> and <code>name</code> pointing
at the producing snippet — an <code>ExternalArtifact</code> is published under the producing
<code>JsonnetSnippet</code>&rsquo;s name. The operator fetches A&rsquo;s artifact tarball into B&rsquo;s file
tree. In the default <code>rendered</code> output mode that tarball holds a single
<code>rendered.json</code>, so snippet B sets <code>entryFile: rendered.json</code> to evaluate A&rsquo;s
output. Because JSON is valid Jsonnet, B&rsquo;s entry file can extend the imported
object directly:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="c"># Snippet A renders a shared config blob other snippets consume.</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">apiVersion</span><span class="p">:</span><span class="w"> </span><span class="l">jaas.metio.wtf/v1</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">kind</span><span class="p">:</span><span class="w"> </span><span class="l">JsonnetSnippet</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">metadata</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">base-config</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">namespace</span><span class="p">:</span><span class="w"> </span><span class="l">default</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">spec</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">serviceAccountName</span><span class="p">:</span><span class="w"> </span><span class="l">chained-tenant</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">entryFile</span><span class="p">:</span><span class="w"> </span><span class="l">main.jsonnet</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">files</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">main.jsonnet</span><span class="p">:</span><span class="w"> </span><span class="p">|</span><span class="sd">
</span></span></span><span class="line"><span class="cl"><span class="sd">      {
</span></span></span><span class="line"><span class="cl"><span class="sd">        cluster: &#39;prod&#39;,
</span></span></span><span class="line"><span class="cl"><span class="sd">        region: &#39;eu-west-1&#39;,
</span></span></span><span class="line"><span class="cl"><span class="sd">        retentionDays: 30,
</span></span></span><span class="line"><span class="cl"><span class="sd">      }</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nn">---</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="c"># Snippet B sources from base-config&#39;s ExternalArtifact and extends it.</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">apiVersion</span><span class="p">:</span><span class="w"> </span><span class="l">jaas.metio.wtf/v1</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">kind</span><span class="p">:</span><span class="w"> </span><span class="l">JsonnetSnippet</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">metadata</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">derived-config</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">namespace</span><span class="p">:</span><span class="w"> </span><span class="l">default</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">spec</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">serviceAccountName</span><span class="p">:</span><span class="w"> </span><span class="l">chained-tenant</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">entryFile</span><span class="p">:</span><span class="w"> </span><span class="l">rendered.json</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">sourceRef</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">kind</span><span class="p">:</span><span class="w"> </span><span class="l">ExternalArtifact</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">base-config</span><span class="w">
</span></span></span></code></pre></div><p><code>derived-config</code> re-emits <code>base-config</code>&rsquo;s rendered JSON as its own artifact. The
operator&rsquo;s watch on <code>ExternalArtifact</code> updates re-queues <code>derived-config</code>
whenever <code>base-config</code> republishes, so the pipeline stays current end to end.</p>
<h3 id="source-variant">Source variant</h3>
<p>The rendered variant above passes evaluated JSON downstream. The source variant
passes raw Jsonnet downstream instead, for snippet B to re-evaluate itself.</p>
<p>Snippet A sets <code>spec.output: source</code>, so its <code>ExternalArtifact</code> carries A&rsquo;s raw
<code>.jsonnet</code> / <code>.libsonnet</code> files rather than the evaluated JSON. Snippet B points
<code>spec.sourceRef</code> at A&rsquo;s <code>ExternalArtifact</code> and imports A&rsquo;s files as Jsonnet,
re-evaluating them with B&rsquo;s own external variables, TLAs, and libraries. A
becomes a source that the pipeline produces dynamically rather than one an
operator authors by hand:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="c"># Snippet A publishes its raw Jsonnet, not its evaluated output.</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">apiVersion</span><span class="p">:</span><span class="w"> </span><span class="l">jaas.metio.wtf/v1</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">kind</span><span class="p">:</span><span class="w"> </span><span class="l">JsonnetSnippet</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">metadata</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">dashboard-template</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">namespace</span><span class="p">:</span><span class="w"> </span><span class="l">default</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">spec</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">serviceAccountName</span><span class="p">:</span><span class="w"> </span><span class="l">chained-tenant</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">output</span><span class="p">:</span><span class="w"> </span><span class="l">source</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">entryFile</span><span class="p">:</span><span class="w"> </span><span class="l">main.jsonnet</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">files</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">main.jsonnet</span><span class="p">:</span><span class="w"> </span><span class="p">|</span><span class="sd">
</span></span></span><span class="line"><span class="cl"><span class="sd">      function(env=&#39;dev&#39;) {
</span></span></span><span class="line"><span class="cl"><span class="sd">        title: &#39;API latency — &#39; + env,
</span></span></span><span class="line"><span class="cl"><span class="sd">        refresh: if env == &#39;prod&#39; then &#39;30s&#39; else &#39;5m&#39;,
</span></span></span><span class="line"><span class="cl"><span class="sd">      }</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nn">---</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="c"># Snippet B sources A&#39;s raw Jsonnet and re-evaluates it with its own TLAs.</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">apiVersion</span><span class="p">:</span><span class="w"> </span><span class="l">jaas.metio.wtf/v1</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">kind</span><span class="p">:</span><span class="w"> </span><span class="l">JsonnetSnippet</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">metadata</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">dashboard-prod</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">namespace</span><span class="p">:</span><span class="w"> </span><span class="l">default</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">spec</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">serviceAccountName</span><span class="p">:</span><span class="w"> </span><span class="l">chained-tenant</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">entryFile</span><span class="p">:</span><span class="w"> </span><span class="l">main.jsonnet</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">sourceRef</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">kind</span><span class="p">:</span><span class="w"> </span><span class="l">ExternalArtifact</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">dashboard-template</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">tlas</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">env</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">prod</span><span class="w">
</span></span></span></code></pre></div><p>Because A published <code>source</code> output, B&rsquo;s <code>sourceRef</code> extracts A&rsquo;s raw
<code>main.jsonnet</code> into B&rsquo;s file tree, and B evaluates it as the entry file with
<code>env=prod</code> supplied as a TLA. When A&rsquo;s template changes, A republishes and the
<code>ExternalArtifact</code> watch re-renders B against the new Jsonnet.</p>
<p>Choose between the two by what the downstream snippet needs: rendered chaining
passes JSON data downstream; source chaining passes Jsonnet to be re-evaluated
downstream. For when to reach for a <code>source</code>-output snippet instead of a
<code>JsonnetLibrary</code>, see
<a href="/usage/jsonnet-libraries/#jsonnetlibrary-vs-a-source-output-snippet">JsonnetLibrary vs a source-output snippet</a>
.</p>
<p>The tenant ServiceAccount needs <code>get</code> on
<code>externalartifacts.source.toolkit.fluxcd.io</code> for both variants; see
<a href="/usage/tenancy-and-rbac/">Tenancy and RBAC</a>
.</p>
<h3 id="cycle-detection">Cycle detection</h3>
<p>A snippet cannot transitively depend on itself. The operator walks the
dependency graph — <code>spec.sourceRef</code> edges to <code>ExternalArtifact</code>s and their
producing snippets, plus <code>spec.libraries</code> edges through <code>JsonnetLibrary</code>
<code>sourceRef</code>s — at reconcile time, before any tenant work. If the walk revisits
the snippet it started from, the operator refuses to publish and reports
<code>Ready=False</code> with reason <code>DependencyCycle</code>. This catches a chain that feeds
back into itself directly or through a library, so a cycle surfaces as a clear
status condition rather than an endless re-render loop.</p>
<h2 id="related-pages">Related pages</h2>
<ul>
<li><a href="/usage/jsonnet-libraries/">Jsonnet libraries</a>
 — reusable <code>.libsonnet</code> files
referenced via <code>spec.libraries</code>.</li>
<li><a href="/usage/tenancy-and-rbac/">Tenancy and RBAC</a>
 — the verbs the tenant
ServiceAccount needs for each source kind.</li>
</ul>
]]></content><category scheme="https://jaas.projects.metio.wtf/tags/operator" term="operator" label="operator"/><category scheme="https://jaas.projects.metio.wtf/tags/sources" term="sources" label="sources"/><category scheme="https://jaas.projects.metio.wtf/tags/chaining" term="chaining" label="chaining"/></entry><entry><title type="html">Snippets and libraries</title><link href="https://jaas.projects.metio.wtf/usage/snippets-and-libraries/?utm_source=atom_feed" rel="alternate" type="text/html"/><link href="https://jaas.projects.metio.wtf/usage/jsonnet-libraries/?utm_source=atom_feed" rel="related" type="text/html" title="Jsonnet libraries"/><link href="https://jaas.projects.metio.wtf/api/externalartifact/?utm_source=atom_feed" rel="related" type="text/html" title="ExternalArtifact output contract"/><link href="https://jaas.projects.metio.wtf/usage/joi-images/?utm_source=atom_feed" rel="related" type="text/html" title="JOI images"/><link href="https://jaas.projects.metio.wtf/api/jsonnetlibrary/?utm_source=atom_feed" rel="related" type="text/html" title="JsonnetLibrary"/><link href="https://jaas.projects.metio.wtf/api/jsonnetsnippet/?utm_source=atom_feed" rel="related" type="text/html" title="JsonnetSnippet"/><id>https://jaas.projects.metio.wtf/usage/snippets-and-libraries/</id><published>0001-01-01T00:00:00+00:00</published><updated>2026-06-16T20:48:39+02:00</updated><content type="html"><![CDATA[<blockquote>Declaring snippets and libraries on disk for the HTTP renderer, and how imports resolve.</blockquote><p>In renderer mode you declare snippets and libraries on disk through command-line
flags. A snippet becomes reachable at the
<a href="/usage/rendering-endpoint/">rendering endpoint</a>
; a library is importable by any
snippet.</p>
<h2 id="directory-snippets">Directory snippets</h2>
<p>Point <code>--snippet-directory</code> at a directory whose subdirectories each hold a
<code>main.jsonnet</code>. Each subdirectory name becomes a snippet name:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">./jaas --snippet-directory examples/snippets/dashboards
</span></span><span class="line"><span class="cl">curl http://127.0.0.1:8080/jsonnet/example1
</span></span></code></pre></div><p>Given this layout, <code>example1</code> resolves to
<code>examples/snippets/dashboards/example1/main.jsonnet</code>:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">examples/snippets/dashboards
</span></span><span class="line"><span class="cl">├── example1
</span></span><span class="line"><span class="cl">│   └── main.jsonnet
</span></span><span class="line"><span class="cl">├── tla-example
</span></span><span class="line"><span class="cl">│   └── main.jsonnet
</span></span><span class="line"><span class="cl">└── multi-tla
</span></span><span class="line"><span class="cl">    └── main.jsonnet
</span></span></code></pre></div><h2 id="file-snippets">File snippets</h2>
<p>Point <code>--snippet</code> at an individual Jsonnet file. The file path becomes the
snippet name:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">./jaas --snippet examples/snippets/example.jsonnet
</span></span><span class="line"><span class="cl">curl http://127.0.0.1:8080/jsonnet/examples/snippets/example.jsonnet
</span></span></code></pre></div><p>Both <code>--snippet</code> and <code>--snippet-directory</code> are repeatable, so one process serves
several roots:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">./jaas <span class="se">\
</span></span></span><span class="line"><span class="cl">  --snippet-directory examples/snippets/dashboards <span class="se">\
</span></span></span><span class="line"><span class="cl">  --snippet examples/snippets/example.jsonnet
</span></span></code></pre></div><h2 id="libraries">Libraries</h2>
<p>Point <code>--library-path</code> at a directory that holds importable Jsonnet libraries. A
snippet imports a library by its path under that directory:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">examples/libraries
</span></span><span class="line"><span class="cl">├── examplonet
</span></span><span class="line"><span class="cl">│   └── main.libsonnet
</span></span><span class="line"><span class="cl">└── text
</span></span><span class="line"><span class="cl">    └── welcome.txt
</span></span></code></pre></div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">./jaas <span class="se">\
</span></span></span><span class="line"><span class="cl">  --snippet-directory examples/snippets/dashboards <span class="se">\
</span></span></span><span class="line"><span class="cl">  --library-path examples/libraries
</span></span></code></pre></div><p>A snippet then imports the library with a string-literal path:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-jsonnet" data-lang="jsonnet"><span class="line"><span class="cl"><span class="k">local</span><span class="w"> </span><span class="nv">examplonet</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="k">import</span><span class="w"> </span><span class="s">&#39;examplonet/main.libsonnet&#39;</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nv">person1</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nv">name</span><span class="p">:</span><span class="w"> </span><span class="nv">examplonet</span><span class="p">.</span><span class="nv">standard</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nv">welcome</span><span class="p">:</span><span class="w"> </span><span class="s">&#39;Hello &#39;</span><span class="w"> </span><span class="o">+</span><span class="w"> </span><span class="k">self</span><span class="p">.</span><span class="nv">name</span><span class="w"> </span><span class="o">+</span><span class="w"> </span><span class="s">&#39;!&#39;</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="p">},</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="p">}</span><span class="w">
</span></span></span></code></pre></div><p><code>--library-path</code> is repeatable. When the same import path matches under more than
one library directory, the rightmost matching directory wins — list the override
directory last.</p>
<h2 id="embedding-non-jsonnet-files">Embedding non-Jsonnet files</h2>
<p>Use <code>importstr</code> to pull the raw contents of a file under a library path into a
snippet as a string. The <code>embed-text</code> example reads a text file from the
<code>text/</code> library:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-jsonnet" data-lang="jsonnet"><span class="line"><span class="cl"><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nv">banner</span><span class="p">:</span><span class="w"> </span><span class="k">importstr</span><span class="w"> </span><span class="s">&#39;text/welcome.txt&#39;</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nv">length</span><span class="p">:</span><span class="w"> </span><span class="nb">std.length</span><span class="p">(</span><span class="k">self</span><span class="p">.</span><span class="nv">banner</span><span class="p">),</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="p">}</span><span class="w">
</span></span></span></code></pre></div><p>Any file reachable under a <code>--library-path</code> directory or the snippet&rsquo;s own
directory can be <code>import</code>-ed or <code>importstr</code>-ed by any snippet. Scope these
directories tightly — see
<a href="/usage/evaluation-and-security/">Evaluation and security</a>
.</p>
<p>For the operator-side equivalent — <code>JsonnetLibrary</code> CRDs and OCI-mounted shared
libraries — see <a href="/usage/jsonnet-libraries/">Jsonnet libraries</a>
.</p>
]]></content><category scheme="https://jaas.projects.metio.wtf/tags/snippets" term="snippets" label="snippets"/><category scheme="https://jaas.projects.metio.wtf/tags/libraries" term="libraries" label="libraries"/><category scheme="https://jaas.projects.metio.wtf/tags/imports" term="imports" label="imports"/></entry><entry><title type="html">SourceFetchFailed</title><link href="https://jaas.projects.metio.wtf/runbooks/sourcefetchfailed/?utm_source=atom_feed" rel="alternate" type="text/html"/><link href="https://jaas.projects.metio.wtf/runbooks/dependencycycle/?utm_source=atom_feed" rel="related" type="text/html" title="DependencyCycle"/><link href="https://jaas.projects.metio.wtf/runbooks/sourcenotready/?utm_source=atom_feed" rel="related" type="text/html" title="SourceNotReady"/><link href="https://jaas.projects.metio.wtf/runbooks/sourcerefnotyetsupported/?utm_source=atom_feed" rel="related" type="text/html" title="SourceRefNotYetSupported"/><link href="https://jaas.projects.metio.wtf/runbooks/artifacttoolarge/?utm_source=atom_feed" rel="related" type="text/html" title="ArtifactTooLarge"/><link href="https://jaas.projects.metio.wtf/runbooks/crd-watch-engagement/?utm_source=atom_feed" rel="related" type="text/html" title="CRD watch engagement failing"/><id>https://jaas.projects.metio.wtf/runbooks/sourcefetchfailed/</id><published>0001-01-01T00:00:00+00:00</published><updated>2026-06-16T20:48:39+02:00</updated><content type="html"><![CDATA[<blockquote>The operator resolved the source CR but the artifact download failed due to an HTTP error, digest mismatch, or oversized tarball</blockquote><h2 id="symptom">Symptom</h2>
<p><code>READY=False</code>, <code>REASON=SourceFetchFailed</code>. The Message describes what went wrong (HTTP error, digest mismatch, tarball too large, etc.).</p>
<h2 id="cause">Cause</h2>
<p>The Fetcher resolved the source CR and started downloading the artifact, but the download itself failed. Three subcategories:</p>
<ul>
<li><strong>HTTP failure</strong> — connection refused, 5xx from the source-controller endpoint, TLS handshake error</li>
<li><strong>Digest mismatch</strong> — the bytes don&rsquo;t hash to <code>status.artifact.digest</code>. Possible truncation or in-flight tampering</li>
<li><strong>Tarball oversized</strong> — extracted bytes exceed <code>MaxArchiveBytes</code> (default 64 MiB)</li>
</ul>
<h2 id="diagnosis">Diagnosis</h2>
<p>Check the source CR&rsquo;s <code>status.artifact.url</code> is reachable from the operator pod:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">kubectl <span class="nb">exec</span> deploy/jaas -- wget -O- &lt;status.artifact.url&gt; <span class="p">|</span> wc -c
</span></span></code></pre></div><p>A connection refused means the storage endpoint of source-controller (or another publisher) is unreachable — usually a NetworkPolicy issue.</p>
<p>For digest mismatches, the source CR has likely been republished mid-fetch — the next reconcile typically succeeds.</p>
<p>For oversized tarballs, the snippet&rsquo;s <code>spec.sourceRef.path</code> filter is too broad — narrow it so only the files the snippet actually <code>import</code>s come through.</p>
<h2 id="remediation">Remediation</h2>
<ul>
<li><strong>Network</strong>: fix the NetworkPolicy / DNS / TLS that&rsquo;s blocking the fetch</li>
<li><strong>Digest</strong>: re-reconcile (manual: <code>kubectl annotate jsonnetsnippet &lt;name&gt; jaas.metio.wtf/reconcile-at=$(date -u +%FT%TZ) --overwrite</code>)</li>
<li><strong>Oversized</strong>: narrow <code>spec.sourceRef.path</code> to the subdirectory the snippet needs, or split the source repo</li>
</ul>
]]></content><category scheme="https://jaas.projects.metio.wtf/tags/runbooks" term="runbooks" label="runbooks"/><category scheme="https://jaas.projects.metio.wtf/tags/troubleshooting" term="troubleshooting" label="troubleshooting"/><category scheme="https://jaas.projects.metio.wtf/tags/sources" term="sources" label="sources"/></entry><entry><title type="html">SourceNotReady</title><link href="https://jaas.projects.metio.wtf/runbooks/sourcenotready/?utm_source=atom_feed" rel="alternate" type="text/html"/><link href="https://jaas.projects.metio.wtf/runbooks/dependencycycle/?utm_source=atom_feed" rel="related" type="text/html" title="DependencyCycle"/><link href="https://jaas.projects.metio.wtf/runbooks/sourcefetchfailed/?utm_source=atom_feed" rel="related" type="text/html" title="SourceFetchFailed"/><link href="https://jaas.projects.metio.wtf/runbooks/sourcerefnotyetsupported/?utm_source=atom_feed" rel="related" type="text/html" title="SourceRefNotYetSupported"/><link href="https://jaas.projects.metio.wtf/runbooks/artifacttoolarge/?utm_source=atom_feed" rel="related" type="text/html" title="ArtifactTooLarge"/><link href="https://jaas.projects.metio.wtf/runbooks/crd-watch-engagement/?utm_source=atom_feed" rel="related" type="text/html" title="CRD watch engagement failing"/><id>https://jaas.projects.metio.wtf/runbooks/sourcenotready/</id><published>0001-01-01T00:00:00+00:00</published><updated>2026-06-17T10:56:50+02:00</updated><content type="html"><![CDATA[<blockquote>The referenced Flux source CR exists but has not yet reported Ready=True or has no published artifact</blockquote><h2 id="symptom">Symptom</h2>
<p><code>READY=False</code>, <code>REASON=SourceNotReady</code>. The Message names the source CR (<code>GitRepository/foo</code>, <code>ExternalArtifact/bar</code>, etc.).</p>
<h2 id="cause">Cause</h2>
<p>The Flux source CR the snippet references exists but its own <code>status.conditions[Ready]</code> is not yet True (or <code>status.artifact</code> is empty). The operator refuses to fetch from a source it can&rsquo;t trust as ready.</p>
<p>For chained snippets specifically: the upstream snippet may have failed reconciliation, so its ExternalArtifact is stale or unpopulated.</p>
<h2 id="diagnosis">Diagnosis</h2>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">kubectl describe &lt;kind&gt; &lt;source-name&gt;
</span></span><span class="line"><span class="cl"><span class="c1"># Look for the Ready condition and any error messages.</span>
</span></span></code></pre></div><p>For Flux sources, also check the source-controller logs:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">kubectl --namespace flux-system logs deploy/source-controller <span class="p">|</span> grep &lt;source-name&gt;
</span></span></code></pre></div><h2 id="remediation">Remediation</h2>
<p>Fix the upstream source. The operator watches Flux source kinds and will re-reconcile the snippet automatically when the source flips to Ready=True — no manual nudge required.</p>
]]></content><category scheme="https://jaas.projects.metio.wtf/tags/runbooks" term="runbooks" label="runbooks"/><category scheme="https://jaas.projects.metio.wtf/tags/troubleshooting" term="troubleshooting" label="troubleshooting"/><category scheme="https://jaas.projects.metio.wtf/tags/sources" term="sources" label="sources"/></entry><entry><title type="html">SourceRefNotYetSupported</title><link href="https://jaas.projects.metio.wtf/runbooks/sourcerefnotyetsupported/?utm_source=atom_feed" rel="alternate" type="text/html"/><link href="https://jaas.projects.metio.wtf/runbooks/dependencycycle/?utm_source=atom_feed" rel="related" type="text/html" title="DependencyCycle"/><link href="https://jaas.projects.metio.wtf/runbooks/sourcefetchfailed/?utm_source=atom_feed" rel="related" type="text/html" title="SourceFetchFailed"/><link href="https://jaas.projects.metio.wtf/runbooks/sourcenotready/?utm_source=atom_feed" rel="related" type="text/html" title="SourceNotReady"/><link href="https://jaas.projects.metio.wtf/runbooks/artifacttoolarge/?utm_source=atom_feed" rel="related" type="text/html" title="ArtifactTooLarge"/><link href="https://jaas.projects.metio.wtf/runbooks/crd-watch-engagement/?utm_source=atom_feed" rel="related" type="text/html" title="CRD watch engagement failing"/><id>https://jaas.projects.metio.wtf/runbooks/sourcerefnotyetsupported/</id><published>0001-01-01T00:00:00+00:00</published><updated>2026-06-16T20:48:39+02:00</updated><content type="html"><![CDATA[<blockquote>The snippet uses spec.sourceRef but the running binary was built without a Fetcher wired in</blockquote><h2 id="symptom">Symptom</h2>
<p><code>READY=False</code>, <code>REASON=SourceRefNotYetSupported</code>.</p>
<h2 id="cause">Cause</h2>
<p>The snippet sets <code>spec.sourceRef</code> but the operator was built without a Fetcher wired in. This is a mis-deployment in practice — production binary always wires <code>sources.New()</code>. Seeing this in a real cluster means you&rsquo;re running:</p>
<ul>
<li>a test/dev binary</li>
<li>a custom build where <code>defaultBuilder</code> was modified</li>
<li>a future code path that hasn&rsquo;t enabled sourceRef yet</li>
</ul>
<h2 id="diagnosis">Diagnosis</h2>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">kubectl logs deploy/jaas <span class="p">|</span> grep -i <span class="s2">&#34;fetcher&#34;</span>
</span></span></code></pre></div><p>If the operator logs no Fetcher initialization, the binary is incomplete.</p>
<h2 id="remediation">Remediation</h2>
<p>Use a release binary, or convert the snippet to <code>spec.files</code> inline as a temporary workaround.</p>
]]></content><category scheme="https://jaas.projects.metio.wtf/tags/runbooks" term="runbooks" label="runbooks"/><category scheme="https://jaas.projects.metio.wtf/tags/troubleshooting" term="troubleshooting" label="troubleshooting"/><category scheme="https://jaas.projects.metio.wtf/tags/sources" term="sources" label="sources"/></entry><entry><title type="html">Storage and high availability</title><link href="https://jaas.projects.metio.wtf/usage/storage-and-ha/?utm_source=atom_feed" rel="alternate" type="text/html"/><link href="https://jaas.projects.metio.wtf/usage/admission-webhook/?utm_source=atom_feed" rel="related" type="text/html" title="Admission webhook"/><link href="https://jaas.projects.metio.wtf/usage/alerting/?utm_source=atom_feed" rel="related" type="text/html" title="Alerting"/><link href="https://jaas.projects.metio.wtf/runbooks/artifacttoolarge/?utm_source=atom_feed" rel="related" type="text/html" title="ArtifactTooLarge"/><link href="https://jaas.projects.metio.wtf/usage/creating-sources/?utm_source=atom_feed" rel="related" type="text/html" title="Creating source artifacts"/><link href="https://jaas.projects.metio.wtf/api/externalartifact/?utm_source=atom_feed" rel="related" type="text/html" title="ExternalArtifact output contract"/><id>https://jaas.projects.metio.wtf/usage/storage-and-ha/</id><published>0001-01-01T00:00:00+00:00</published><updated>2026-06-17T16:04:27+02:00</updated><content type="html"><![CDATA[<blockquote>The local and S3 artifact backends, leader election, multi-replica HA, revision retention, and the orphan-tmp sweep.</blockquote><p>In <a href="/usage/operator-mode/">operator mode</a>
 JaaS renders each <code>JsonnetSnippet</code> into
a tar.gz artifact, stores it, and publishes an <code>ExternalArtifact</code> CR that points a
Flux consumer at the tarball over HTTP. JaaS publishes artifacts through one of two
storage backends — local filesystem or S3-compatible object storage — with optional
leader election for multi-replica high availability and configurable revision
retention.</p>
<h2 id="serving-the-tarballs">Serving the tarballs</h2>
<p>Regardless of backend, the operator runs an HTTP server that Flux consumers fetch
artifacts from. Three flags govern it, and <code>--storage-base-url</code> and
<code>--storage-path</code> are required whenever <code>--enable-flux-integration</code> is set:</p>
<ul>
<li><code>--storage-base-url</code> — the public URL prefix stamped into each
<code>ExternalArtifact</code>&rsquo;s <code>status.artifact.url</code>. This is what downstream Flux
controllers dial, so it must be reachable from them.</li>
<li><code>--storage-listen-address</code> (default <code>0.0.0.0</code>) and <code>--storage-port</code> (default
<code>8082</code>) — the bind address of the storage HTTP server.</li>
</ul>
<h2 id="local-backend">Local backend</h2>
<p><code>--storage-backend=local</code> (the default) writes tarballs to the filesystem under
<code>--storage-path</code>. The Helm chart pairs this with an <code>emptyDir</code> by default, or a
<code>PersistentVolumeClaim</code> when <code>operator.storage.persistence.enabled: true</code>.</p>
<p>A ReadWriteOnce PVC caps the install at a single replica, because only one pod can
mount the volume for writing. If you need more than one replica, use the S3
backend below.</p>
<h2 id="s3-backend">S3 backend</h2>
<p><code>--storage-backend=s3</code> stores tarballs in any S3-compatible bucket (AWS S3, MinIO,
Ceph RGW, Backblaze B2, and similar). The bucket must already exist. Configure it
with:</p>
<table>
	<thead>
			<tr>
					<th>Flag</th>
					<th>Purpose</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td><code>--s3-endpoint</code></td>
					<td>S3 host:port, e.g. <code>s3.amazonaws.com</code> or <code>minio.minio.svc:9000</code>. Required.</td>
			</tr>
			<tr>
					<td><code>--s3-bucket</code></td>
					<td>Bucket the artifacts live in. Required.</td>
			</tr>
			<tr>
					<td><code>--s3-prefix</code></td>
					<td>Optional key prefix so JaaS can coexist with other tenants in one bucket.</td>
			</tr>
			<tr>
					<td><code>--s3-region</code></td>
					<td>Region the bucket lives in. Required for AWS multi-region; ignored by most other servers.</td>
			</tr>
			<tr>
					<td><code>--s3-use-ssl</code></td>
					<td>Talk HTTPS to the endpoint (default <code>true</code>). Set <code>false</code> only for local MinIO over HTTP.</td>
			</tr>
			<tr>
					<td><code>--s3-access-key</code></td>
					<td>Static access key ID.</td>
			</tr>
			<tr>
					<td><code>--s3-secret-key</code></td>
					<td>Static secret access key, paired with <code>--s3-access-key</code>.</td>
			</tr>
			<tr>
					<td><code>--s3-session-token</code></td>
					<td>Optional session token for temporary credentials.</td>
			</tr>
			<tr>
					<td><code>--s3-anonymous</code></td>
					<td>Skip request signing entirely; only for a public bucket, test and dev only.</td>
			</tr>
	</tbody>
</table>
<p>Leave <code>--s3-access-key</code> and <code>--s3-secret-key</code> empty to engage the IAM/IRSA
discovery chain — environment credentials, web-identity tokens, and EC2/EKS
instance metadata — so a pod running with an IRSA-annotated ServiceAccount needs
no static keys.</p>
<h3 id="bring-your-own-secret">Bring your own Secret</h3>
<p>The chart never bakes credentials into a rendered Secret. It references a Secret
you provide by name (<code>operator.storage.s3.credentialsSecret.name</code>) and consumes it
via <code>envFrom</code>, expecting the keys <code>AWS_ACCESS_KEY_ID</code>, <code>AWS_SECRET_ACCESS_KEY</code>, and
the optional <code>AWS_SESSION_TOKEN</code>. The Secret&rsquo;s provenance is yours to choose.</p>
<p>Any of these can produce that Secret, and the chart works with all of them
unchanged — point the tool at the same name the chart references:</p>
<ul>
<li><strong>External Secrets Operator</strong> — an <code>ExternalSecret</code> that syncs from Vault, AWS
Secrets Manager, GCP Secret Manager, or Azure Key Vault into a Secret of that
name.</li>
<li><strong>Sealed Secrets</strong> — a <code>SealedSecret</code> that the controller decrypts in-cluster.</li>
<li><strong>Vault Agent / CSI</strong> — a Secret materialized from Vault.</li>
<li><strong>SOPS</strong> — a Secret decrypted by your GitOps tooling at apply time.</li>
<li><strong><code>kubectl create secret</code></strong> — a plain hand-managed Secret.</li>
</ul>
<p>This is why the chart ships no native <code>ExternalSecret</code> resource: the reference seam
already integrates with every secret backend, without coupling the chart to one
operator&rsquo;s CRDs.</p>
<p>On the cloud, <strong>IAM/IRSA</strong> — leaving the credentials Secret unset (above) — avoids
a stored secret entirely and is preferred where available.</p>
<p>A minimal External Secrets example whose <code>target.name</code> matches the referenced
Secret and whose keys are the ones JaaS expects:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nt">apiVersion</span><span class="p">:</span><span class="w"> </span><span class="l">external-secrets.io/v1</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">kind</span><span class="p">:</span><span class="w"> </span><span class="l">ExternalSecret</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">metadata</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">jaas-s3</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">spec</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">refreshInterval</span><span class="p">:</span><span class="w"> </span><span class="l">1h</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">secretStoreRef</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">vault</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">kind</span><span class="p">:</span><span class="w"> </span><span class="l">SecretStore</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">target</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">jaas-s3-credentials</span><span class="w"> </span><span class="c"># = operator.storage.s3.credentialsSecret.name</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">data</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span>- <span class="nt">secretKey</span><span class="p">:</span><span class="w"> </span><span class="l">AWS_ACCESS_KEY_ID</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">remoteRef</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">key</span><span class="p">:</span><span class="w"> </span><span class="l">jaas/s3</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">property</span><span class="p">:</span><span class="w"> </span><span class="l">access_key_id</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span>- <span class="nt">secretKey</span><span class="p">:</span><span class="w"> </span><span class="l">AWS_SECRET_ACCESS_KEY</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">remoteRef</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">key</span><span class="p">:</span><span class="w"> </span><span class="l">jaas/s3</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">property</span><span class="p">:</span><span class="w"> </span><span class="l">secret_access_key</span><span class="w">
</span></span></span></code></pre></div><p>For the full chart values, see the <a href="/installation/helm-values/">Helm values</a>

reference.</p>
<h2 id="leader-election">Leader election</h2>
<p>Leader election is on by default in operator mode (<code>--leader-election</code>, honored
only when <code>--enable-flux-integration</code> is set). The lease lets exactly one replica
reconcile at a time. On <code>SIGTERM</code> during a rolling update the lease is released
immediately rather than waiting out the 15-second lease duration, so the next
replica picks up reconciliation within seconds.</p>
<p>Set <code>--leader-election=false</code> only when running a single replica with no rollout
overlap.</p>
<h2 id="multi-replica-ha">Multi-replica HA</h2>
<p>High availability is the S3 backend plus leader election: every replica reads from
the same bucket, and only the lease-holder writes. No ReadWriteMany storage class
is required. During a rolling update the lease hands over on <code>SIGTERM</code>, so the
write path moves to the new leader without a manual step.</p>
<h2 id="revision-retention-and-rollback">Revision retention and rollback</h2>
<p><code>spec.history</code> on a <code>JsonnetSnippet</code> (default <code>1</code>, maximum <code>50</code>) keeps the last N
rendered revisions in storage. Downstream consumers can pin to an older <code>sha256</code>
for rollback or blue-green cutover, instead of always tracking the newest render.</p>
<p>Two flags shape how superseded revisions age out:</p>
<ul>
<li><code>--artifact-gc-grace</code> (default <code>5m</code>) retains a revision for a short window after
it leaves the keep-set. This closes the pin→fetch race in which a Flux consumer
reads <code>status.artifact</code> a moment before the operator garbage-collects the
revision that consumer pinned. Set it to <code>0</code> to disable the grace and restore
eager pruning. Snippet teardown (the deletion path) is unaffected by this flag.</li>
<li><code>--max-artifact-bytes</code> (default <code>0</code>, disabled) caps the rendered artifact size in
bytes. A snippet whose render exceeds the cap fails with <code>ReasonArtifactTooLarge</code>
rather than publishing an oversized tarball.</li>
</ul>
<h2 id="orphan-tmp-sweep">Orphan-tmp sweep</h2>
<p>This is a local-backend concern only. On the filesystem backend a <code>Put</code> that
dies after writing the temporary file but before the atomic rename leaves a
<code>&lt;rev&gt;.tar.gz.tmp</code> residue, and a background sweep removes it. The S3 backend has
no such residue — <code>PutObject</code> is atomic — so its sweep is a no-op:</p>
<ul>
<li><code>--storage-sweep-interval</code> (default <code>10m</code>) — how often the sweep runs. <code>0</code>
disables it.</li>
<li><code>--storage-sweep-max-tmp-age</code> (default <code>30m</code>) — the minimum age before an
orphaned <code>.tmp</code> file is eligible, set wider than the longest plausible in-flight
<code>Put</code> so the sweep never races a live writer.</li>
</ul>
<p>For production sizing of these knobs, see the
<a href="/installation/production/">production guide</a>
. The full flag list with defaults is
on the <a href="/installation/configuration/">configuration page</a>
.</p>
]]></content><category scheme="https://jaas.projects.metio.wtf/tags/operator" term="operator" label="operator"/><category scheme="https://jaas.projects.metio.wtf/tags/storage" term="storage" label="storage"/><category scheme="https://jaas.projects.metio.wtf/tags/ha" term="ha" label="ha"/></entry><entry><title type="html">Storage backend recovery</title><link href="https://jaas.projects.metio.wtf/runbooks/storage-recovery/?utm_source=atom_feed" rel="alternate" type="text/html"/><link href="https://jaas.projects.metio.wtf/runbooks/artifacttoolarge/?utm_source=atom_feed" rel="related" type="text/html" title="ArtifactTooLarge"/><link href="https://jaas.projects.metio.wtf/runbooks/crd-watch-engagement/?utm_source=atom_feed" rel="related" type="text/html" title="CRD watch engagement failing"/><link href="https://jaas.projects.metio.wtf/runbooks/crossnamespacerefrejected/?utm_source=atom_feed" rel="related" type="text/html" title="CrossNamespaceRefRejected"/><link href="https://jaas.projects.metio.wtf/runbooks/dependencycycle/?utm_source=atom_feed" rel="related" type="text/html" title="DependencyCycle"/><link href="https://jaas.projects.metio.wtf/runbooks/eval-saturation/?utm_source=atom_feed" rel="related" type="text/html" title="Eval-concurrency saturation"/><id>https://jaas.projects.metio.wtf/runbooks/storage-recovery/</id><published>0001-01-01T00:00:00+00:00</published><updated>2026-06-17T10:56:50+02:00</updated><content type="html"><![CDATA[<blockquote>The artifact store is degraded (PVC lost, S3 endpoint down, or storage HTTP server unreachable) and downstream Flux consumers can no longer fetch tarballs</blockquote><p>Not tied to a single <code>Reason</code> — this page covers what to do when the artifact store itself is degraded (PVC lost, S3 endpoint unavailable, the storage HTTP server is down). Downstream Flux consumers (kustomize-controller, helm-controller, grafana-operator) dereference <code>ExternalArtifact.status.artifact.url</code> to fetch tarballs; when that URL stops returning bytes, dependent resources stall.</p>
<h2 id="symptom">Symptom</h2>
<p>One or more of:</p>
<ul>
<li>Downstream Flux consumers report <code>404 Not Found</code> or <code>connection refused</code> against the JaaS storage URL.</li>
<li><code>kubectl get externalartifact --all-namespaces</code> shows resources whose URL is unreachable from the consumer pods.</li>
<li>The operator pod is healthy (Ready=True on snippets), but the storage Service is unresponsive.</li>
<li><code>helm upgrade</code> of the chart from <code>persistence.enabled: false</code> to <code>true</code> — or vice versa — caused a gap.</li>
</ul>
<h2 id="triage-which-backend-are-you-running">Triage: which backend are you running?</h2>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">kubectl --namespace &lt;jaas-ns&gt; get deploy jaas <span class="se">\
</span></span></span><span class="line"><span class="cl">  --output <span class="nv">jsonpath</span><span class="o">=</span><span class="s1">&#39;{.spec.template.spec.containers[0].args}&#39;</span> <span class="se">\
</span></span></span><span class="line"><span class="cl">  <span class="p">|</span> tr <span class="s1">&#39;,&#39;</span> <span class="s1">&#39;\n&#39;</span> <span class="p">|</span> grep -E <span class="s1">&#39;storage-backend|storage-path|s3-endpoint&#39;</span>
</span></span></code></pre></div><ul>
<li><code>--storage-backend=local</code> → filesystem behind <code>--storage-path</code>. Either an emptyDir (chart default) or a PVC.</li>
<li><code>--storage-backend=s3</code> → an external S3-compatible bucket; the storage HTTP server in-pod is a thin streaming proxy over <code>minio-go</code>.</li>
</ul>
<h2 id="filesystem-backend">Filesystem backend</h2>
<h3 id="pvc-lost-or-replaced">PVC lost or replaced</h3>
<p>Symptom: every <code>ExternalArtifact</code> URL returns 404 even though the snippet&rsquo;s Ready=True. The Publisher writes idempotently on every reconcile, so making the operator re-render every snippet is the fix:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl"><span class="c1"># Roll the operator — the cache is rebuilt from the apiserver and every</span>
</span></span><span class="line"><span class="cl"><span class="c1"># snippet is reconciled. Each reconcile re-runs the Publisher, which</span>
</span></span><span class="line"><span class="cl"><span class="c1"># writes the tarball back to disk. With a clean PVC, the gap closes in</span>
</span></span><span class="line"><span class="cl"><span class="c1"># one reconcile loop.</span>
</span></span><span class="line"><span class="cl">kubectl --namespace &lt;jaas-ns&gt; rollout restart deploy/jaas
</span></span></code></pre></div><p>If reconciles do not produce tarballs again, force a reconcile per snippet:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">kubectl annotate --all-namespaces jsonnetsnippet --all <span class="se">\
</span></span></span><span class="line"><span class="cl">  jaas.metio.wtf/reconcile-at<span class="o">=</span><span class="k">$(</span>date -u +%FT%TZ<span class="k">)</span> --overwrite
</span></span></code></pre></div><p>The window between PVC loss and the first re-render is the only outage downstream consumers see. With <code>replicas.max: 1</code> (chart default) that window is bounded by the rollout time; with multi-replica HA + RWX PVC, the lease-holder writes immediately and the gap is sub-second.</p>
<h3 id="emptydir-reset-pod-restart">emptyDir reset (pod restart)</h3>
<p><code>persistence.enabled: false</code> is fine for low-stakes deployments but every pod restart re-renders every snippet. The &ldquo;fix&rdquo; is to enable persistence:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">helm --namespace &lt;jaas-ns&gt; upgrade jaas oci://ghcr.io/metio/helm-charts/jaas <span class="se">\
</span></span></span><span class="line"><span class="cl">  --reuse-values <span class="se">\
</span></span></span><span class="line"><span class="cl">  --set operator.storage.persistence.enabled<span class="o">=</span><span class="nb">true</span> <span class="se">\
</span></span></span><span class="line"><span class="cl">  --set operator.storage.persistence.size<span class="o">=</span>10Gi
</span></span></code></pre></div><p>After the upgrade, follow the PVC-lost steps above to repopulate the new volume.</p>
<h3 id="storage-http-server-unreachable-but-operator-healthy">Storage HTTP server unreachable but operator healthy</h3>
<p>Diagnose:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">kubectl --namespace &lt;jaas-ns&gt; port-forward svc/jaas-storage &lt;port&gt;:8082 <span class="p">&amp;</span>
</span></span><span class="line"><span class="cl">curl -fsSL http://localhost:&lt;port&gt;/&lt;namespace&gt;/&lt;snippet&gt;/&lt;rev&gt;.tar.gz <span class="p">|</span> wc -c
</span></span></code></pre></div><p>If port-forward works but in-cluster fetches fail, look at NetworkPolicy:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">kubectl get networkpolicy --all-namespaces <span class="p">|</span> grep -i jaas
</span></span></code></pre></div><p>The chart&rsquo;s optional NetworkPolicy locks the storage port to a single source-controller selector. If your Flux install lives elsewhere or carries different labels, the NetworkPolicy will silently drop the traffic. Either widen <code>networkPolicy.fromSourceControllerSelector</code> or disable the NetworkPolicy on this chart and rely on a cluster-wide policy.</p>
<h2 id="s3-backend">S3 backend</h2>
<h3 id="endpoint-unreachable--5xx-from-the-provider">Endpoint unreachable / 5xx from the provider</h3>
<p>The pod-side storage HTTP server is a proxy. When the upstream S3 endpoint is down, the proxy returns 502/504 and downstream Flux consumers retry with backoff. Operator pod health is unaffected.</p>
<p>Diagnose:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl"><span class="c1"># Pull a recent tarball directly to confirm it&#39;s the upstream</span>
</span></span><span class="line"><span class="cl">kubectl --namespace &lt;jaas-ns&gt; <span class="nb">exec</span> deploy/jaas -- <span class="se">\
</span></span></span><span class="line"><span class="cl">  wget -O- http://localhost:8082/&lt;namespace&gt;/&lt;snippet&gt;/&lt;rev&gt;.tar.gz <span class="p">|</span> wc -c
</span></span></code></pre></div><p>If the in-pod fetch fails too, check the operator logs for <code>minio-go</code> errors. Auth problems (expired session token, rotated access key) show up here distinctly from network problems.</p>
<h3 id="bucket-gone-or-wrong-prefix">Bucket gone or wrong prefix</h3>
<p>If the bucket was emptied or the <code>--s3-prefix</code> changed, the proxy returns 404 even though the snippet is Ready. Re-render every snippet to repopulate:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">kubectl annotate --all-namespaces jsonnetsnippet --all <span class="se">\
</span></span></span><span class="line"><span class="cl">  jaas.metio.wtf/reconcile-at<span class="o">=</span><span class="k">$(</span>date -u +%FT%TZ<span class="k">)</span> --overwrite
</span></span></code></pre></div><p>The Publisher writes idempotently — running this against a working bucket is safe.</p>
<h3 id="credentials-rotated">Credentials rotated</h3>
<p>With static credentials (<code>--s3-access-key</code> / <code>--s3-secret-key</code> / inline chart values), a rotation requires a Deployment restart for <code>minio-go</code> to pick up the new values. With IAM/IRSA, the discovery chain re-reads at request time — the operator picks the new identity up automatically.</p>
<p>Force the new keys to take effect:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">kubectl --namespace &lt;jaas-ns&gt; rollout restart deploy/jaas
</span></span></code></pre></div><h2 id="disk-full-enospc">Disk full (<code>ENOSPC</code>)</h2>
<p>When the volume backing <code>--storage-path</code> fills up, <code>Store.Put</code> returns the kernel&rsquo;s <code>ENOSPC</code> verbatim → <code>Publisher.Publish</code> wraps it → no specific sentinel matches → classified as transient <code>ReasonSourceFetchFailed</code> → controller-runtime backoff retries forever at the ~16 min cap. The operator pod stays healthy; every snippet using local storage starts looping.</p>
<h3 id="symptom-1">Symptom</h3>
<ul>
<li>Multiple snippets simultaneously flip to <code>Ready=False</code> with messages mentioning <code>no space left on device</code>.</li>
<li><code>JaaSControllerWorkqueueDepthHigh</code> alert fires (the backoff queue saturates).</li>
<li><code>kubectl --namespace &lt;jaas-ns&gt; exec deploy/jaas -- df -h /var/lib/jaas/artifacts</code> shows the volume at 100%.</li>
</ul>
<h3 id="recovery">Recovery</h3>
<ol>
<li>
<p><strong>Free space.</strong> Either resize the PVC (if <code>operator.storage.persistence.enabled: true</code>), increase <code>operator.storage.sizeLimit</code> (emptyDir), or prune retained revisions:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl"><span class="c1"># Lower spec.history on noisy snippets so the next reconcile prunes</span>
</span></span><span class="line"><span class="cl"><span class="c1"># older revisions. The Publisher&#39;s Prune step removes everything</span>
</span></span><span class="line"><span class="cl"><span class="c1"># outside the keep-set, freeing space proportional to the change.</span>
</span></span><span class="line"><span class="cl">kubectl patch jsonnetsnippet &lt;name&gt; --type<span class="o">=</span>merge --patch <span class="s1">&#39;{&#34;spec&#34;:{&#34;history&#34;:1}}&#39;</span>
</span></span></code></pre></div><p>For an immediate flush, force-prune by removing the artifact directory of a snippet you&rsquo;re certain doesn&rsquo;t need its history:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">kubectl --namespace &lt;jaas-ns&gt; <span class="nb">exec</span> deploy/jaas -- <span class="se">\
</span></span></span><span class="line"><span class="cl">    rm -rf /var/lib/jaas/artifacts/&lt;namespace&gt;/&lt;expendable-snippet&gt;
</span></span></code></pre></div></li>
<li>
<p><strong>Drive reconciliation.</strong> The backoff cap is ~16 min, so failing snippets retry within that window automatically. To force immediate re-render:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl"><span class="c1"># Annotate every snippet that flipped to Ready=False:</span>
</span></span><span class="line"><span class="cl">kubectl get jsonnetsnippets --all-namespaces <span class="se">\
</span></span></span><span class="line"><span class="cl">  --output <span class="nv">jsonpath</span><span class="o">=</span><span class="s1">&#39;{range .items[?(@.status.conditions[?(@.type==&#34;Ready&#34;)].status==&#34;False&#34;)]}{.metadata.namespace}/{.metadata.name}{&#34;\n&#34;}{end}&#39;</span> <span class="se">\
</span></span></span><span class="line"><span class="cl">  <span class="p">|</span> xargs -n1 -I <span class="o">{}</span> sh -c <span class="s1">&#39;ns=$(echo &#34;{}&#34; | cut -d/ -f1); n=$(echo &#34;{}&#34; | cut -d/ -f2); \
</span></span></span><span class="line"><span class="cl"><span class="s1">      kubectl --namespace &#34;$ns&#34; annotate jsonnetsnippet &#34;$n&#34; \
</span></span></span><span class="line"><span class="cl"><span class="s1">      jaas.metio.wtf/reconcile-at=$(date -u +%FT%TZ) --overwrite&#39;</span>
</span></span></code></pre></div></li>
<li>
<p><strong>Prevention.</strong> Set <code>operator.storage.maxArtifactBytes</code> to cap individual snippet renders before they hit the disk. Use <code>JaaSSnippetArtifactGrowing</code> (opt-in PrometheusRule alert) to catch creep before saturation.</p>
</li>
</ol>
<h2 id="oom-during-render-kubelet-kills-the-operator-pod-mid-publish">OOM during render (kubelet kills the operator pod mid-publish)</h2>
<p>A snippet that evaluates into a multi-MB JSON tree can push the operator past its <code>resources.memory</code> limit. The kubelet SIGKILLs the pod. Effects:</p>
<ul>
<li>The pod restarts cleanly. Probes flip back to Ready within seconds.</li>
<li>The leader-election lease is dropped on the killed pod&rsquo;s process death; the next replica picks it up immediately (or, on single-replica installs, the new pod re-acquires).</li>
<li><strong>Mid-write <code>.tmp</code> files</strong> in the local store are orphaned because the <code>Store.Put</code> was interrupted between <code>Create</code> and the atomic <code>Rename</code>. The background <code>Sweep</code> (default every 10 min, configurable via <code>operator.storage.sweep.interval</code>) cleans them up once their <code>ModTime</code> falls outside the <code>maxTmpAge</code> window.</li>
<li><strong>The snippet that triggered the OOM</strong> keeps failing in the same way on the next reconcile because its rendered output is what blew memory in the first place. The pod loop-restarts until either the snippet is fixed or its memory cost falls below the limit.</li>
</ul>
<h3 id="symptom-2">Symptom</h3>
<ul>
<li>Operator pod restarts every few minutes; <code>kubectl describe pod</code> shows <code>Last State: Terminated, Reason: OOMKilled</code>.</li>
<li><code>JaaSOperatorPodDown</code> alert fires (the restart window is shorter than the recovery, so probes flap).</li>
<li>One specific snippet correlates with each restart.</li>
</ul>
<h3 id="diagnose">Diagnose</h3>
<p>The killing snippet is whichever the operator was reconciling when memory peaked. Easiest way to identify:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">kubectl --namespace &lt;jaas-ns&gt; logs deploy/jaas --previous --tail<span class="o">=</span><span class="m">200</span> <span class="se">\
</span></span></span><span class="line"><span class="cl">    <span class="p">|</span> grep -B2 -i <span class="s1">&#39;reconcil\|publish&#39;</span> <span class="p">|</span> tail -30
</span></span></code></pre></div><p>The last <code>Reconcile</code> log line before the kill names the snippet. Confirm via <code>jaas_snippet_rendered_bytes</code> if the operator&rsquo;s metrics endpoint was reachable before the kill (the histogram captures bytes per Synced reconcile; a runaway snippet stands out).</p>
<h3 id="remediate">Remediate</h3>
<ol>
<li>
<p><strong>Cap the runaway snippet.</strong> Set <code>operator.storage.maxArtifactBytes</code> cluster-wide to refuse renders past a threshold (e.g., <code>16777216</code> for 16 MiB). The Publisher fails the snippet with <code>ReasonArtifactTooLarge</code> instead of attempting the write. The operator pod stops OOM-restarting.</p>
</li>
<li>
<p><strong>Raise operator memory.</strong> The chart&rsquo;s <code>resources.memory</code> default is conservative (<code>64Mi</code>); a cluster with large rendered artifacts may need <code>256Mi</code> or more. Update via:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">helm --namespace &lt;jaas-ns&gt; upgrade jaas oci://ghcr.io/metio/helm-charts/jaas <span class="se">\
</span></span></span><span class="line"><span class="cl">  --reuse-values --set resources.memory<span class="o">=</span>256Mi
</span></span></code></pre></div></li>
<li>
<p><strong>Wait for <code>Sweep</code>.</strong> The orphan <code>.tar.gz.tmp</code> files clear automatically once the surrounding issue is fixed and <code>maxTmpAge</code> (default 30 min) elapses. <code>jaas_storage_sweep_failures_total</code> flags any persistent issue.</p>
</li>
</ol>
<p>For S3 backends, OOM during a multipart upload leaves an incomplete upload at the S3 endpoint — most providers expire these automatically (AWS S3: 7-day default). No JaaS-side action needed.</p>
<h2 id="multi-replica-considerations">Multi-replica considerations</h2>
<p>With leader election on (the chart default when operator mode is enabled), only the lease-holder writes to storage. A storage-incident on the lease-holder is the worst case: the standby reads but cannot fill the gap until the lease transfers. To force a handover during a storage incident on one replica:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">kubectl --namespace &lt;jaas-ns&gt; delete lease &lt;release-name&gt;-operator
</span></span></code></pre></div><p>The next replica acquires the lease within <code>LeaseDuration</code> (15s default), and its Publisher writes against its own (presumably healthy) view of the backend.</p>
<h2 id="prevention">Prevention</h2>
<ul>
<li>Use <code>persistence.enabled: true</code> in production. Default-off is for quick demos.</li>
<li>Run the chart&rsquo;s opt-in PrometheusRule (<code>operator.metrics.prometheusRule.enabled: true</code>) — <code>JaaSSnippetArtifactGrowing</code> catches runaway tarballs before they fill the PVC.</li>
<li>Set <code>operator.storage.maxArtifactBytes</code> to cap pathological snippets at admission time, not after they&rsquo;ve written to disk.</li>
<li>For S3, configure a bucket lifecycle policy that does not delete tarballs the operator still considers live. The Publisher&rsquo;s <code>Prune</code> only deletes revisions the snippet&rsquo;s <code>status.history</code> no longer references.</li>
</ul>
<h2 id="jaasstoragesweepfailures-alert"><code>JaaSStorageSweepFailures</code> alert</h2>
<p>Linked from the alert by name. The sweep is a background GC that removes orphaned <code>.tar.gz.tmp</code> residue left by Puts whose process died after the tmpfile landed but before the rename. The reconcile hot path is unaffected — Put still works — but stale <code>.tmp</code> files accumulate until the underlying issue is fixed.</p>
<p><strong>Symptom:</strong> <code>jaas_storage_sweep_failures_total</code> increases over time; <code>JaaSStorageSweepFailures</code> alert fires after &gt;3 failures/hour (configurable).</p>
<p><strong>Diagnose:</strong></p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl"><span class="c1"># Operator logs carry the underlying sweep error:</span>
</span></span><span class="line"><span class="cl">kubectl --namespace &lt;jaas-ns&gt; logs deploy/jaas --tail<span class="o">=</span><span class="m">200</span> <span class="p">|</span> grep <span class="s2">&#34;Storage sweep failed&#34;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># For local backend: check the volume&#39;s free space + permissions.</span>
</span></span><span class="line"><span class="cl">kubectl --namespace &lt;jaas-ns&gt; <span class="nb">exec</span> deploy/jaas -- df -h /var/lib/jaas/artifacts
</span></span><span class="line"><span class="cl">kubectl --namespace &lt;jaas-ns&gt; <span class="nb">exec</span> deploy/jaas -- ls -la /var/lib/jaas/artifacts
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># For S3 backend: sweep is a no-op (Put is atomic, no .tmp residue),</span>
</span></span><span class="line"><span class="cl"><span class="c1"># so this alert firing on S3 is a wiring bug. Confirm backend:</span>
</span></span><span class="line"><span class="cl">kubectl --namespace &lt;jaas-ns&gt; get deploy jaas --output <span class="nv">jsonpath</span><span class="o">=</span><span class="s1">&#39;{.spec.template.spec.containers[0].args}&#39;</span> <span class="p">|</span> tr <span class="s1">&#39;,&#39;</span> <span class="s1">&#39;\n&#39;</span> <span class="p">|</span> grep storage-backend
</span></span></code></pre></div><p><strong>Remediate:</strong></p>
<ul>
<li>Disk full → increase the PVC size or shrink <code>operator.storage.maxArtifactBytes</code>.</li>
<li>Permission errors → ensure the operator&rsquo;s <code>securityContext.runAsUser</code> matches the PVC&rsquo;s filesystem ownership; reset with <code>chown</code> on a one-shot Job.</li>
<li>Sustained S3 listing throttling → unlikely on the local backend; for S3 this alert shouldn&rsquo;t fire at all because Sweep is a no-op there.</li>
</ul>
<p>Manual cleanup once the underlying issue is fixed:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">kubectl --namespace &lt;jaas-ns&gt; <span class="nb">exec</span> deploy/jaas -- find /var/lib/jaas/artifacts -name <span class="s1">&#39;*.tar.gz.tmp&#39;</span> -mmin +30 -delete
</span></span></code></pre></div><h2 id="withdrawforced-event-on-snippet-deletion"><code>WithdrawForced</code> event on snippet deletion</h2>
<p>If a snippet stuck in <code>Terminating</code> carries a <code>Warning WithdrawForced</code> Kubernetes Event, the operator has already done what it could — the finalizer was dropped after <code>--max-withdraw-wait</code> (default 1h) of failing Withdraws against the backend, and the snippet itself is GC&rsquo;d. The tarball it owned is now orphaned in storage. To clean up:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl"><span class="c1"># Read the elapsed time + last backend error from the event message:</span>
</span></span><span class="line"><span class="cl">kubectl describe jsonnetsnippet &lt;name&gt;
</span></span><span class="line"><span class="cl"><span class="c1"># Locate the orphan in the configured backend:</span>
</span></span><span class="line"><span class="cl"><span class="c1">#   local:  &lt;--storage-path&gt;/&lt;namespace&gt;/&lt;name&gt;/&lt;rev&gt;.tar.gz</span>
</span></span><span class="line"><span class="cl"><span class="c1">#   s3:     &lt;--s3-prefix&gt;/&lt;namespace&gt;/&lt;name&gt;/&lt;rev&gt;.tar.gz</span>
</span></span><span class="line"><span class="cl"><span class="c1"># Remove it once the backend is reachable again.</span>
</span></span></code></pre></div><p>A force-drop should be rare — it means the backend was broken for the full wait window. Investigate <strong>why</strong> before lowering <code>maxWithdrawWait</code>: aggressive timeouts make transient apiserver/S3 incidents cause orphans you&rsquo;d otherwise have recovered from naturally.</p>
<h2 id="related-runbooks">Related runbooks</h2>
<ul>
<li><a href="/runbooks/artifacttoolarge/">artifacttoolarge.md</a>
 — one snippet&rsquo;s output exceeds the cap (different symptom: snippet Ready=False, not &ldquo;URL unreachable&rdquo;)</li>
<li><a href="/runbooks/sourcefetchfailed/">sourcefetchfailed.md</a>
 — JaaS <em>consuming</em> an upstream artifact, not its own storage</li>
</ul>
]]></content><category scheme="https://jaas.projects.metio.wtf/tags/runbooks" term="runbooks" label="runbooks"/><category scheme="https://jaas.projects.metio.wtf/tags/troubleshooting" term="troubleshooting" label="troubleshooting"/><category scheme="https://jaas.projects.metio.wtf/tags/storage" term="storage" label="storage"/></entry><entry><title type="html">Suspended</title><link href="https://jaas.projects.metio.wtf/runbooks/suspended/?utm_source=atom_feed" rel="alternate" type="text/html"/><link href="https://jaas.projects.metio.wtf/runbooks/crd-watch-engagement/?utm_source=atom_feed" rel="related" type="text/html" title="CRD watch engagement failing"/><link href="https://jaas.projects.metio.wtf/runbooks/invalidspec/?utm_source=atom_feed" rel="related" type="text/html" title="InvalidSpec"/><link href="https://jaas.projects.metio.wtf/runbooks/operator-pod-down/?utm_source=atom_feed" rel="related" type="text/html" title="Operator pod not ready"/><link href="https://jaas.projects.metio.wtf/runbooks/pending/?utm_source=atom_feed" rel="related" type="text/html" title="Pending"/><link href="https://jaas.projects.metio.wtf/runbooks/synced/?utm_source=atom_feed" rel="related" type="text/html" title="Synced"/><id>https://jaas.projects.metio.wtf/runbooks/suspended/</id><published>0001-01-01T00:00:00+00:00</published><updated>2026-06-17T10:56:50+02:00</updated><content type="html"><![CDATA[<blockquote>Reconciliation is intentionally paused because spec.suspend is true; the last published artifact remains intact</blockquote><h2 id="symptom">Symptom</h2>
<p><code>READY=False</code>, <code>REASON=Suspended</code>. The snippet&rsquo;s <code>spec.suspend</code> is set to <code>true</code>.</p>
<h2 id="cause">Cause</h2>
<p>An operator (or automation) paused reconciliation for this snippet, typically to investigate a downstream issue without the artifact being rewritten underneath them. The previously-published <code>ExternalArtifact</code> and the on-disk tarball are left intact — downstream Flux consumers continue serving the last successful render.</p>
<p>This is a normal, intentional state. It is not a failure.</p>
<h2 id="diagnosis">Diagnosis</h2>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">kubectl get jsonnetsnippet &lt;name&gt; --output <span class="nv">jsonpath</span><span class="o">=</span><span class="s1">&#39;{.spec.suspend}&#39;</span>
</span></span></code></pre></div><p>If the value is <code>true</code>, the suspension is set on the spec. Check <code>kubectl describe</code> for the last condition transition timestamp to see when it happened.</p>
<h2 id="remediation">Remediation</h2>
<p>To resume reconciliation:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">kubectl patch jsonnetsnippet &lt;name&gt; --type<span class="o">=</span>merge --patch <span class="s1">&#39;{&#34;spec&#34;:{&#34;suspend&#34;:false}}&#39;</span>
</span></span></code></pre></div><p>Or remove the field entirely:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">kubectl edit jsonnetsnippet &lt;name&gt;
</span></span><span class="line"><span class="cl"><span class="c1"># delete the `suspend: true` line under spec</span>
</span></span></code></pre></div><p>The next reconcile picks up the snippet&rsquo;s current spec and republishes if anything has drifted.</p>
]]></content><category scheme="https://jaas.projects.metio.wtf/tags/runbooks" term="runbooks" label="runbooks"/><category scheme="https://jaas.projects.metio.wtf/tags/troubleshooting" term="troubleshooting" label="troubleshooting"/><category scheme="https://jaas.projects.metio.wtf/tags/lifecycle" term="lifecycle" label="lifecycle"/></entry><entry><title type="html">Synced</title><link href="https://jaas.projects.metio.wtf/runbooks/synced/?utm_source=atom_feed" rel="alternate" type="text/html"/><link href="https://jaas.projects.metio.wtf/runbooks/crd-watch-engagement/?utm_source=atom_feed" rel="related" type="text/html" title="CRD watch engagement failing"/><link href="https://jaas.projects.metio.wtf/runbooks/invalidspec/?utm_source=atom_feed" rel="related" type="text/html" title="InvalidSpec"/><link href="https://jaas.projects.metio.wtf/runbooks/operator-pod-down/?utm_source=atom_feed" rel="related" type="text/html" title="Operator pod not ready"/><link href="https://jaas.projects.metio.wtf/runbooks/pending/?utm_source=atom_feed" rel="related" type="text/html" title="Pending"/><link href="https://jaas.projects.metio.wtf/runbooks/suspended/?utm_source=atom_feed" rel="related" type="text/html" title="Suspended"/><id>https://jaas.projects.metio.wtf/runbooks/synced/</id><published>0001-01-01T00:00:00+00:00</published><updated>2026-06-17T10:56:50+02:00</updated><content type="html"><![CDATA[<blockquote>The snippet reconciled end-to-end and its ExternalArtifact is current; no action is required</blockquote><h2 id="symptom">Symptom</h2>
<p><code>kubectl get jsonnetsnippet</code> shows <code>READY=True</code> with <code>REASON=Synced</code>. This is the healthy state — no action required.</p>
<h2 id="cause">Cause</h2>
<p>The most recent reconcile pass completed end-to-end: source resolved, libraries resolved, eval succeeded, tarball published, ExternalArtifact upserted.</p>
<h2 id="diagnosis">Diagnosis</h2>
<p>To inspect the published artifact:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">kubectl get externalartifact &lt;snippet-name&gt; --output yaml
</span></span></code></pre></div><p>The <code>status.artifact.url</code> points at the operator&rsquo;s storage HTTP server. Curl it from a pod in the cluster to confirm the bytes match:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">kubectl run --rm --stdin --tty --restart<span class="o">=</span>Never tmp --image<span class="o">=</span>docker.io/library/curlimages/curl -- <span class="se">\
</span></span></span><span class="line"><span class="cl">  curl -sL &lt;status.artifact.url&gt; <span class="p">|</span> tar tzv
</span></span></code></pre></div><h2 id="remediation">Remediation</h2>
<p>None — this is the healthy state.</p>
]]></content><category scheme="https://jaas.projects.metio.wtf/tags/runbooks" term="runbooks" label="runbooks"/><category scheme="https://jaas.projects.metio.wtf/tags/troubleshooting" term="troubleshooting" label="troubleshooting"/><category scheme="https://jaas.projects.metio.wtf/tags/lifecycle" term="lifecycle" label="lifecycle"/></entry><entry><title type="html">Tenancy and RBAC</title><link href="https://jaas.projects.metio.wtf/usage/tenancy-and-rbac/?utm_source=atom_feed" rel="alternate" type="text/html"/><link href="https://jaas.projects.metio.wtf/usage/admission-webhook/?utm_source=atom_feed" rel="related" type="text/html" title="Admission webhook"/><link href="https://jaas.projects.metio.wtf/usage/alerting/?utm_source=atom_feed" rel="related" type="text/html" title="Alerting"/><link href="https://jaas.projects.metio.wtf/usage/creating-sources/?utm_source=atom_feed" rel="related" type="text/html" title="Creating source artifacts"/><link href="https://jaas.projects.metio.wtf/runbooks/crossnamespacerefrejected/?utm_source=atom_feed" rel="related" type="text/html" title="CrossNamespaceRefRejected"/><link href="https://jaas.projects.metio.wtf/api/externalartifact/?utm_source=atom_feed" rel="related" type="text/html" title="ExternalArtifact output contract"/><id>https://jaas.projects.metio.wtf/usage/tenancy-and-rbac/</id><published>0001-01-01T00:00:00+00:00</published><updated>2026-06-16T20:48:39+02:00</updated><content type="html"><![CDATA[<blockquote>Per-snippet ServiceAccount impersonation, the minimal operator ClusterRole, the tenant Role callers must grant, and the watch-scope flags.</blockquote><p>In <a href="/usage/operator-mode/">operator mode</a>
 the JaaS operator never acts with its
own broad privileges when touching tenant resources. Every reconcile of a
<code>JsonnetSnippet</code> runs against the RBAC of a tenant ServiceAccount, so a snippet
can only reach what its own ServiceAccount is allowed to reach.</p>
<h2 id="per-snippet-impersonation">Per-snippet impersonation</h2>
<p>Each <code>JsonnetSnippet</code> carries a <code>spec.serviceAccountName</code>. On every reconcile the
operator mints a short-lived Bearer token for that ServiceAccount through the
Kubernetes TokenRequest API (<code>serviceaccounts/token: create</code>) and performs all
tenant-side API calls — reading <code>JsonnetLibrary</code> objects, fetching Flux source
artifacts, and writing the published <code>ExternalArtifact</code> — as that ServiceAccount.
The operator does not use the <code>impersonate</code> verb; it uses a real token, so the
apiserver evaluates the tenant&rsquo;s own RBAC.</p>
<p>When a snippet omits <code>spec.serviceAccountName</code>, the operator falls back to the
ServiceAccount named in <code>--default-service-account</code>. If that flag is also empty,
such a snippet is rejected at reconcile time rather than silently running with
elevated rights. Set <code>--default-service-account</code> to a low-privilege account if
you want snippets without an explicit ServiceAccount to reconcile at all.</p>
<h2 id="the-operators-own-clusterrole">The operator&rsquo;s own ClusterRole</h2>
<p>Because every tenant-side call is the tenant&rsquo;s, the operator&rsquo;s own ClusterRole
stays minimal:</p>
<ul>
<li><code>serviceaccounts/token: create</code> — to mint the Bearer tokens above.</li>
<li><code>get</code>/<code>list</code>/<code>watch</code> on <code>customresourcedefinitions.apiextensions.k8s.io</code> — the
CRD watcher subscribes to the cluster&rsquo;s CRD stream so that Flux source-kind
watches engage automatically when a previously-absent CRD becomes established,
without a process restart.</li>
<li>Watch verbs on the JaaS CRDs (<code>JsonnetSnippet</code>, <code>JsonnetLibrary</code>) and on the
Flux source kinds it chains from (<code>GitRepository</code>, <code>OCIRepository</code>, <code>Bucket</code>,
<code>ExternalArtifact</code>).</li>
</ul>
<p>The operator does not need <code>create</code>/<code>update</code>/<code>patch</code> on <code>ExternalArtifact</code> in its
own ClusterRole — that write is done as the tenant, so the verb lives on the
tenant Role below.</p>
<h2 id="the-tenant-role">The tenant Role</h2>
<p>The ServiceAccount each snippet runs as needs explicit verbs, or the first
reconcile fails with <code>Forbidden</code> and the failure points at the wrong cause. Grant
this Role in the tenant&rsquo;s namespace:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nt">apiVersion</span><span class="p">:</span><span class="w"> </span><span class="l">rbac.authorization.k8s.io/v1</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">kind</span><span class="p">:</span><span class="w"> </span><span class="l">Role</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">metadata</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">namespace</span><span class="p">:</span><span class="w"> </span><span class="l">&lt;tenant-namespace&gt;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">jaas-tenant</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">rules</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="c"># Required: the operator writes the snippet&#39;s ExternalArtifact as</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="c"># the tenant ServiceAccount. Without these the publish step is denied.</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span>- <span class="nt">apiGroups</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="l">source.toolkit.fluxcd.io]</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">resources</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="l">externalartifacts]</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">verbs</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="l">get, create, update, patch]</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="c"># Required only when the snippet uses spec.libraries (JsonnetLibrary refs).</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span>- <span class="nt">apiGroups</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="l">jaas.metio.wtf]</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">resources</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="l">jsonnetlibraries]</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">verbs</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="l">get, list]</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="c"># Required only when the snippet uses spec.sourceRef. Grant only</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="c"># the source kinds your tenants actually reference.</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span>- <span class="nt">apiGroups</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="l">source.toolkit.fluxcd.io]</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">resources</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="l">gitrepositories, ocirepositories, buckets, externalartifacts]</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">verbs</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="l">get]</span><span class="w">
</span></span></span></code></pre></div><p>Notes on each rule:</p>
<ul>
<li>The <code>externalartifacts</code> write verbs (<code>create</code>, <code>update</code>, <code>patch</code>) are
mandatory. The operator writes the published artifact CR through the
impersonating client on purpose, so one tenant Role governs both source-side
reads and artifact-side writes.</li>
<li>The <code>jsonnetlibraries</code> rule is needed only when a snippet references libraries
through <code>spec.libraries</code>. See <a href="/usage/snippet-sources/">snippet sources</a>
 for how
libraries reach a snippet.</li>
<li>The source-kind <code>get</code> rule is needed only when a snippet has a <code>spec.sourceRef</code>.
Grant only the kinds your tenants reference. The <code>externalartifacts</code> entry here
covers chained snippets — snippet B reading the <code>ExternalArtifact</code> snippet A
publishes.</li>
</ul>
<h2 id="binding-per-namespace">Binding per namespace</h2>
<p>For namespace-scoped multitenancy, bind the Role to each tenant ServiceAccount in
its own namespace with a <code>RoleBinding</code>:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nt">apiVersion</span><span class="p">:</span><span class="w"> </span><span class="l">rbac.authorization.k8s.io/v1</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">kind</span><span class="p">:</span><span class="w"> </span><span class="l">RoleBinding</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">metadata</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">namespace</span><span class="p">:</span><span class="w"> </span><span class="l">&lt;tenant-namespace&gt;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">jaas-tenant</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">roleRef</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">apiGroup</span><span class="p">:</span><span class="w"> </span><span class="l">rbac.authorization.k8s.io</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">kind</span><span class="p">:</span><span class="w"> </span><span class="l">Role</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">jaas-tenant</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">subjects</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span>- <span class="nt">kind</span><span class="p">:</span><span class="w"> </span><span class="l">ServiceAccount</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">&lt;tenant-service-account&gt;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">namespace</span><span class="p">:</span><span class="w"> </span><span class="l">&lt;tenant-namespace&gt;</span><span class="w">
</span></span></span></code></pre></div><p>Each tenant namespace gets its own <code>Role</code> + <code>RoleBinding</code>, so a snippet&rsquo;s blast
radius is its own namespace&rsquo;s grants.</p>
<h2 id="single-tenant-clusters">Single-tenant clusters</h2>
<p>When a cluster runs only your own workloads and snippets do not need isolating from
each other, a Role per namespace is more than you need. The operator still
impersonates a ServiceAccount — it never applies with its own identity — so the
simplest setup is one shared account:</p>
<ol>
<li>
<p>Create a single ServiceAccount and grant it the rights your snippets need. On a
single-tenant cluster that can be broad: a <code>ClusterRoleBinding</code> to the built-in
<code>cluster-admin</code> ClusterRole lets any snippet read any source and publish into
any namespace.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nt">apiVersion</span><span class="p">:</span><span class="w"> </span><span class="l">v1</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">kind</span><span class="p">:</span><span class="w"> </span><span class="l">ServiceAccount</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">metadata</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">jaas-snippets</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">namespace</span><span class="p">:</span><span class="w"> </span><span class="l">jaas-system</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nn">---</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">apiVersion</span><span class="p">:</span><span class="w"> </span><span class="l">rbac.authorization.k8s.io/v1</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">kind</span><span class="p">:</span><span class="w"> </span><span class="l">ClusterRoleBinding</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">metadata</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">jaas-snippets-admin</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">roleRef</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">apiGroup</span><span class="p">:</span><span class="w"> </span><span class="l">rbac.authorization.k8s.io</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">kind</span><span class="p">:</span><span class="w"> </span><span class="l">ClusterRole</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">cluster-admin</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">subjects</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span>- <span class="nt">kind</span><span class="p">:</span><span class="w"> </span><span class="l">ServiceAccount</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">jaas-snippets</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">namespace</span><span class="p">:</span><span class="w"> </span><span class="l">jaas-system</span><span class="w">
</span></span></span></code></pre></div></li>
<li>
<p>Point the operator&rsquo;s <code>--default-service-account</code> at it (set through the chart&rsquo;s
operator values), and leave <code>spec.serviceAccountName</code> off your snippets. Every
snippet then reconciles as that one account.</p>
</li>
</ol>
<p>This trades isolation for simplicity — every snippet has the same rights, so use it
only where you trust every snippet author. To move to multitenancy later, give
individual snippets their own <code>spec.serviceAccountName</code> scoped to a tenant Role as
above; anything still relying on the default keeps working.</p>
<h2 id="restricting-cross-namespace-references">Restricting cross-namespace references</h2>
<p><code>--no-cross-namespace-refs</code> defaults to <code>true</code>: a <code>JsonnetSnippet</code> or
<code>JsonnetLibrary</code> whose <code>sourceRef</code> targets a different namespace is rejected. Keep
this on for multitenancy — it stops one tenant from pointing a snippet at another
tenant&rsquo;s source. Set it to <code>false</code> only when you operate every namespace yourself
and deliberately want cross-namespace chaining.</p>
<h2 id="narrowing-the-watch">Narrowing the watch</h2>
<p>Two flags scope which CRs the operator reconciles:</p>
<ul>
<li><code>--label-selector</code> narrows the watch to CRs whose labels match the selector.
Empty (the default) selects every CR in the watched scope. Use it to run an
operator over only a labelled subset of snippets.</li>
<li><code>--watch-namespaces</code> (or the <code>JAAS_WATCH_NAMESPACES</code> environment variable) takes
a comma-separated namespace list and restricts the manager&rsquo;s cache to those
namespaces. Empty (the default) is cluster-wide. The Helm chart&rsquo;s
<code>operator.watchNamespaces</code> mirrors this: when set, it threads the value into the
deployment&rsquo;s <code>--watch-namespaces</code> argument and pivots the rendered RBAC to one
<code>RoleBinding</code> per listed namespace instead of a cluster-wide
<code>ClusterRoleBinding</code>. Cluster-scoped resources (CRDs, the optional
<code>ValidatingWebhookConfiguration</code>) stay bound through a <code>ClusterRoleBinding</code>,
since they are inherently cluster-scoped.</li>
</ul>
<p>The full flag list, with defaults, is on the
<a href="/installation/configuration/">configuration page</a>
.</p>
]]></content><category scheme="https://jaas.projects.metio.wtf/tags/operator" term="operator" label="operator"/><category scheme="https://jaas.projects.metio.wtf/tags/rbac" term="rbac" label="rbac"/><category scheme="https://jaas.projects.metio.wtf/tags/multitenancy" term="multitenancy" label="multitenancy"/></entry><entry><title type="html">Tracing</title><link href="https://jaas.projects.metio.wtf/usage/tracing/?utm_source=atom_feed" rel="alternate" type="text/html"/><link href="https://jaas.projects.metio.wtf/usage/alerting/?utm_source=atom_feed" rel="related" type="text/html" title="Alerting"/><link href="https://jaas.projects.metio.wtf/usage/logging/?utm_source=atom_feed" rel="related" type="text/html" title="Logging"/><link href="https://jaas.projects.metio.wtf/usage/metrics/?utm_source=atom_feed" rel="related" type="text/html" title="Metrics"/><link href="https://jaas.projects.metio.wtf/usage/observability/?utm_source=atom_feed" rel="related" type="text/html" title="Observability"/><link href="https://jaas.projects.metio.wtf/usage/admission-webhook/?utm_source=atom_feed" rel="related" type="text/html" title="Admission webhook"/><id>https://jaas.projects.metio.wtf/usage/tracing/</id><published>0001-01-01T00:00:00+00:00</published><updated>2026-06-17T22:19:37+02:00</updated><content type="html"><![CDATA[<blockquote>The JaaS operator exports OpenTelemetry traces over OTLP gRPC. Pointing it at a collector, sampling, viewing spans, and the Helm chart keys that drive it.</blockquote><p>The JaaS operator exports OpenTelemetry traces over OTLP gRPC. With an endpoint
configured, each reconcile and the work it fans out into — source fetch, library
resolution, evaluation, publish — becomes a span you can follow in a tracing
backend. When no endpoint is set, the OpenTelemetry SDK runs in no-op mode and
emits nothing, so tracing carries no cost until you opt in.</p>
<h2 id="the-binary">The binary</h2>
<p>Three flags configure the exporter:</p>
<ul>
<li><code>--tracing-endpoint</code> — the OTLP gRPC collector <code>host:port</code>, e.g.
<code>otel-collector.observability.svc:4317</code>. Empty (the default) disables tracing
entirely.</li>
<li><code>--tracing-insecure</code> — skip TLS when dialing the collector. Default <code>false</code>.
Use only for in-cluster collectors that do not terminate TLS themselves.</li>
<li><code>--tracing-sample-ratio</code> — TraceID-ratio sampling between <code>0.0</code> and <code>1.0</code>.
Default <code>1.0</code> samples every trace.</li>
</ul>
<p>The full flag list with defaults is on the
<a href="/installation/configuration/">configuration page</a>
.</p>
<h3 id="viewing-spans">Viewing spans</h3>
<p>Point <code>--tracing-endpoint</code> at any OTLP-gRPC-speaking collector — the
OpenTelemetry Collector, Jaeger, Tempo, or a vendor agent — and view the spans
in whatever backend that collector feeds. A reconcile span carries the snippet&rsquo;s
namespace and name plus the spec generation it acted on
(<code>jaas.generation</code>), so you can search for one snippet and see the latency
breakdown across its fetch, eval, and publish phases. That is the fastest way to
tell a slow upstream source fetch apart from a slow evaluation when a snippet&rsquo;s
reconcile latency climbs.</p>
<p>When a phase fails — source resolution, library resolution, evaluation, or
publish — its span records the error and is marked with an error status, so the
failed span shows up as <code>status=error</code> and is directly queryable in the tracing
backend. Searching for error spans surfaces the exact phase a failing reconcile
broke in without reading through the full trace.</p>
<p>On a busy operator, drop <code>--tracing-sample-ratio</code> below <code>1.0</code> to keep only a
fraction of traces — <code>0.1</code> samples one in ten. Leave it at <code>1.0</code> while
diagnosing a specific problem so no trace is dropped.</p>
<h2 id="the-helm-chart">The Helm chart</h2>
<p>Tracing lives under <code>operator.tracing</code>. It only takes effect in operator mode
(<code>operator.enabled: true</code>):</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nt">operator</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">enabled</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">tracing</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">endpoint</span><span class="p">:</span><span class="w"> </span><span class="l">otel-collector.observability.svc:4317</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">insecure</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">sampleRatio</span><span class="p">:</span><span class="w"> </span><span class="m">1.0</span><span class="w">
</span></span></span></code></pre></div><p>The keys map directly onto the flags: <code>endpoint</code> → <code>--tracing-endpoint</code>,
<code>insecure</code> → <code>--tracing-insecure</code>, <code>sampleRatio</code> → <code>--tracing-sample-ratio</code>.
Leaving <code>endpoint</code> empty (the default) keeps the SDK in no-op mode.</p>
]]></content><category scheme="https://jaas.projects.metio.wtf/tags/operator" term="operator" label="operator"/><category scheme="https://jaas.projects.metio.wtf/tags/tracing" term="tracing" label="tracing"/><category scheme="https://jaas.projects.metio.wtf/tags/observability" term="observability" label="observability"/></entry><entry><title type="html">Watch-layer silent failure</title><link href="https://jaas.projects.metio.wtf/runbooks/operator-watch-silent/?utm_source=atom_feed" rel="alternate" type="text/html"/><link href="https://jaas.projects.metio.wtf/runbooks/crossnamespacerefrejected/?utm_source=atom_feed" rel="related" type="text/html" title="CrossNamespaceRefRejected"/><link href="https://jaas.projects.metio.wtf/runbooks/librarynotfound/?utm_source=atom_feed" rel="related" type="text/html" title="LibraryNotFound"/><link href="https://jaas.projects.metio.wtf/runbooks/rbacdenied/?utm_source=atom_feed" rel="related" type="text/html" title="RBACDenied"/><link href="https://jaas.projects.metio.wtf/runbooks/serviceaccountmissing/?utm_source=atom_feed" rel="related" type="text/html" title="ServiceAccountMissing"/><link href="https://jaas.projects.metio.wtf/runbooks/artifacttoolarge/?utm_source=atom_feed" rel="related" type="text/html" title="ArtifactTooLarge"/><id>https://jaas.projects.metio.wtf/runbooks/operator-watch-silent/</id><published>0001-01-01T00:00:00+00:00</published><updated>2026-06-17T10:56:50+02:00</updated><content type="html"><![CDATA[<blockquote>The operator&rsquo;s own ClusterRole is missing a verb on a watched kind, so controller-runtime&rsquo;s informer retries silently and no snippet status reflects the problem</blockquote><p>Not tied to a per-snippet <code>Reason</code>. This page covers the one RBAC-denial path the reconciler cannot surface itself: when the <strong>operator&rsquo;s own</strong> ClusterRole is missing a verb on a watched resource kind, controller-runtime&rsquo;s informer fails to start its watch, logs warnings, and retries silently. The reconciler never sees the failure — and no snippet&rsquo;s status condition will tell you about it.</p>
<p>If a per-snippet runbook (<code>rbacdenied.md</code>, <code>sourcefetchfailed.md</code>, <code>sourcenotready.md</code>) doesn&rsquo;t match the symptoms, this is where to look next.</p>
<h2 id="symptom">Symptom</h2>
<ul>
<li>Snippets that worked yesterday stop receiving watch-driven re-renders. They still reconcile on edits or <code>spec.interval</code> ticks, but not on upstream source changes.</li>
<li><code>kubectl describe jsonnetsnippet</code> shows healthy or stale state — never <code>Reason=RBACDenied</code> or any other failure.</li>
<li>Operator pod is <code>Ready=True</code>, all probes pass.</li>
<li>A <code>Flux GitRepository</code> (or <code>OCIRepository</code> / <code>Bucket</code> / <code>ExternalArtifact</code> / <code>JsonnetLibrary</code>) advances its <code>status.artifact</code> but no JaaS reconcile fires.</li>
</ul>
<h2 id="why-jaas-cant-tell-you-directly">Why JaaS can&rsquo;t tell you directly</h2>
<p>controller-runtime&rsquo;s informer is what watches resource kinds at the apiserver. If the operator SA lacks <code>list</code>/<code>watch</code> on a kind:</p>
<ol>
<li>The informer&rsquo;s initial LIST returns <code>Forbidden</code>.</li>
<li>controller-runtime logs <code>Failed to watch *v1.X: forbidden: ...</code> at error level.</li>
<li>The informer retries with exponential backoff — forever.</li>
<li>The reconciler&rsquo;s reconcile loop never gets events from that kind.</li>
<li>The reconciler itself is unaware that the watch is non-functional. The &ldquo;no events arriving&rdquo; condition is indistinguishable from &ldquo;no actual changes upstream.&rdquo;</li>
</ol>
<p>This is the one diagnostic surface the operator can&rsquo;t unify with its other RBAC-denial paths (Fetcher / library Get / Publisher write — all per-reconcile and surfaced via <code>Reason=RBACDenied</code>).</p>
<h2 id="diagnosis">Diagnosis</h2>
<p>The smoking gun is in the operator&rsquo;s logs:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">kubectl --namespace &lt;jaas-ns&gt; logs deploy/jaas --tail<span class="o">=</span><span class="m">2000</span> <span class="se">\
</span></span></span><span class="line"><span class="cl">  <span class="p">|</span> grep -E <span class="s1">&#39;Failed to watch|&#34;reflector.go&#34;&#39;</span> <span class="se">\
</span></span></span><span class="line"><span class="cl">  <span class="p">|</span> head -30
</span></span></code></pre></div><p>Expected output if the watch layer is healthy: nothing.</p>
<p>If broken, you&rsquo;ll see lines like:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">E0610 12:34:56.789  reflector.go:227 &#34;Failed to watch&#34; err=&#34;failed to list *v1.GitRepository: ... forbidden: ServiceAccount \&#34;jaas\&#34; cannot list resource \&#34;gitrepositories\&#34; in API group \&#34;source.toolkit.fluxcd.io\&#34; at the cluster scope&#34; type=&#34;*v1.GitRepository&#34;
</span></span></code></pre></div><p>The error names the SA, verb, resource, and API group — that&rsquo;s the exact gap to close.</p>
<p>Check what the operator SA can actually do:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">kubectl auth can-i list gitrepositories.source.toolkit.fluxcd.io <span class="se">\
</span></span></span><span class="line"><span class="cl">    --as<span class="o">=</span>system:serviceaccount:&lt;jaas-ns&gt;:jaas
</span></span></code></pre></div><p>Compare against the chart-rendered ClusterRole:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">kubectl get clusterrole &lt;release&gt;-operator --output yaml <span class="p">|</span> grep -A2 source.toolkit.fluxcd.io
</span></span></code></pre></div><h2 id="remediation">Remediation</h2>
<p>The operator&rsquo;s ClusterRole verbs are defined in the chart&rsquo;s <code>templates/clusterrole-operator.yaml</code> (in the metio/helm-charts repo, under <code>charts/jaas/</code>). Three causes warrant separate fixes:</p>
<h3 id="1-chart-upgraded-with-rbaccreate-false">1. Chart upgraded with <code>rbac.create: false</code></h3>
<p>Someone disabled chart-rendered RBAC (<code>operator.rbac.create: false</code>) and the external RBAC source missed a verb. Either re-enable chart-rendered RBAC, or update whatever owns the ClusterRole to grant the missing verbs.</p>
<h3 id="2-manual-chart-edit-removed-verbs">2. Manual chart edit removed verbs</h3>
<p>A <code>kubectl edit clusterrole &lt;release&gt;-operator</code> or a hand-rolled overlay removed verbs the chart originally rendered. Restore via <code>helm upgrade --install</code> (idempotent for an installed chart).</p>
<h3 id="3-new-source-kind-added-but-charts-drift-gate-didnt-catch-it">3. New source kind added but chart&rsquo;s drift gate didn&rsquo;t catch it</h3>
<p>The drift-gate test in the chart&rsquo;s <code>tests/clusterrole-operator_test.yaml</code> (metio/helm-charts, under <code>charts/jaas/</code>) — the &ldquo;ClusterRole drift gate&rdquo; case — is supposed to fail at PR time if <code>operator.FluxSourceKinds</code> adds a kind without a matching ClusterRole entry. If you reach this runbook page because of a new kind, <strong>it means the test passed but production RBAC is still missing it</strong> — investigate why (test bypassed, chart drift, etc.). Add the verb manually as a hotfix, then file a bug against the drift gate.</p>
<p>After granting the verb, restart the operator pod so a fresh informer picks up the new ServiceAccount-token permissions:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">kubectl --namespace &lt;jaas-ns&gt; rollout restart deploy/jaas
</span></span></code></pre></div><p>Watch-driven re-renders resume within seconds.</p>
<h2 id="why-not-detect-this-automatically">Why not detect this automatically</h2>
<p>A startup probe (operator does a test <code>LIST</code> per kind and refuses to boot on Forbidden) was considered and rejected:</p>
<ul>
<li>It would block startup on transient apiserver flakes during deploys.</li>
<li>The CRD-watcher pattern already handles the missing-CRD case gracefully (the <code>crdWatcher</code> engages a watch dynamically when the CRD becomes <code>Established=True</code>). Layering &ldquo;and also fail on Forbidden&rdquo; complicates that contract.</li>
<li>A misconfigured cluster should surface the issue via the existing logs + the <code>kubectl auth can-i</code> workflow, which is the standard k8s troubleshooting path.</li>
</ul>
<p>The diagnostic trail above is the supported recovery story. If a user reports hitting this in the wild and finds the log-grep step too obscure, a follow-up is a <code>jaas_informer_watch_failures_total</code> Prometheus counter plus a <code>JaaSInformerWatchFailing</code> alert — same shape as <code>JaaSStorageSweepFailures</code> from the storage layer. Track in <code>open-items.md</code> if it comes up.</p>
<h2 id="related-runbooks">Related runbooks</h2>
<ul>
<li><a href="/runbooks/rbacdenied/">rbacdenied.md</a>
 — per-reconcile RBAC denials the reconciler CAN surface (tenant SA can&rsquo;t read a source CR / library, can&rsquo;t write ExternalArtifact). If a snippet&rsquo;s status says <code>Reason=RBACDenied</code>, start there instead.</li>
<li><a href="/runbooks/storage-recovery/">storage-recovery.md</a>
 — different failure surface (storage backend rather than apiserver), same &ldquo;graceful degradation, diagnosis via logs + metrics&rdquo; shape.</li>
</ul>
]]></content><category scheme="https://jaas.projects.metio.wtf/tags/runbooks" term="runbooks" label="runbooks"/><category scheme="https://jaas.projects.metio.wtf/tags/troubleshooting" term="troubleshooting" label="troubleshooting"/><category scheme="https://jaas.projects.metio.wtf/tags/rbac" term="rbac" label="rbac"/></entry><entry><title type="html">Workqueue saturation</title><link href="https://jaas.projects.metio.wtf/runbooks/workqueue-saturation/?utm_source=atom_feed" rel="alternate" type="text/html"/><link href="https://jaas.projects.metio.wtf/runbooks/reconcile-latency/?utm_source=atom_feed" rel="related" type="text/html" title="High reconcile latency"/><link href="https://jaas.projects.metio.wtf/runbooks/artifacttoolarge/?utm_source=atom_feed" rel="related" type="text/html" title="ArtifactTooLarge"/><link href="https://jaas.projects.metio.wtf/runbooks/crd-watch-engagement/?utm_source=atom_feed" rel="related" type="text/html" title="CRD watch engagement failing"/><link href="https://jaas.projects.metio.wtf/runbooks/crossnamespacerefrejected/?utm_source=atom_feed" rel="related" type="text/html" title="CrossNamespaceRefRejected"/><link href="https://jaas.projects.metio.wtf/runbooks/dependencycycle/?utm_source=atom_feed" rel="related" type="text/html" title="DependencyCycle"/><id>https://jaas.projects.metio.wtf/runbooks/workqueue-saturation/</id><published>0001-01-01T00:00:00+00:00</published><updated>2026-06-17T10:56:50+02:00</updated><content type="html"><![CDATA[<blockquote>The reconciler&rsquo;s workqueue depth exceeds the threshold because the operator is dequeuing reconciles slower than the apiserver enqueues them</blockquote><p>Linked from the <code>JaaSControllerWorkqueueDepthHigh</code> alert. Fires when the reconciler&rsquo;s workqueue holds more items than the configured threshold (default 50) for the alert window. Not tied to a <code>Reason</code> constant — workqueue depth is a controller-runtime signal, not a per-snippet status.</p>
<h2 id="symptom">Symptom</h2>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">ALERTS{alertname=&#34;JaaSControllerWorkqueueDepthHigh&#34;, controller=&#34;jsonnetsnippet&#34;}
</span></span></code></pre></div><ul>
<li>New snippet writes settle slowly (status takes minutes to flip, not seconds).</li>
<li>Existing snippets re-render later than <code>spec.interval</code> would suggest.</li>
<li><code>kubectl describe jsonnetsnippet</code> shows a stale <code>ObservedGeneration</code>.</li>
</ul>
<h2 id="cause">Cause</h2>
<p>The operator is dequeuing reconciles slower than the API server enqueues them. Common causes, in observed-frequency order:</p>
<ol>
<li><strong>Slow Publisher backend.</strong> S3 throttling, a slow PVC, or a stalled object-store transaction — each reconcile blocks on the storage <code>Put</code>.</li>
<li><strong>API server pressure.</strong> The cluster&rsquo;s apiserver is slow on <code>GET</code> / <code>UPDATE</code> (often during a control-plane upgrade or under heavy general load).</li>
<li><strong>Per-snippet rate-limiter exhaustion.</strong> A flapping snippet eats its token-bucket budget; the controller&rsquo;s exponential backoff stretches the queue.</li>
<li><strong>A large fan-out from a single source watch.</strong> One Flux source republishes and 100 snippets reference it; every snippet&rsquo;s reconcile lands in the queue at once.</li>
<li><strong>Webhook latency.</strong> When <code>--enable-webhook</code> is on, every snippet write traverses the validating webhook. A wedged webhook (cert issue, slow tenant client) holds the apiserver&rsquo;s call open and indirectly enlarges the queue.</li>
</ol>
<h2 id="diagnosis">Diagnosis</h2>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl"><span class="c1"># Per-controller queue depth — confirm which controller is saturated</span>
</span></span><span class="line"><span class="cl">kubectl --namespace &lt;jaas-ns&gt; port-forward svc/jaas-metrics 8083:8083 <span class="p">&amp;</span>
</span></span><span class="line"><span class="cl">curl -s localhost:8083/metrics <span class="p">|</span> grep -E <span class="s1">&#39;workqueue_depth|workqueue_adds_total&#39;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Reconcile-time histogram — separates &#34;lots of queued items&#34; (fan-out)</span>
</span></span><span class="line"><span class="cl"><span class="c1"># from &#34;each reconcile is slow&#34; (storage / apiserver).</span>
</span></span><span class="line"><span class="cl">curl -s localhost:8083/metrics <span class="p">|</span> grep <span class="s1">&#39;controller_runtime_reconcile_time_seconds&#39;</span>
</span></span></code></pre></div><p>Cross-reference operator logs for the slow path:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">kubectl --namespace &lt;jaas-ns&gt; logs deploy/jaas --tail<span class="o">=</span><span class="m">500</span> <span class="se">\
</span></span></span><span class="line"><span class="cl">  <span class="p">|</span> grep -E <span class="s1">&#39;reconcile|publisher|s3|webhook&#39;</span>
</span></span></code></pre></div><p>If <code>controller_runtime_reconcile_time_seconds</code> p99 is also high, the alert is the symptom — <code>JaaSReconcileLatencyHigh</code> is the more useful page; see <a href="/runbooks/reconcile-latency/">reconcile-latency.md</a>
.</p>
<h2 id="remediation">Remediation</h2>
<ul>
<li><strong>Storage backend slow.</strong> Switch from <code>local</code> (PVC) to <code>s3</code> for higher write throughput, or vice versa if S3 is throttled. See <a href="/runbooks/storage-recovery/">storage-recovery.md</a>
.</li>
<li><strong>Apiserver slow.</strong> Pause spec-update churn (<code>spec.interval</code> longer on hot snippets), then wait for control-plane health to return.</li>
<li><strong>Rate-limiter exhaustion.</strong> Increase <code>operator.rerenderBurst</code> to absorb the spike, then investigate why a snippet is flapping (typically a <code>Reason*</code> other than <code>Synced</code> keeps firing — check <code>kubectl get events</code>).</li>
<li><strong>Fan-out from a single source.</strong> Stagger snippet intervals so their watch events don&rsquo;t all settle at once. The controller serializes per-snippet; concurrency across snippets is bounded by <code>MaxConcurrentReconciles</code> (set high enough at chart default — 5 — that drag from a single fan-out is unusual).</li>
<li><strong>Webhook latency.</strong> <code>kubectl get validatingwebhookconfiguration jaas-jsonnetsnippet --output yaml</code> and confirm the <code>caBundle</code> is current; restart the operator if the cert was rotated externally.</li>
</ul>
<h2 id="prevention">Prevention</h2>
<ul>
<li>Run <code>operator.metrics.prometheusRule.enabled: true</code> so this alert fires <em>before</em> downstream consumers notice.</li>
<li>Cap <code>--max-artifact-bytes</code> so a runaway snippet can&rsquo;t slow every Publisher write behind it.</li>
<li>For multi-replica HA, leader election keeps only one replica reconciling — workqueue depth on the lease-holder is the only one that matters.</li>
</ul>
]]></content><category scheme="https://jaas.projects.metio.wtf/tags/runbooks" term="runbooks" label="runbooks"/><category scheme="https://jaas.projects.metio.wtf/tags/troubleshooting" term="troubleshooting" label="troubleshooting"/><category scheme="https://jaas.projects.metio.wtf/tags/metrics" term="metrics" label="metrics"/></entry></feed>