<?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">StageSet Controller</title><link href="https://stageset.projects.metio.wtf/" rel="alternate" type="text/html" title="html"/><link href="https://stageset.projects.metio.wtf/index.xml" rel="alternate" type="application/rss+xml" title="rss"/><link href="https://stageset.projects.metio.wtf/atom.xml" rel="self" type="application/atom+xml" title="atom"/><link href="https://stageset.projects.metio.wtf/humans.txt" rel="alternate" type="text/plain" title="humans"/><link href="https://stageset.projects.metio.wtf/foaf.rdf" rel="alternate" type="application/rdf+xml" title="foaf"/><link href="https://stageset.projects.metio.wtf/llms-full.txt" rel="alternate" type="text/plain" title="llms"/><updated>2026-06-16T12:40:10+00:00</updated><author><name>metio.wtf</name></author><id>https://stageset.projects.metio.wtf/</id><entry><title type="html">Actions</title><link href="https://stageset.projects.metio.wtf/usage/actions/?utm_source=atom_feed" rel="alternate" type="text/html"/><link href="https://stageset.projects.metio.wtf/usage/ready-checks/?utm_source=atom_feed" rel="related" type="text/html" title="Ready checks"/><link href="https://stageset.projects.metio.wtf/runbooks/stagefailed/?utm_source=atom_feed" rel="related" type="text/html" title="StageFailed"/><link href="https://stageset.projects.metio.wtf/usage/conflict-policies/?utm_source=atom_feed" rel="related" type="text/html" title="Conflict policies"/><link href="https://stageset.projects.metio.wtf/runbooks/dependencynotready/?utm_source=atom_feed" rel="related" type="text/html" title="DependencyNotReady"/><link href="https://stageset.projects.metio.wtf/tutorials/jsonnet-to-rollout/?utm_source=atom_feed" rel="related" type="text/html" title="From Jsonnet to a gated rollout"/><id>https://stageset.projects.metio.wtf/usage/actions/</id><published>0001-01-01T00:00:00+00:00</published><updated>2026-06-16T13:41:17+02:00</updated><content type="html"><![CDATA[<blockquote>Typed pre, post, and on-failure steps — jobs, HTTP gates, waits, patches, deletes, transient applies.</blockquote><p>Actions are typed steps the controller runs around a stage&rsquo;s apply. They turn an
ordered apply into an orchestrated rollout — run a migration before the app, gate
the stage on an external check, clean up on failure.</p>
<p>A stage has three action hooks:</p>
<ul>
<li><strong><code>pre</code></strong> — run before the manifests are built and applied. A failure aborts the
stage with nothing applied.</li>
<li><strong><code>post</code></strong> — run after the apply is verified. The stage is <code>Ready</code> only if these
all succeed.</li>
<li><strong><code>onFailure</code></strong> — best-effort steps run on any failure from the apply onward.</li>
</ul>
<p>Each action has a <code>name</code>, optional <code>timeout</code> and <code>retries</code>, and <strong>exactly one</strong>
operation type (<code>patch</code>, <code>http</code>, <code>wait</code>, <code>job</code>, <code>delete</code>, or <code>apply</code>) — enforced
by the validating admission webhook. Actions within a hook run in list order.</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">stages</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">application</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">name</span><span class="p">:</span><span class="w"> </span><span class="l">my-app</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">actions</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">pre</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">db-migrate</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span><span class="nt">timeout</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">job</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">name</span><span class="p">:</span><span class="w"> </span><span class="l">my-app-migrations   </span><span class="w"> </span><span class="c"># render &amp; await Jobs from this artifact</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">post</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">smoke-test</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span><span class="nt">retries</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">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">url</span><span class="p">:</span><span class="w"> </span><span class="l">https://my-app.internal/healthz</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">              </span><span class="nt">expectedStatus</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="m">200</span><span class="p">]</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">onFailure</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">page-oncall</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">url</span><span class="p">:</span><span class="w"> </span><span class="l">https://alerts.internal/stageset-failed</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">              </span><span class="nt">method</span><span class="p">:</span><span class="w"> </span><span class="l">POST</span><span class="w">
</span></span></span></code></pre></div><h2 id="the-six-action-types">The six action types</h2>
<h3 id="job"><code>job</code></h3>
<p>Render and await Kubernetes Jobs from an artifact. The classic use is a database
migration that must complete before the app is applied.</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">name</span><span class="p">:</span><span class="w"> </span><span class="l">db-migrate</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">job</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">name</span><span class="p">:</span><span class="w"> </span><span class="l">my-app-migrations</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">./jobs</span><span class="w">
</span></span></span></code></pre></div><h3 id="http"><code>http</code></h3>
<p>Call an HTTP endpoint and gate on the response — an approval webhook, a smoke
test, an external readiness probe. Hosts must be permitted by the controller&rsquo;s
<code>--allowed-action-hosts</code>; loopback and link-local are always denied. <code>method</code>
defaults to <code>POST</code>; <code>expectedStatus</code> defaults to any <code>2xx</code>. The body and headers
can be read from a <code>Secret</code> so tokens never sit in the spec:</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">name</span><span class="p">:</span><span class="w"> </span><span class="l">smoke-test</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">url</span><span class="p">:</span><span class="w"> </span><span class="l">https://my-app.internal/healthz</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">method</span><span class="p">:</span><span class="w"> </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">expectedStatus</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="m">200</span><span class="p">]</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">headersFrom</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">gate-token      </span><span class="w"> </span><span class="c"># Secret name</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">authorization    </span><span class="w"> </span><span class="c"># the key names the header; its value is the value</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">bodyFrom</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">gate-payload      </span><span class="w"> </span><span class="c"># Secret name</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">body               </span><span class="w"> </span><span class="c"># this key&#39;s value becomes the request body</span><span class="w">
</span></span></span></code></pre></div><h3 id="wait"><code>wait</code></h3>
<p>Block for a fixed duration, or until a <a href="https://github.com/google/cel-spec">CEL</a>
expression holds against a target 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">name</span><span class="p">:</span><span class="w"> </span><span class="l">settle</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">wait</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">duration</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="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">until-available</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">wait</span><span class="p">:</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">apiVersion</span><span class="p">:</span><span class="w"> </span><span class="l">apps/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">Deployment</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">web</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">expr</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;status.availableReplicas &gt;= 3&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">timeout</span><span class="p">:</span><span class="w"> </span><span class="l">5m</span><span class="w">
</span></span></span></code></pre></div><h3 id="patch"><code>patch</code></h3>
<p>Patch an existing object — flip a feature flag, scale something, annotate. <code>type</code>
is <code>merge</code> (default) for a strategic-merge patch, or <code>json6902</code> for a JSON Patch:</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">name</span><span class="p">:</span><span class="w"> </span><span class="l">enable-traffic</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">patch</span><span class="p">:</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">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="w">      </span><span class="nt">kind</span><span class="p">:</span><span class="w"> </span><span class="l">Service</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">web</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">merge              </span><span class="w"> </span><span class="c"># default; or json6902</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">patch</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">      { &#34;spec&#34;: { &#34;selector&#34;: { &#34;release&#34;: &#34;green&#34; } } }</span><span class="w">
</span></span></span></code></pre></div><h3 id="delete"><code>delete</code></h3>
<p>Remove an existing object; a missing object counts as success.</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">name</span><span class="p">:</span><span class="w"> </span><span class="l">drop-old-job</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">delete</span><span class="p">:</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">apiVersion</span><span class="p">:</span><span class="w"> </span><span class="l">batch/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">Job</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">legacy-migration</span><span class="w">
</span></span></span></code></pre></div><h3 id="apply"><code>apply</code></h3>
<p>Apply transient, rollout-scoped manifests that are <strong>not</strong> inventory-tracked and
are never pruned — a maintenance page, a one-shot canary, a temporary config. With
<code>wait: true</code> the action blocks until the applied objects report Ready (kstatus),
bounded by the action <code>timeout</code>, so a following <code>patch</code> can repoint traffic only
once the resource is serving.</p>
<p>Because the applied objects are never pruned by the inventory diff, stand a
resource up only for the duration of a rollout by pairing an <code>apply</code> in <code>pre</code> with
a matching <code>delete</code> in <code>post</code>, and guard a mid-run crash with an <code>onFailure</code>
delete:</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">actions</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">pre</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">stand-up-maintenance-page</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">apply</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">name</span><span class="p">:</span><span class="w"> </span><span class="l">maintenance-page   </span><span class="w"> </span><span class="c"># an ExternalArtifact holding a Pod + Service</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">wait</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="w">                  </span><span class="c"># block until it is serving</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">post</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">tear-down-maintenance-page</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">delete</span><span class="p">:</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">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="w">          </span><span class="nt">kind</span><span class="p">:</span><span class="w"> </span><span class="l">Pod</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">maintenance-page</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">onFailure</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">tear-down-maintenance-page-on-failure</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">delete</span><span class="p">:</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">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="w">          </span><span class="nt">kind</span><span class="p">:</span><span class="w"> </span><span class="l">Pod</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">maintenance-page</span><span class="w">
</span></span></span></code></pre></div><p>The action ledger gates each step per pinned revision, so a retry or controller
restart never re-applies or re-deletes the resource for the same snapshot.</p>
<p>To run a <code>job</code> action only when the deployed version crosses a release boundary,
see <a href="/usage/versioned-migrations/">versioned migrations</a>.</p>
]]></content><category scheme="https://stageset.projects.metio.wtf/tags/actions" term="actions" label="actions"/><category scheme="https://stageset.projects.metio.wtf/tags/stages" term="stages" label="stages"/><category scheme="https://stageset.projects.metio.wtf/tags/ready-checks" term="ready-checks" label="ready-checks"/></entry><entry><title type="html">ArtifactNotFound</title><link href="https://stageset.projects.metio.wtf/runbooks/artifactnotfound/?utm_source=atom_feed" rel="alternate" type="text/html"/><link href="https://stageset.projects.metio.wtf/runbooks/resolvefailed/?utm_source=atom_feed" rel="related" type="text/html" title="ResolveFailed"/><link href="https://stageset.projects.metio.wtf/runbooks/sourcenotready/?utm_source=atom_feed" rel="related" type="text/html" title="SourceNotReady"/><link href="https://stageset.projects.metio.wtf/runbooks/controller-pod-down/?utm_source=atom_feed" rel="related" type="text/html" title="Controller pod down"/><link href="https://stageset.projects.metio.wtf/runbooks/dependencynotready/?utm_source=atom_feed" rel="related" type="text/html" title="DependencyNotReady"/><link href="https://stageset.projects.metio.wtf/runbooks/downgraderequiresmigration/?utm_source=atom_feed" rel="related" type="text/html" title="DowngradeRequiresMigration"/><id>https://stageset.projects.metio.wtf/runbooks/artifactnotfound/</id><published>0001-01-01T00:00:00+00:00</published><updated>2026-06-16T13:41:17+02:00</updated><content type="html"><![CDATA[<blockquote>The referenced ExternalArtifact could not be found (transient; the controller requeues).</blockquote><h2 id="symptom">Symptom</h2>
<p><code>READY=False</code>, <code>REASON=ArtifactNotFound</code>. Transient: the controller requeues in case the artifact appears.</p>
<h2 id="cause">Cause</h2>
<p>A stage&rsquo;s <code>sourceRef</code> resolves to <strong>no <code>ExternalArtifact</code></strong>. Either:</p>
<ul>
<li>a <strong>direct</strong> <code>sourceRef</code> (<code>kind: ExternalArtifact</code>, the default) names an object that does not exist in the target namespace; or</li>
<li>a <strong>producer</strong> <code>sourceRef</code> (e.g. <code>kind: JsonnetSnippet</code>) exists, but no <code>ExternalArtifact</code> carries a <code>spec.sourceRef</code> back-pointer to it yet — the producer has not created its artifact object.</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 stageset &lt;name&gt; -n &lt;namespace&gt;     <span class="c1"># Message names the missing ref</span>
</span></span><span class="line"><span class="cl">kubectl get externalartifact -n &lt;namespace&gt;
</span></span></code></pre></div><p>For a producer ref, confirm the producer object exists and that it is configured to publish an <code>ExternalArtifact</code> (not only serve over HTTP):</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">kubectl get &lt;producer-kind&gt; &lt;name&gt; -n &lt;namespace&gt; -o yaml
</span></span></code></pre></div><h2 id="remediation">Remediation</h2>
<ul>
<li>Fix a typo in <code>sourceRef.name</code> / <code>sourceRef.namespace</code>.</li>
<li>For a direct ref, create (or wait for) the named <code>ExternalArtifact</code>.</li>
<li>For a producer ref, ensure the producer actually publishes an artifact and that it lands in the same namespace as the StageSet (cross-namespace producer refs are gated by <code>--no-cross-namespace-refs</code>).</li>
</ul>
<p>If the artifact exists but is not yet published, the reason is <a href="/runbooks/sourcenotready/"><code>SourceNotReady</code></a>; a spec/API resolution failure is <a href="/runbooks/resolvefailed/"><code>ResolveFailed</code></a>. See <a href="/usage/stages-and-sources/">stages and sources</a>.</p>
]]></content><category scheme="https://stageset.projects.metio.wtf/tags/runbooks" term="runbooks" label="runbooks"/><category scheme="https://stageset.projects.metio.wtf/tags/externalartifact" term="externalartifact" label="externalartifact"/><category scheme="https://stageset.projects.metio.wtf/tags/sources" term="sources" label="sources"/><category scheme="https://stageset.projects.metio.wtf/tags/troubleshooting" term="troubleshooting" label="troubleshooting"/></entry><entry><title type="html">Building and testing</title><link href="https://stageset.projects.metio.wtf/contributing/building/?utm_source=atom_feed" rel="alternate" type="text/html"/><link href="https://stageset.projects.metio.wtf/contributing/ci-and-release/?utm_source=atom_feed" rel="related" type="text/html" title="CI and releases"/><link href="https://stageset.projects.metio.wtf/cli/diff/?utm_source=atom_feed" rel="related" type="text/html" title="stagesetctl diff"/><id>https://stageset.projects.metio.wtf/contributing/building/</id><published>0001-01-01T00:00:00+00:00</published><updated>2026-06-16T13:41:17+02:00</updated><content type="html"><![CDATA[<blockquote>Build the controller, run the test suite, and pass the static-analysis gate.</blockquote><p>The controller is a standard Go module. With a Go toolchain installed:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">go build ./...
</span></span><span class="line"><span class="cl">go <span class="nb">test</span> -race -cover ./...
</span></span></code></pre></div><h2 id="test-layers">Test layers</h2>
<ul>
<li><strong>Unit tests</strong> sit next to the code across <code>internal/...</code> and <code>api/v1/</code>. Several
are drift gates — e.g. <code>conditions_test.go</code> asserts every Ready <code>Reason</code> has a
matching runbook page under <code>docs/content/runbooks/</code>.</li>
<li><strong>envtest-backed tests</strong> (<code>envtest_*_test.go</code>) boot a real kube-apiserver + etcd
via controller-runtime&rsquo;s <code>envtest</code>. They <code>t.Skip</code> unless <code>KUBEBUILDER_ASSETS</code>
points at an asset bundle — install it with
<a href="https://book.kubebuilder.io/reference/envtest.html"><code>setup-envtest</code></a>.</li>
<li><strong>Fuzz tests</strong> (<code>FuzzXxx</code>) harden the parsing-heavy paths; their seed corpus runs
as ordinary unit tests, and <code>-fuzz</code> fuzzes for real.</li>
<li><strong>Kind smoke</strong> scenarios under <code>hack/smoke/</code> run the controller end to end
against a real kind cluster.</li>
</ul>
<h2 id="static-analysis">Static analysis</h2>
<p>A pull request must be clean under each of these — run them locally before
pushing:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">go vet ./...
</span></span><span class="line"><span class="cl">go run honnef.co/go/tools/cmd/staticcheck@latest ./...
</span></span><span class="line"><span class="cl">go run github.com/securego/gosec/v2/cmd/gosec@latest ./...
</span></span><span class="line"><span class="cl">go run golang.org/x/vuln/cmd/govulncheck@latest ./...
</span></span><span class="line"><span class="cl">go run mvdan.cc/gofumpt@latest -l .          <span class="c1"># empty output == formatted</span>
</span></span><span class="line"><span class="cl">go run github.com/fe3dback/arch-go@latest    <span class="c1"># architecture rules (arch-go.yml)</span>
</span></span></code></pre></div><h2 id="containerized-dev-shell">Containerized dev shell</h2>
<p>The toolchain — including the pinned <code>setup-envtest</code> assets — is also packaged in
a container via <a href="https://ilo.projects.metio.wtf/">ilo</a>, so you can build and test
without installing anything on the host:</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 -race -cover ./...&#39;</span>
</span></span></code></pre></div>]]></content><category scheme="https://stageset.projects.metio.wtf/tags/contributing" term="contributing" label="contributing"/><category scheme="https://stageset.projects.metio.wtf/tags/ci" term="ci" label="ci"/></entry><entry><title type="html">CI and releases</title><link href="https://stageset.projects.metio.wtf/contributing/ci-and-release/?utm_source=atom_feed" rel="alternate" type="text/html"/><link href="https://stageset.projects.metio.wtf/contributing/building/?utm_source=atom_feed" rel="related" type="text/html" title="Building and testing"/><link href="https://stageset.projects.metio.wtf/cli/diff/?utm_source=atom_feed" rel="related" type="text/html" title="stagesetctl diff"/><id>https://stageset.projects.metio.wtf/contributing/ci-and-release/</id><published>0001-01-01T00:00:00+00:00</published><updated>2026-06-16T13:41:17+02:00</updated><content type="html"><![CDATA[<blockquote>The pull-request gates and the automated calendar-based release process.</blockquote><h2 id="continuous-integration">Continuous integration</h2>
<p>Every pull request runs <code>verify.yml</code>, which fans out into one job per concern so a
failure points straight at the cause:</p>
<ul>
<li><strong>test</strong> — <code>go build</code> then the full <code>go test</code> suite.</li>
<li><strong>lint-go</strong> — <code>go vet</code>, <code>staticcheck</code>, <code>gosec</code>, and a <code>gofumpt</code> formatting check.</li>
<li><strong>vulnerabilities</strong> — <code>govulncheck</code> (a reachable advisory is a hard gate).</li>
<li><strong>architecture</strong> — <code>arch-go</code> against <code>arch-go.yml</code>.</li>
<li><strong>reuse</strong> — SPDX/REUSE compliance on every file.</li>
<li><strong>text linters</strong> — <code>yamllint</code>, <code>actionlint</code>, <code>markdownlint</code>, <code>typos</code>.</li>
<li><strong>container-image</strong> — a buildx image build plus a Trivy scan.</li>
</ul>
<p>A single <strong>all-green</strong> job depends on every other job and is the only required
check, so new jobs are covered automatically. A separate <code>kind-smoke.yml</code> runs the
operator end to end against a real kind cluster, and <code>fuzz.yml</code> exercises the fuzz
targets.</p>
<h2 id="releases">Releases</h2>
<p>Releases are <strong>calendar-based and fully automated</strong> — there is no semver tag to
bump by hand. <code>release.yml</code> runs on a Monday cron (and on manual dispatch), and the
version is the run date (<code>date +'%Y.%-m.%-d'</code>, e.g. <code>2026.6.15</code>). A prepare job
counts commits since the last release; an empty week publishes nothing.</p>
<p>The pipeline is hand-rolled — no goreleaser, no GPG:</p>
<ul>
<li>Binaries are cross-compiled with <code>go build</code> (<code>CGO_ENABLED=0</code>, <code>-trimpath</code>,
<code>-ldflags</code>) and archived per platform.</li>
<li>A multi-arch image is pushed to <code>ghcr.io/metio/stageset-controller</code> and signed
with <strong>cosign keyless</strong> (Fulcio OIDC) by digest.</li>
<li>The GitHub Release attaches the archives, a <code>SHA256SUMS</code> file, and its cosign
signature; identity is proven by the workflow&rsquo;s OIDC certificate, so there is no
key to distribute.</li>
</ul>
<p>The Helm chart lives in the <a href="https://github.com/metio/helm-charts">metio/helm-charts</a>
repository and vendors this repo&rsquo;s CRDs at each release; its <code>appVersion</code> tracks the
binary&rsquo;s releases.</p>
]]></content><category scheme="https://stageset.projects.metio.wtf/tags/contributing" term="contributing" label="contributing"/><category scheme="https://stageset.projects.metio.wtf/tags/ci" term="ci" label="ci"/><category scheme="https://stageset.projects.metio.wtf/tags/release" term="release" label="release"/></entry><entry><title type="html">Conflict policies</title><link href="https://stageset.projects.metio.wtf/usage/conflict-policies/?utm_source=atom_feed" rel="alternate" type="text/html"/><link href="https://stageset.projects.metio.wtf/usage/actions/?utm_source=atom_feed" rel="related" type="text/html" title="Actions"/><link href="https://stageset.projects.metio.wtf/runbooks/dependencynotready/?utm_source=atom_feed" rel="related" type="text/html" title="DependencyNotReady"/><link href="https://stageset.projects.metio.wtf/tutorials/jsonnet-to-rollout/?utm_source=atom_feed" rel="related" type="text/html" title="From Jsonnet to a gated rollout"/><link href="https://stageset.projects.metio.wtf/usage/producer-aware-sources/?utm_source=atom_feed" rel="related" type="text/html" title="Producer-aware sources"/><link href="https://stageset.projects.metio.wtf/tutorials/progressive-delivery/?utm_source=atom_feed" rel="related" type="text/html" title="Progressive delivery"/><id>https://stageset.projects.metio.wtf/usage/conflict-policies/</id><published>0001-01-01T00:00:00+00:00</published><updated>2026-06-16T13:41:17+02:00</updated><content type="html"><![CDATA[<blockquote>Resolve immutable-field and ownership conflicts per resource.</blockquote><p>Conflict policies decide what happens when an apply hits an immutable-field
conflict — a changed <code>clusterIP</code>, a <code>Job</code> pod template, a <code>StorageClass</code> field
that can&rsquo;t be updated in place. By default the controller fails the stage and
reports it, so nothing destructive happens by surprise. A policy opts specific
resources into automatic resolution.</p>
<h2 id="the-three-actions">The three actions</h2>
<ul>
<li><code>Fail</code> — stop and report (the default; safest).</li>
<li><code>Recreate</code> — delete and re-create the object to get past an immutable-field
change.</li>
<li><code>KeepExisting</code> — leave the live object as-is and move on.</li>
</ul>
<h2 id="a-default-for-the-whole-stage">A default for the whole stage</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">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">stages</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">app</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">name</span><span class="p">:</span><span class="w"> </span><span class="l">my-app</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">conflictPolicy</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">default</span><span class="p">:</span><span class="w"> </span><span class="l">Fail           </span><span class="w"> </span><span class="c"># explicit; the safe default</span><span class="w">
</span></span></span></code></pre></div><p>The <code>force: true</code> shorthand on a stage is equivalent to
<code>conflictPolicy.default: Recreate</code>.</p>
<h2 id="per-resource-rules">Per-resource rules</h2>
<p>Rules recreate exactly the resources that need it while everything else stays
<code>Fail</code>. A rule&rsquo;s <code>target</code> is a partial selector — any field you omit matches
everything. Rules are evaluated in list order; the <strong>first</strong> rule whose target
matches wins, and an object matching no rule falls back to <code>default</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="w">      </span><span class="nt">conflictPolicy</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">default</span><span class="p">:</span><span class="w"> </span><span class="l">Fail</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="c"># a Job&#39;s pod template is immutable — recreate it on change</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">apiVersion</span><span class="p">:</span><span class="w"> </span><span class="l">batch/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">Job</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span><span class="nt">action</span><span class="p">:</span><span class="w"> </span><span class="l">Recreate</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="c"># never fight an HPA over replica counts</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">kind</span><span class="p">:</span><span class="w"> </span><span class="l">Deployment</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">web</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span><span class="nt">action</span><span class="p">:</span><span class="w"> </span><span class="l">KeepExisting</span><span class="w">
</span></span></span></code></pre></div><h2 id="recreating-storage">Recreating storage</h2>
<p>Recreating a <code>PersistentVolumeClaim</code> or <code>PersistentVolume</code> destroys data, so a
<code>Recreate</code> <strong>rule</strong> targeting one is refused unless you explicitly accept the loss:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><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">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">kind</span><span class="p">:</span><span class="w"> </span><span class="l">PersistentVolumeClaim</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">scratch</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span><span class="nt">action</span><span class="p">:</span><span class="w"> </span><span class="l">Recreate</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span><span class="nt">allowDataLoss</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="w">     </span><span class="c"># required for PVC/PV Recreate, refused otherwise</span><span class="w">
</span></span></span></code></pre></div><p>Without <code>allowDataLoss: true</code>, a <code>Recreate</code> rule targeting a PVC/PV is rejected —
a guardrail against accidentally wiping a volume.</p>
]]></content><category scheme="https://stageset.projects.metio.wtf/tags/conflict-policies" term="conflict-policies" label="conflict-policies"/><category scheme="https://stageset.projects.metio.wtf/tags/stages" term="stages" label="stages"/></entry><entry><title type="html">Controller pod down</title><link href="https://stageset.projects.metio.wtf/runbooks/controller-pod-down/?utm_source=atom_feed" rel="alternate" type="text/html"/><link href="https://stageset.projects.metio.wtf/installation/operations/?utm_source=atom_feed" rel="related" type="text/html" title="Operations"/><link href="https://stageset.projects.metio.wtf/runbooks/reconcile-latency/?utm_source=atom_feed" rel="related" type="text/html" title="Reconcile latency high"/><link href="https://stageset.projects.metio.wtf/runbooks/suspended/?utm_source=atom_feed" rel="related" type="text/html" title="Suspended"/><link href="https://stageset.projects.metio.wtf/runbooks/webhook-cert-renewal/?utm_source=atom_feed" rel="related" type="text/html" title="Webhook cert renewal failing"/><link href="https://stageset.projects.metio.wtf/runbooks/workqueue-saturation/?utm_source=atom_feed" rel="related" type="text/html" title="Workqueue saturation"/><id>https://stageset.projects.metio.wtf/runbooks/controller-pod-down/</id><published>0001-01-01T00:00:00+00:00</published><updated>2026-06-16T13:41:17+02:00</updated><content type="html"><![CDATA[<blockquote>A stageset-controller pod has been NotReady for the alert window.</blockquote><h2 id="symptom">Symptom</h2>
<p>A <code>stageset-controller</code> pod is <code>NotReady</code>; the <code>StageSetControllerPodDown</code> alert
fires. While no replica is Ready, StageSets are not reconciled and the
<a href="https://kubernetes.io/docs/">Kubernetes</a> admission webhook may reject <code>StageSet</code>
writes (<code>failurePolicy: Fail</code>).</p>
<h2 id="cause">Cause</h2>
<ul>
<li>a crash-looping container (bad config flag, missing RBAC, panic),</li>
<li>the node draining or out of resources,</li>
<li>a failing readiness probe (<code>/readyz</code> on <code>--health-probe-bind-address</code>),</li>
<li>the leader-election lease unobtainable.</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 -n stageset-system get pods -l app.kubernetes.io/name<span class="o">=</span>stageset-controller
</span></span><span class="line"><span class="cl">kubectl -n stageset-system describe pod &lt;pod&gt;
</span></span><span class="line"><span class="cl">kubectl -n stageset-system logs &lt;pod&gt; --previous --tail<span class="o">=</span><span class="m">200</span>
</span></span></code></pre></div><p>Look for flag-parse errors at startup, RBAC <code>Forbidden</code> on the controller&rsquo;s own
<code>ServiceAccount</code>, or OOMKills.</p>
<h2 id="remediation">Remediation</h2>
<ul>
<li>Fix the surfaced cause (correct the flag/values, grant the missing controller
RBAC, raise resource limits).</li>
<li>Run more than one replica with leader election so a single pod failure doesn&rsquo;t
stop reconciliation — see <a href="/installation/production/#high-availability">production</a>.</li>
<li>If admission is blocking writes during the outage and you must unblock urgently,
scope or relax the webhook <code>failurePolicy</code>, then restore it once the controller
is healthy.</li>
</ul>
<p>See <a href="/installation/operations/">operations</a> for the full alert set and its thresholds.</p>
]]></content><category scheme="https://stageset.projects.metio.wtf/tags/runbooks" term="runbooks" label="runbooks"/><category scheme="https://stageset.projects.metio.wtf/tags/operations" term="operations" label="operations"/><category scheme="https://stageset.projects.metio.wtf/tags/alerts" term="alerts" label="alerts"/><category scheme="https://stageset.projects.metio.wtf/tags/troubleshooting" term="troubleshooting" label="troubleshooting"/></entry><entry><title type="html">DependencyNotReady</title><link href="https://stageset.projects.metio.wtf/runbooks/dependencynotready/?utm_source=atom_feed" rel="alternate" type="text/html"/><link href="https://stageset.projects.metio.wtf/runbooks/stagefailed/?utm_source=atom_feed" rel="related" type="text/html" title="StageFailed"/><link href="https://stageset.projects.metio.wtf/runbooks/stalled/?utm_source=atom_feed" rel="related" type="text/html" title="Stalled"/><link href="https://stageset.projects.metio.wtf/runbooks/artifactnotfound/?utm_source=atom_feed" rel="related" type="text/html" title="ArtifactNotFound"/><link href="https://stageset.projects.metio.wtf/runbooks/controller-pod-down/?utm_source=atom_feed" rel="related" type="text/html" title="Controller pod down"/><link href="https://stageset.projects.metio.wtf/runbooks/downgraderequiresmigration/?utm_source=atom_feed" rel="related" type="text/html" title="DowngradeRequiresMigration"/><id>https://stageset.projects.metio.wtf/runbooks/dependencynotready/</id><published>0001-01-01T00:00:00+00:00</published><updated>2026-06-16T13:41:17+02:00</updated><content type="html"><![CDATA[<blockquote>A StageSet named in spec.dependsOn is not yet Ready.</blockquote><h2 id="symptom">Symptom</h2>
<p><code>READY=False</code>, <code>REASON=DependencyNotReady</code>. Transient: the controller requeues at <code>spec.retryInterval</code> (or <code>spec.interval</code>).</p>
<h2 id="cause">Cause</h2>
<p>A StageSet listed in <code>spec.dependsOn</code> is not <code>Ready</code> at its observed generation, so this StageSet holds before doing any work. Semantics match kustomize-controller: a dependency is satisfied only when its <code>Ready=True</code> <strong>and</strong> its <code>status.observedGeneration</code> equals its current generation (so a freshly-edited dependency mid-reconcile does not count as ready).</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 stageset &lt;name&gt; -n &lt;namespace&gt;            <span class="c1"># Message names the dependency</span>
</span></span><span class="line"><span class="cl">kubectl get stageset &lt;dependency&gt; -n &lt;namespace&gt;           <span class="c1"># is it Ready?</span>
</span></span><span class="line"><span class="cl">kubectl describe stageset &lt;dependency&gt; -n &lt;namespace&gt;      <span class="c1"># why not?</span>
</span></span></code></pre></div><h2 id="remediation">Remediation</h2>
<p>Resolve the dependency&rsquo;s own Ready condition first (follow its runbook). Once it reports <code>Ready=True</code> at its current generation, this StageSet proceeds on the next reconcile. If the dependency is intentionally <a href="/runbooks/suspended/">suspended</a>, this StageSet waits indefinitely by design — remove the <code>dependsOn</code> entry or resume the dependency.</p>
<p>A <code>dependsOn</code> <strong>cycle</strong> is reported as <a href="/runbooks/stalled/"><code>Stalled</code></a>, not this reason.</p>
]]></content><category scheme="https://stageset.projects.metio.wtf/tags/runbooks" term="runbooks" label="runbooks"/><category scheme="https://stageset.projects.metio.wtf/tags/stages" term="stages" label="stages"/><category scheme="https://stageset.projects.metio.wtf/tags/troubleshooting" term="troubleshooting" label="troubleshooting"/></entry><entry><title type="html">DowngradeRequiresMigration</title><link href="https://stageset.projects.metio.wtf/runbooks/downgraderequiresmigration/?utm_source=atom_feed" rel="alternate" type="text/html"/><link href="https://stageset.projects.metio.wtf/runbooks/invalidversion/?utm_source=atom_feed" rel="related" type="text/html" title="InvalidVersion"/><link href="https://stageset.projects.metio.wtf/runbooks/artifactnotfound/?utm_source=atom_feed" rel="related" type="text/html" title="ArtifactNotFound"/><link href="https://stageset.projects.metio.wtf/runbooks/controller-pod-down/?utm_source=atom_feed" rel="related" type="text/html" title="Controller pod down"/><link href="https://stageset.projects.metio.wtf/runbooks/dependencynotready/?utm_source=atom_feed" rel="related" type="text/html" title="DependencyNotReady"/><link href="https://stageset.projects.metio.wtf/runbooks/invalidspec/?utm_source=atom_feed" rel="related" type="text/html" title="InvalidSpec"/><id>https://stageset.projects.metio.wtf/runbooks/downgraderequiresmigration/</id><published>0001-01-01T00:00:00+00:00</published><updated>2026-06-16T13:41:17+02:00</updated><content type="html"><![CDATA[<blockquote>The desired version is below the deployed version and a migration boundary blocks the downgrade.</blockquote><h2 id="symptom">Symptom</h2>
<p><code>READY=False</code>, <code>REASON=DowngradeRequiresMigration</code>. Terminal: the run does not requeue until the desired version is at or above <code>status.version</code>.</p>
<h2 id="cause">Cause</h2>
<p>The desired version (<code>spec.version</code>) is <strong>lower</strong> than the version the controller last recorded as deployed (<code>status.version</code>). Downgrades are refused by default: <a href="/usage/versioned-migrations/">migrations</a> are forward-only action ladders, and replaying upgrade migrations in reverse is how data gets destroyed. The controller does not silently run a downgrade.</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 stageset &lt;name&gt; -n &lt;namespace&gt;
</span></span><span class="line"><span class="cl">kubectl get stageset &lt;name&gt; -n &lt;namespace&gt; -o <span class="nv">jsonpath</span><span class="o">=</span><span class="s1">&#39;{.status.version}&#39;</span>   <span class="c1"># deployed</span>
</span></span><span class="line"><span class="cl"><span class="c1"># desired: read spec.version.value, or the version file the artifact carries</span>
</span></span></code></pre></div><h2 id="remediation">Remediation</h2>
<p>Pick the intended direction:</p>
<ul>
<li><strong>You did not mean to downgrade</strong> (e.g. a source revert pulled an older version file): roll the source forward again so the desired version is <code>&gt;=</code> the deployed version. The StageSet converges normally.</li>
<li><strong>You genuinely need to go back</strong>: a downgrade is an operational decision with potential data loss. Perform it deliberately — restore from backup or apply an explicit down-migration out of band — then set <code>status.version</code> to match. There is no automatic reverse-migration path by design.</li>
</ul>
]]></content><category scheme="https://stageset.projects.metio.wtf/tags/runbooks" term="runbooks" label="runbooks"/><category scheme="https://stageset.projects.metio.wtf/tags/migrations" term="migrations" label="migrations"/><category scheme="https://stageset.projects.metio.wtf/tags/versioning" term="versioning" label="versioning"/><category scheme="https://stageset.projects.metio.wtf/tags/troubleshooting" term="troubleshooting" label="troubleshooting"/></entry><entry><title type="html">From Jsonnet to a gated rollout</title><link href="https://stageset.projects.metio.wtf/tutorials/jsonnet-to-rollout/?utm_source=atom_feed" rel="alternate" type="text/html"/><link href="https://stageset.projects.metio.wtf/tutorials/parameters/?utm_source=atom_feed" rel="related" type="text/html" title="Parameterizing a rollout"/><link href="https://stageset.projects.metio.wtf/usage/producer-aware-sources/?utm_source=atom_feed" rel="related" type="text/html" title="Producer-aware sources"/><link href="https://stageset.projects.metio.wtf/tutorials/progressive-delivery/?utm_source=atom_feed" rel="related" type="text/html" title="Progressive delivery"/><link href="https://stageset.projects.metio.wtf/comparisons/tanka-kubecfg/?utm_source=atom_feed" rel="related" type="text/html" title="StageSet vs Tanka and kubecfg"/><link href="https://stageset.projects.metio.wtf/comparisons/jsonnet-controller/?utm_source=atom_feed" rel="related" type="text/html" title="StageSet vs jsonnet-controller"/><id>https://stageset.projects.metio.wtf/tutorials/jsonnet-to-rollout/</id><published>0001-01-01T00:00:00+00:00</published><updated>2026-06-16T13:41:17+02:00</updated><content type="html"><![CDATA[<blockquote>Write Jsonnet manifests, render them with JaaS, and roll them out with StageSet — end to end.</blockquote><p>This tutorial follows a complete delivery: write <a href="https://kubernetes.io/docs/">Kubernetes</a>
manifests in <a href="https://jsonnet.org/">Jsonnet</a> and publish the source through
<a href="https://fluxcd.io/">Flux</a>; <a href="https://jaas.projects.metio.wtf/">JaaS</a> renders it into a
Flux <code>ExternalArtifact</code>, and a StageSet rolls it out with a readiness gate.</p>
<p>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">Jsonnet in Git/OCI/Bucket  →  JaaS (JsonnetSnippet)  →  ExternalArtifact  →  StageSet
</span></span></code></pre></div><p>This tutorial renders <em>Jsonnet</em>, so it goes through JaaS: JaaS turns the Jsonnet
into an <code>ExternalArtifact</code> the stage consumes. (If your manifests were already plain
YAML, a stage could read a <code>GitRepository</code>/<code>OCIRepository</code>/<code>Bucket</code> directly — see
<a href="/tutorials/flux-sources/">Stage sources</a>. The renderer is here because the input is
Jsonnet, not because StageSet can&rsquo;t read Git.)</p>
<h2 id="prerequisites">Prerequisites</h2>
<ul>
<li>
<p>Flux installed (with the <code>ExternalArtifact</code> API — Flux ≥ v2.7.0).</p>
</li>
<li>
<p><a href="https://jaas.projects.metio.wtf/">JaaS</a> installed in operator mode.</p>
</li>
<li>
<p>StageSet installed (see <a href="/installation/kubernetes/">Installation</a>).</p>
</li>
<li>
<p>An <code>apps</code> namespace, and a <code>web-deployer</code> <code>ServiceAccount</code> in it whose RBAC can
apply the workload (the StageSet impersonates it):</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">kubectl create namespace apps
</span></span><span class="line"><span class="cl">kubectl -n apps create serviceaccount web-deployer
</span></span><span class="line"><span class="cl"><span class="c1"># bind web-deployer to a Role/ClusterRole that can manage Deployments and</span>
</span></span><span class="line"><span class="cl"><span class="c1"># Services in the apps namespace — see /usage/multi-cluster/ for the tenancy model</span>
</span></span></code></pre></div></li>
</ul>
<h2 id="1-write-the-manifests-in-jsonnet">1. Write the manifests in Jsonnet</h2>
<p>A small web app, parameterized as a Jsonnet top-level function so the same source
renders for any environment. Commit this as <code>jsonnet/main.jsonnet</code> in a Git repo:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-jsonnet" data-lang="jsonnet"><span class="line"><span class="cl"><span class="c1">// jsonnet/main.jsonnet
</span></span></span><span class="line"><span class="cl"><span class="k">function</span><span class="p">(</span><span class="nv">name</span><span class="o">=</span><span class="s">&#39;web&#39;</span><span class="p">,</span><span class="w"> </span><span class="nv">image</span><span class="o">=</span><span class="s">&#39;registry.internal/web:latest&#39;</span><span class="p">,</span><span class="w"> </span><span class="nv">replicas</span><span class="o">=</span><span class="s">&#39;2&#39;</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">apiVersion</span><span class="p">:</span><span class="w"> </span><span class="s">&#39;v1&#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">kind</span><span class="p">:</span><span class="w"> </span><span class="s">&#39;List&#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">items</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="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nv">apiVersion</span><span class="p">:</span><span class="w"> </span><span class="s">&#39;apps/v1&#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">kind</span><span class="p">:</span><span class="w"> </span><span class="s">&#39;Deployment&#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">metadata</span><span class="p">:</span><span class="w"> </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">name</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">spec</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">replicas</span><span class="p">:</span><span class="w"> </span><span class="nb">std.parseInt</span><span class="p">(</span><span class="nv">replicas</span><span class="p">),</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nv">selector</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nv">matchLabels</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nv">app</span><span class="p">:</span><span class="w"> </span><span class="nv">name</span><span class="w"> </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">template</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">metadata</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nv">labels</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nv">app</span><span class="p">:</span><span class="w"> </span><span class="nv">name</span><span class="w"> </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">spec</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nv">containers</span><span class="p">:</span><span class="w"> </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">name</span><span class="p">,</span><span class="w"> </span><span class="nv">image</span><span class="p">:</span><span class="w"> </span><span class="nv">image</span><span class="w"> </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="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><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">apiVersion</span><span class="p">:</span><span class="w"> </span><span class="s">&#39;v1&#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">kind</span><span class="p">:</span><span class="w"> </span><span class="s">&#39;Service&#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">metadata</span><span class="p">:</span><span class="w"> </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">name</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">spec</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nv">selector</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nv">app</span><span class="p">:</span><span class="w"> </span><span class="nv">name</span><span class="w"> </span><span class="p">},</span><span class="w"> </span><span class="nv">ports</span><span class="p">:</span><span class="w"> </span><span class="p">[{</span><span class="w"> </span><span class="nv">port</span><span class="p">:</span><span class="w"> </span><span class="mf">80</span><span class="p">,</span><span class="w"> </span><span class="nv">targetPort</span><span class="p">:</span><span class="w"> </span><span class="mf">8080</span><span class="w"> </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="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>Rendering a <code>kind: List</code> keeps several resources in one document — both the
kustomize build the controller runs and <code>kubectl</code> flatten it transparently.</p>
<h2 id="2-publish-the-source-through-flux">2. Publish the source through Flux</h2>
<p>Point a Flux <code>GitRepository</code> at the repo so the cluster has the 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">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">web-manifests</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">apps</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">1m</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/acme/web-manifests</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><p>Apply it and wait for the source to sync:</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 gitrepository.yaml
</span></span><span class="line"><span class="cl">kubectl -n apps <span class="nb">wait</span> --for<span class="o">=</span><span class="nv">condition</span><span class="o">=</span>Ready gitrepository/web-manifests
</span></span></code></pre></div><h2 id="3-render-with-jaas">3. Render with JaaS</h2>
<p>A <code>JsonnetSnippet</code> reads the Jsonnet from that source, passes the parameters as
top-level arguments, and publishes the rendered result as an <code>ExternalArtifact</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">web</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">apps</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">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">web-manifests</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">./jsonnet</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">tlas</span><span class="p">:</span><span class="w">                            </span><span class="c"># top-level args → the function() parameters</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="p">[</span><span class="s2">&#34;web&#34;</span><span class="p">]</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">image</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="s2">&#34;registry.internal/web:2.1.0&#34;</span><span class="p">]</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">replicas</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="s2">&#34;3&#34;</span><span class="p">]</span><span class="w">
</span></span></span></code></pre></div><p>Apply it; JaaS then publishes an <code>ExternalArtifact</code> named <code>web</code> in the <code>apps</code>
namespace. Confirm it went Ready:</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 jsonnetsnippet.yaml
</span></span><span class="line"><span class="cl">kubectl -n apps get externalartifact web
</span></span></code></pre></div><h2 id="4-roll-it-out-with-stageset">4. Roll it out with StageSet</h2>
<p>Reference the <code>JsonnetSnippet</code> as the stage source — StageSet resolves the
producer to its <code>ExternalArtifact</code> — and gate the stage on the Deployment becoming
available:</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">stages.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">StageSet</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">web</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">apps</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">web-deployer     </span><span class="w"> </span><span class="c"># applies are impersonated as this SA</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">stages</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">web</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">web</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">readyChecks</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">checks</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">apps/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">Deployment</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">web</span><span class="w">
</span></span></span></code></pre></div><p>Apply it, preview the change before it lands, then watch it roll out:</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 stageset.yaml
</span></span><span class="line"><span class="cl">stagesetctl diff web -n apps          <span class="c1"># preview against live cluster state</span>
</span></span><span class="line"><span class="cl">stagesetctl get  web -n apps          <span class="c1"># per-stage progress</span>
</span></span></code></pre></div><h2 id="5-ship-a-change">5. Ship a change</h2>
<p>Edit <code>jsonnet/main.jsonnet</code> (or bump the <code>image</code> TLA on the snippet) and commit.
Flux pulls the new commit, JaaS re-renders and republishes the <code>ExternalArtifact</code>,
and StageSet — watching the producer — reconciles the new revision through the
same gate. No StageSet edit required.</p>
<h3 id="no-labels-or-annotations-needed">No labels or annotations needed</h3>
<p>You do <strong>not</strong> annotate or label anything to make this chain fire. The linkage is
the <code>sourceRef</code> itself: the controller watches the source <em>kinds</em> (<code>ExternalArtifact</code>,
<code>GitRepository</code>, <code>OCIRepository</code>, <code>Bucket</code>, and producers like <code>JsonnetSnippet</code>) and,
when one changes, maps it back to every StageSet whose <code>sourceRef</code> points at it — then
reconciles those. JaaS works the same way for a snippet&rsquo;s own <code>sourceRef</code> and
library references. Discovery is automatic; you only declare the references.</p>
<h2 id="versioning-the-rollout">Versioning the rollout</h2>
<p>To gate one-time <a href="/usage/versioned-migrations/">migrations</a> on a release boundary,
declare the version. The simplest is to pin it on the StageSet, bumped alongside the
image:</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">version</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">value</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;2.1.0&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">migrations</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">backfill-2-0</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 class="s2">&#34;2.0.0&#34;</span><span class="w">               </span><span class="c"># runs once when the deployed version crosses 2.0.0</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">stage</span><span class="p">:</span><span class="w"> </span><span class="l">web</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">actions</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">backfill</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">job</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">name</span><span class="p">:</span><span class="w"> </span><span class="l">web-migrations</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">stages</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">web</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">web</span><span class="w">
</span></span></span></code></pre></div><h3 id="let-the-version-travel-with-the-rendered-manifests">Let the version travel with the rendered manifests</h3>
<p>Pinning works, but the cleaner pattern is to let the version ride <em>inside</em> the
manifests the snippet renders — so a single value flows from your CI all the way to
the rollout gate. Feed the version into the snippet and stamp it onto the standard
<code>app.kubernetes.io/version</code> label (and the image tag, from the same value):</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-jsonnet" data-lang="jsonnet"><span class="line"><span class="cl"><span class="c1">// web.jsonnet
</span></span></span><span class="line"><span class="cl"><span class="k">local</span><span class="w"> </span><span class="nv">version</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nb">std.extVar</span><span class="p">(</span><span class="s">&#39;version&#39;</span><span class="p">);</span><span class="w">   </span><span class="c1">// supplied by JaaS extVars / your CI
</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">apiVersion</span><span class="p">:</span><span class="w"> </span><span class="s">&#39;apps/v1&#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">kind</span><span class="p">:</span><span class="w"> </span><span class="s">&#39;Deployment&#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">metadata</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="s">&#39;web&#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">labels</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nv">&#39;app.kubernetes.io/version&#39;</span><span class="p">:</span><span class="w"> </span><span class="nv">version</span><span class="w"> </span><span class="p">},</span><span class="w">   </span><span class="c1">// ← the version, in the manifest
</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">spec</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">template</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">metadata</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nv">labels</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nv">&#39;app.kubernetes.io/version&#39;</span><span class="p">:</span><span class="w"> </span><span class="nv">version</span><span class="w"> </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">spec</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nv">containers</span><span class="p">:</span><span class="w"> </span><span class="p">[{</span><span class="w"> </span><span class="nv">name</span><span class="p">:</span><span class="w"> </span><span class="s">&#39;web&#39;</span><span class="p">,</span><span class="w"> </span><span class="nv">image</span><span class="p">:</span><span class="w"> </span><span class="s">&#39;registry.example/web:&#39;</span><span class="w"> </span><span class="o">+</span><span class="w"> </span><span class="nv">version</span><span class="w"> </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="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>Then point <code>version.fromObject</code> at that object and drop the inline <code>value</code> — the
controller reads the label off the rendered <code>Deployment</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">version</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">fromObject</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">stage</span><span class="p">:</span><span class="w"> </span><span class="l">web</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">Deployment</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">web</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="c"># defaults to the app.kubernetes.io/version label</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">migrations</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">backfill-2-0</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 class="s2">&#34;2.0.0&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">stage</span><span class="p">:</span><span class="w"> </span><span class="l">web</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">actions</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">backfill</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">job</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">name</span><span class="p">:</span><span class="w"> </span><span class="l">web-migrations</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">stages</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">web</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">web</span><span class="w">
</span></span></span></code></pre></div><p>Now the version has exactly one source of truth — the value your pipeline feeds the
snippet — and it shows up in the image tag, the version label, <em>and</em> the migration
gate together. The same <code>fromObject</code> works for a <code>GitRepository</code>/<code>OCIRepository</code>
source too; only a source that ships a dedicated file wants
<a href="/usage/versioned-migrations/#from-a-file-in-the-artifact--versionfromartifact"><code>version.fromArtifact</code></a>
instead. See <a href="/usage/versioned-migrations/">versioned migrations</a> for all three.</p>
<h2 id="next">Next</h2>
<p>From here, add more <a href="/usage/stages-and-sources/">stages</a>, pre/post
<a href="/usage/actions/">actions</a>, or <a href="/usage/update-windows/">update windows</a> to turn
this single rollout into a gated, multi-stage release. To parameterize per
environment, see <a href="/tutorials/parameters/">Parameters</a>.</p>
]]></content><category scheme="https://stageset.projects.metio.wtf/tags/tutorials" term="tutorials" label="tutorials"/><category scheme="https://stageset.projects.metio.wtf/tags/jsonnet" term="jsonnet" label="jsonnet"/><category scheme="https://stageset.projects.metio.wtf/tags/jaas" term="jaas" label="jaas"/><category scheme="https://stageset.projects.metio.wtf/tags/stages" term="stages" label="stages"/></entry><entry><title type="html">Install on Kubernetes</title><link href="https://stageset.projects.metio.wtf/installation/kubernetes/?utm_source=atom_feed" rel="alternate" type="text/html"/><link href="https://stageset.projects.metio.wtf/comparisons/helm/?utm_source=atom_feed" rel="related" type="text/html" title="StageSet vs Helm"/><link href="https://stageset.projects.metio.wtf/comparisons/kustomize/?utm_source=atom_feed" rel="related" type="text/html" title="StageSet vs Kustomize"/><id>https://stageset.projects.metio.wtf/installation/kubernetes/</id><published>0001-01-01T00:00:00+00:00</published><updated>2026-06-16T13:41:17+02:00</updated><content type="html"><![CDATA[<blockquote>Prerequisites and the Helm install that get the controller onto a cluster.</blockquote><h2 id="prerequisites">Prerequisites</h2>
<ul>
<li>A <a href="https://kubernetes.io/docs/">Kubernetes</a> cluster with <code>kubectl</code> and
<a href="https://helm.sh/"><code>helm</code></a> configured against it.</li>
<li><a href="https://fluxcd.io/">Flux</a> <code>source-controller</code>, specifically the
<code>ExternalArtifact</code> API (<code>source.toolkit.fluxcd.io</code>). A <code>StageSet</code> stage always
resolves to an <code>ExternalArtifact</code>, so the CRD must exist. <code>ExternalArtifact</code>
lands in Flux <strong>v2.7.0</strong>; install at least that version. The controller also
watches <code>GitRepository</code>, <code>OCIRepository</code>, and <code>Bucket</code> sources for
producer-aware resolution.</li>
<li><a href="https://cert-manager.io/">cert-manager</a>, only if you choose the
<code>cert-manager</code> webhook certificate mode. The chart defaults to <code>self-signed</code>,
which provisions and rotates the admission 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><a href="https://jaas.projects.metio.wtf/">JaaS</a>, JOI, or any particular artifact
producer are not required to install the controller — those are sources of
<code>ExternalArtifact</code>s, wired up per <code>StageSet</code>.</p>
<h2 id="install-with-helm">Install with Helm</h2>
<p>The controller is distributed as an OCI <a href="https://helm.sh/">Helm</a> chart. The
deployment manifests live in the chart, not in the controller repository.</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 stageset-controller <span class="se">\
</span></span></span><span class="line"><span class="cl">  oci://ghcr.io/metio/helm-charts/stageset-controller <span class="se">\
</span></span></span><span class="line"><span class="cl">  --namespace stageset-system --create-namespace
</span></span></code></pre></div><p>The container image is <code>ghcr.io/metio/stageset-controller</code>; the chart pins the
tag to its own <code>appVersion</code> by default.</p>
<p>Every setting referenced across these docs — HA replicas, the rollback store,
webhook mode, NetworkPolicy, the ServiceMonitor, and the rest — is a Helm value.
The <a href="https://github.com/metio/helm-charts/tree/main/charts/stageset-controller">chart&rsquo;s README and <code>values.yaml</code></a>
document the full, current list.</p>
<h3 id="what-the-chart-installs">What the chart installs</h3>
<ul>
<li>The <strong>controller <code>Deployment</code></strong>, its <code>ServiceAccount</code>, and the cluster RBAC it
needs (a <code>ClusterRole</code> + <code>ClusterRoleBinding</code>, plus a namespaced leader-election
<code>Role</code>/<code>RoleBinding</code>).</li>
<li>The <strong>CRDs</strong> — <code>StageSet</code> and <code>StageInventory</code>.</li>
<li>The <strong>validating admission webhook</strong> (<code>ValidatingWebhookConfiguration</code> + a
webhook <code>Service</code>).</li>
<li>A <strong>metrics <code>Service</code></strong> (and an opt-in <code>ServiceMonitor</code>).</li>
<li>The <strong>Flagger gate <code>Service</code></strong> for the read-only stage-gate endpoint.</li>
<li>Opt-in extras: <code>NetworkPolicy</code>, <code>PodDisruptionBudget</code>,
<code>HorizontalPodAutoscaler</code>, a rollback-store <code>PersistentVolumeClaim</code>, and a
managed <code>Namespace</code>.</li>
</ul>
<h3 id="about-the-crds">About the CRDs</h3>
<p>The CRDs ship inside the chart&rsquo;s regular templates (not Helm&rsquo;s special <code>crds/</code>
directory), so a <code>helm upgrade</code> applies schema changes like any other resource.
This is 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
StageSets — in place; remove them by hand if you really mean to.</p>
<p>If you manage CRDs out of band, the raw definitions are also published in the
controller repository under <code>config/crd/</code> and can be applied with
<code>kubectl apply --server-side -f</code>.</p>
<h2 id="verify">Verify</h2>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">kubectl -n stageset-system get deploy stageset-controller
</span></span><span class="line"><span class="cl">kubectl get crd stagesets.stages.metio.wtf stageinventories.stages.metio.wtf
</span></span></code></pre></div><p>Once the controller is <code>Available</code>, create your first
<a href="/usage/stages-and-sources/">StageSet</a>.</p>
]]></content><category scheme="https://stageset.projects.metio.wtf/tags/installation" term="installation" label="installation"/><category scheme="https://stageset.projects.metio.wtf/tags/helm" term="helm" label="helm"/><category scheme="https://stageset.projects.metio.wtf/tags/kubernetes" term="kubernetes" label="kubernetes"/></entry><entry><title type="html">InvalidSpec</title><link href="https://stageset.projects.metio.wtf/runbooks/invalidspec/?utm_source=atom_feed" rel="alternate" type="text/html"/><link href="https://stageset.projects.metio.wtf/runbooks/artifactnotfound/?utm_source=atom_feed" rel="related" type="text/html" title="ArtifactNotFound"/><link href="https://stageset.projects.metio.wtf/runbooks/controller-pod-down/?utm_source=atom_feed" rel="related" type="text/html" title="Controller pod down"/><link href="https://stageset.projects.metio.wtf/runbooks/dependencynotready/?utm_source=atom_feed" rel="related" type="text/html" title="DependencyNotReady"/><link href="https://stageset.projects.metio.wtf/runbooks/downgraderequiresmigration/?utm_source=atom_feed" rel="related" type="text/html" title="DowngradeRequiresMigration"/><link href="https://stageset.projects.metio.wtf/runbooks/invalidversion/?utm_source=atom_feed" rel="related" type="text/html" title="InvalidVersion"/><id>https://stageset.projects.metio.wtf/runbooks/invalidspec/</id><published>0001-01-01T00:00:00+00:00</published><updated>2026-06-16T13:41:17+02:00</updated><content type="html"><![CDATA[<blockquote>The StageSet spec is invalid; the Message names the offending field or action.</blockquote><h2 id="symptom">Symptom</h2>
<p><code>READY=False</code>, <code>REASON=InvalidSpec</code>. The Message names the offending field or action. Terminal: the controller does not requeue until the spec changes.</p>
<h2 id="cause">Cause</h2>
<p>The spec failed validation that the CRD schema cannot express cheaply, normally one of:</p>
<ul>
<li>an <strong>action sets zero or more than one verb</strong> — each action must set exactly one of <code>patch</code>, <code>http</code>, <code>wait</code>, <code>job</code>, <code>delete</code>, <code>apply</code> (see <a href="/usage/actions/">actions</a>);</li>
<li><strong><code>spec.migrations</code> without <code>spec.version</code></strong>, or a migration anchored to a stage name that does not exist (see <a href="/usage/versioned-migrations/">versioned migrations</a>);</li>
<li><strong><code>spec.version</code> does not name exactly one source</strong> — set one of <code>value</code>, <code>fromObject</code>, or <code>fromArtifact</code>;</li>
<li><strong><code>spec.decryption.provider</code> is not <code>sops</code></strong>, or a <code>secretRef</code> is given without a <code>name</code> (see <a href="/usage/encryption/">encryption</a>);</li>
<li>an <strong>invalid update window</strong> — a malformed <code>schedule</code>, <code>duration</code>, or <code>timeZone</code> (see <a href="/usage/update-windows/">update windows</a>).</li>
</ul>
<p>The admission webhook normally rejects these at write time; seeing this on the object means the webhook was bypassed or disabled and the reconciler caught it.</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 stageset &lt;name&gt; -n &lt;namespace&gt;
</span></span></code></pre></div><p>Read the Message — it names the stage, action, or field.</p>
<h2 id="remediation">Remediation</h2>
<p>Fix the spec per the Message:</p>
<ul>
<li>give each action exactly one verb;</li>
<li>set <code>spec.version</code> (to one of <code>value</code>/<code>fromObject</code>/<code>fromArtifact</code>) whenever <code>spec.migrations</code> is present, and anchor each migration to a real stage;</li>
<li>set exactly one <code>spec.version</code> source;</li>
<li>use <code>provider: sops</code> for <code>spec.decryption</code>, with a named <code>secretRef</code> when one is given;</li>
<li>correct any malformed update window (<code>schedule</code>, <code>duration</code>, <code>timeZone</code>).</li>
</ul>
<p>If the webhook should have caught this, confirm the <code>ValidatingWebhookConfiguration</code> is installed and its service is reachable.</p>
]]></content><category scheme="https://stageset.projects.metio.wtf/tags/runbooks" term="runbooks" label="runbooks"/><category scheme="https://stageset.projects.metio.wtf/tags/troubleshooting" term="troubleshooting" label="troubleshooting"/><category scheme="https://stageset.projects.metio.wtf/tags/api" term="api" label="api"/></entry><entry><title type="html">InvalidVersion</title><link href="https://stageset.projects.metio.wtf/runbooks/invalidversion/?utm_source=atom_feed" rel="alternate" type="text/html"/><link href="https://stageset.projects.metio.wtf/runbooks/downgraderequiresmigration/?utm_source=atom_feed" rel="related" type="text/html" title="DowngradeRequiresMigration"/><link href="https://stageset.projects.metio.wtf/runbooks/artifactnotfound/?utm_source=atom_feed" rel="related" type="text/html" title="ArtifactNotFound"/><link href="https://stageset.projects.metio.wtf/runbooks/controller-pod-down/?utm_source=atom_feed" rel="related" type="text/html" title="Controller pod down"/><link href="https://stageset.projects.metio.wtf/runbooks/dependencynotready/?utm_source=atom_feed" rel="related" type="text/html" title="DependencyNotReady"/><link href="https://stageset.projects.metio.wtf/runbooks/invalidspec/?utm_source=atom_feed" rel="related" type="text/html" title="InvalidSpec"/><id>https://stageset.projects.metio.wtf/runbooks/invalidversion/</id><published>0001-01-01T00:00:00+00:00</published><updated>2026-06-16T13:41:17+02:00</updated><content type="html"><![CDATA[<blockquote>A version source or value could not be parsed as semver.</blockquote><h2 id="symptom">Symptom</h2>
<p><code>READY=False</code>, <code>REASON=InvalidVersion</code>. Terminal: the run does not requeue until the spec or the version file is fixed.</p>
<h2 id="cause">Cause</h2>
<p>A version <code>spec.version</code> (or a migration boundary) could not be resolved to a parseable <a href="https://semver.org/">semver</a>. The controller refuses to proceed rather than deploy a half-versioned system — a system whose recorded version is unknown is worse for migrations than an unversioned one. The Message names which input failed. By version source:</p>
<ul>
<li><strong><code>spec.version.value</code></strong> — the inline string is not a semver.</li>
<li><strong><code>spec.version.fromObject</code></strong> — the named stage doesn&rsquo;t exist; the object (<code>kind</code>/<code>name</code>) isn&rsquo;t in the stage&rsquo;s rendered manifests; the <code>fieldPath</code> is invalid JSONPath or resolves to empty; or the value read (by default the <code>app.kubernetes.io/version</code> label) is missing or not a semver.</li>
<li><strong><code>spec.version.fromArtifact</code></strong> — the named stage doesn&rsquo;t exist; the file at <code>path</code> is missing from the stage&rsquo;s artifact, empty, or doesn&rsquo;t parse as a semver.</li>
<li><strong><code>spec.version</code> sets none</strong> of <code>value</code>/<code>fromObject</code>/<code>fromArtifact</code>.</li>
<li>A <strong>migration&rsquo;s <code>to</code> or <code>from</code></strong> is not a valid semver.</li>
<li>The recorded <strong><code>status.version</code></strong> is not a semver (corrupted status).</li>
</ul>
<p>Common triggers across all of them: a <code>v</code> prefix or trailing whitespace the parser rejects, or non-semver text (e.g. a Git SHA or a <code>latest</code> tag) where a version was expected.</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 stageset &lt;name&gt; -n &lt;namespace&gt;   <span class="c1"># Message names the failing input</span>
</span></span><span class="line"><span class="cl">kubectl get stageset &lt;name&gt; -n &lt;namespace&gt; -o <span class="nv">jsonpath</span><span class="o">=</span><span class="s1">&#39;{.spec.version}{&#34;\n&#34;}&#39;</span>
</span></span></code></pre></div><p>Then, depending on which source the Message names:</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"># fromObject: confirm the field carries a bare semver on the rendered object</span>
</span></span><span class="line"><span class="cl">stagesetctl build &lt;name&gt; -n &lt;namespace&gt; --stage &lt;stage&gt; <span class="p">|</span> grep -i version
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># fromArtifact: confirm the file exists and contains only a semver (e.g. 2.1.0)</span>
</span></span><span class="line"><span class="cl"><span class="c1"># inspect the resolved artifact for the stage named in the Message</span>
</span></span></code></pre></div><h2 id="remediation">Remediation</h2>
<p>Match the failing input in the Message:</p>
<ul>
<li><strong><code>value</code></strong> — correct the inline string to a bare semver (<code>2.1.0</code>, not <code>v2.1.0</code>).</li>
<li><strong><code>fromObject</code></strong> — fix the <code>stage</code>/<code>kind</code>/<code>name</code> to point at a real rendered object, fix the <code>fieldPath</code>, and ensure the field (default: <code>app.kubernetes.io/version</code> label) carries a semver.</li>
<li><strong><code>fromArtifact</code></strong> — fix <code>path</code>/<code>stage</code> to the real version file, or correct the file to a bare semver.</li>
<li><strong>migration <code>to</code>/<code>from</code></strong> — correct the boundary to a valid semver.</li>
<li>If you don&rsquo;t need <a href="/usage/versioned-migrations/">versioned migrations</a>, remove <code>spec.version</code> entirely (this disables versioning and migrations).</li>
</ul>
]]></content><category scheme="https://stageset.projects.metio.wtf/tags/runbooks" term="runbooks" label="runbooks"/><category scheme="https://stageset.projects.metio.wtf/tags/versioning" term="versioning" label="versioning"/><category scheme="https://stageset.projects.metio.wtf/tags/troubleshooting" term="troubleshooting" label="troubleshooting"/></entry><entry><title type="html">Multi-cluster and tenancy</title><link href="https://stageset.projects.metio.wtf/usage/multi-cluster/?utm_source=atom_feed" rel="alternate" type="text/html"/><id>https://stageset.projects.metio.wtf/usage/multi-cluster/</id><published>0001-01-01T00:00:00+00:00</published><updated>2026-06-16T13:41:17+02:00</updated><content type="html"><![CDATA[<blockquote>Impersonation, watch scoping, single-tenant cluster-admin, and remote clusters.</blockquote><p>There are two ways to run the controller, and they map onto two different trust
models. Pick the one that matches your cluster:</p>
<ul>
<li><strong>Multi-tenant</strong> — the controller holds no write access of its own and applies
every <code>StageSet</code> impersonating that <code>StageSet</code>&rsquo;s <code>serviceAccountName</code>. Each
tenant&rsquo;s RBAC bounds what its releases can touch. This is the chart default.</li>
<li><strong>Single-tenant</strong> — the cluster has one operator, so per-tenant isolation buys
nothing. Run the controller under its own identity bound to <code>cluster-admin</code> and
skip impersonation entirely — the model Flux&rsquo;s <code>helm-controller</code> uses in its
default install.</li>
</ul>
<p>The two sections below set each one up. The optional
<a href="#scoping-the-controller-to-a-namespace-set">watch scoping</a> narrows <em>which</em>
namespaces a multi-tenant controller sees.</p>
<h2 id="impersonation-multi-tenant">Impersonation (multi-tenant)</h2>
<p>The controller never applies your manifests as itself. Set <code>serviceAccountName</code>
and every operation for that <code>StageSet</code> — build, apply, prune, actions — is
performed impersonating that ServiceAccount. The <code>StageSet</code> can do exactly what the
SA&rsquo;s RBAC permits, and nothing more.</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">serviceAccountName</span><span class="p">:</span><span class="w"> </span><span class="l">payments-deployer   </span><span class="w"> </span><span class="c"># all writes impersonate this SA</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">stages</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">app</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">name</span><span class="p">:</span><span class="w"> </span><span class="l">payments-app</span><span class="w">
</span></span></span></code></pre></div><p>Grant the SA only the rights that release needs:</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">name</span><span class="p">:</span><span class="w"> </span><span class="l">payments-deployer</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">payments</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">edit</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">payments-deployer</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">payments</span><span class="w">
</span></span></span></code></pre></div><p>This is the multi-tenancy model: isolation comes from each <code>StageSet</code> being bounded
by its tenant SA, not from the controller&rsquo;s own grant — by default the chart gives
the controller <code>impersonate</code> and read access, no blanket write. A <code>StageSet</code> with no
<code>serviceAccountName</code>, or one bound to a too-narrow SA, fails closed rather than
escalating.</p>
<h2 id="single-tenant-cluster-admin">Single-tenant cluster-admin</h2>
<p>On a cluster with a single operator, per-<code>StageSet</code> impersonation is friction with
no payoff — there is no other tenant to isolate from. Run the controller the way
Flux&rsquo;s <code>helm-controller</code> runs by default: under its own ServiceAccount, bound to
the built-in <code>cluster-admin</code> ClusterRole. <code>StageSet</code>s then omit <code>serviceAccountName</code>
and apply as the controller, which can write any kind cluster-wide.</p>
<p>Turn it on with one Helm value:</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">rbac</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">clusterAdmin</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="w">     </span><span class="c"># bind the controller SA to cluster-admin</span><span class="w">
</span></span></span></code></pre></div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">helm upgrade --install stageset-controller <span class="se">\
</span></span></span><span class="line"><span class="cl">  oci://ghcr.io/metio/helm-charts/stageset-controller <span class="se">\
</span></span></span><span class="line"><span class="cl">  -n stageset-system --create-namespace <span class="se">\
</span></span></span><span class="line"><span class="cl">  --set rbac.clusterAdmin<span class="o">=</span><span class="nb">true</span>
</span></span></code></pre></div><p><code>StageSet</code>s then need nothing tenancy-related — they apply 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="nt">apiVersion</span><span class="p">:</span><span class="w"> </span><span class="l">stages.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">StageSet</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">platform</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">stageset-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">stages</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">app</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">name</span><span class="p">:</span><span class="w"> </span><span class="l">platform-app   </span><span class="w"> </span><span class="c"># applied by the controller&#39;s cluster-admin identity</span><span class="w">
</span></span></span></code></pre></div><p>When <code>serviceAccountName</code> is unset and no <code>kubeConfig</code> is given, the controller
applies with its own client — so the <code>cluster-admin</code> binding is what lets those
<code>StageSet</code>s write. The trade-off: every <code>StageSet</code> on the cluster has full write
access, so this is for single-tenant clusters only. Leave <code>rbac.clusterAdmin</code> at its
default <code>false</code> and use <a href="#impersonation-multi-tenant">impersonation</a> whenever more
than one team shares the cluster. The two mix — a cluster-admin controller still
honors <code>serviceAccountName</code> on any <code>StageSet</code> that sets it, dropping to that SA&rsquo;s
rights for that release.</p>
<h2 id="scoping-the-controller-to-a-namespace-set">Scoping the controller to a namespace set</h2>
<p>By default the controller watches every namespace. To run one controller per
tenant-group instead — disjoint deployments that each see only their own
namespaces — set <code>controller.watchNamespaces</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">controller</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">watchNamespaces</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span>- <span class="l">team-a</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span>- <span class="l">team-b</span><span class="w">
</span></span></span></code></pre></div><p>This does two things together:</p>
<ul>
<li><strong>Cache scoping.</strong> The manager&rsquo;s informers only observe <code>StageSet</code>s and sources
in the listed namespaces. Resources elsewhere never enter the cache, so the
controller cannot act on them even if RBAC would allow it.</li>
<li><strong>RBAC pivot.</strong> The chart stops binding the tenant ClusterRole cluster-wide and
instead renders one <code>RoleBinding</code> per listed namespace — defense in depth, so the
apiserver also refuses out-of-scope calls. (The cluster-scoped webhook-caBundle
grant stays a <code>ClusterRoleBinding</code>, since a <code>ValidatingWebhookConfiguration</code> is
not namespaced.)</li>
</ul>
<p>Run several releases with disjoint <code>watchNamespaces</code> lists to shard the cluster
across independent controller instances. Combine it with impersonation for the
tightest setup: each instance sees only its namespaces, and each <code>StageSet</code> is
bounded by its tenant SA.</p>
<h2 id="remote-clusters">Remote clusters</h2>
<p>Point a <code>StageSet</code> at another cluster with <code>kubeConfig</code>, referencing a Secret that
holds a kubeconfig. Combined with <code>serviceAccountName</code>, the controller applies to
the remote cluster as the impersonated identity there.</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">serviceAccountName</span><span class="p">:</span><span class="w"> </span><span class="l">payments-deployer</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">kubeConfig</span><span class="p">:</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">prod-eu-kubeconfig</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="c"># key defaults to &#34;value&#34; (the Flux convention); set it to override</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">stages</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">app</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">name</span><span class="p">:</span><span class="w"> </span><span class="l">payments-app</span><span class="w">
</span></span></span></code></pre></div><p>The Secret is read with the controller&rsquo;s own identity — connecting to the target
cluster is the controller&rsquo;s job — and the kubeconfig payload defaults to the
<code>value</code> key. A self-contained kubeconfig is required; <code>configMapRef</code>-style
cloud-provider auth is not supported.</p>
<p>Cross-namespace <code>sourceRef</code> and <code>dependsOn</code> references can be disabled
cluster-wide with the controller&rsquo;s <code>--no-cross-namespace-refs</code> flag when you want
hard namespace isolation.</p>
]]></content><category scheme="https://stageset.projects.metio.wtf/tags/multi-cluster" term="multi-cluster" label="multi-cluster"/><category scheme="https://stageset.projects.metio.wtf/tags/tenancy" term="tenancy" label="tenancy"/><category scheme="https://stageset.projects.metio.wtf/tags/impersonation" term="impersonation" label="impersonation"/><category scheme="https://stageset.projects.metio.wtf/tags/rbac" term="rbac" label="rbac"/></entry><entry><title type="html">Operations</title><link href="https://stageset.projects.metio.wtf/installation/operations/?utm_source=atom_feed" rel="alternate" type="text/html"/><link href="https://stageset.projects.metio.wtf/runbooks/controller-pod-down/?utm_source=atom_feed" rel="related" type="text/html" title="Controller pod down"/><link href="https://stageset.projects.metio.wtf/runbooks/reconcile-latency/?utm_source=atom_feed" rel="related" type="text/html" title="Reconcile latency high"/><link href="https://stageset.projects.metio.wtf/runbooks/workqueue-saturation/?utm_source=atom_feed" rel="related" type="text/html" title="Workqueue saturation"/><link href="https://stageset.projects.metio.wtf/runbooks/suspended/?utm_source=atom_feed" rel="related" type="text/html" title="Suspended"/><link href="https://stageset.projects.metio.wtf/runbooks/webhook-cert-renewal/?utm_source=atom_feed" rel="related" type="text/html" title="Webhook cert renewal failing"/><id>https://stageset.projects.metio.wtf/installation/operations/</id><published>0001-01-01T00:00:00+00:00</published><updated>2026-06-16T13:41:17+02:00</updated><content type="html"><![CDATA[<blockquote>Metrics, alerts, events, and runbooks for running the controller day to day.</blockquote><h2 id="metrics">Metrics</h2>
<p>The controller registers custom metrics on the controller-runtime registry, served
on <code>--metrics-bind-address</code> (<code>:8080</code>) alongside the standard
<code>controller_runtime_*</code> and <code>workqueue_*</code> series. Enable scraping with the chart&rsquo;s
opt-in <code>ServiceMonitor</code> (<code>metrics.serviceMonitor.enabled</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"># values.yaml</span><span class="w">
</span></span></span><span class="line"><span class="cl"><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">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 class="c"># needs the Prometheus operator CRDs</span><span class="w">
</span></span></span></code></pre></div><table>
	<thead>
			<tr>
					<th>Metric</th>
					<th>Type</th>
					<th>Labels</th>
					<th>Meaning</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td><code>stageset_reconcile_total</code></td>
					<td>counter</td>
					<td><code>namespace</code>, <code>name</code>, <code>reason</code></td>
					<td>Reconciles, by terminal Ready reason.</td>
			</tr>
			<tr>
					<td><code>stageset_stage_applied_total</code></td>
					<td>counter</td>
					<td><code>namespace</code>, <code>name</code>, <code>stage</code></td>
					<td>Stages applied and verified.</td>
			</tr>
			<tr>
					<td><code>stageset_drift_corrected_total</code></td>
					<td>counter</td>
					<td><code>namespace</code>, <code>name</code>, <code>stage</code></td>
					<td>Out-of-band drift re-asserted on a steady-state reconcile.</td>
			</tr>
			<tr>
					<td><code>stageset_update_deferred_total</code></td>
					<td>counter</td>
					<td><code>namespace</code>, <code>name</code></td>
					<td>Rollouts held by a closed update window.</td>
			</tr>
			<tr>
					<td><code>stageset_webhook_cert_renewal_failures_total</code></td>
					<td>counter</td>
					<td><em>(none)</em></td>
					<td>Failed self-signed webhook cert renewals.</td>
			</tr>
			<tr>
					<td><code>stageset_stage_ready</code></td>
					<td>gauge</td>
					<td><code>namespace</code>, <code>stageset</code>, <code>stage</code></td>
					<td><code>1</code> when a stage is Ready, else <code>0</code> — for metric-based <a href="/tutorials/progressive-delivery/#argo-rollouts">progressive delivery</a>.</td>
			</tr>
	</tbody>
</table>
<h2 id="alerts">Alerts</h2>
<p>The chart ships an opt-in <code>PrometheusRule</code> with a starter alert set, gated on
<code>metrics.prometheusRule.enabled</code> (requires the
<a href="https://prometheus-operator.dev/">Prometheus operator</a> CRDs). It covers the
custom <code>stageset_*</code> metrics plus controller-runtime signals:</p>
<table>
	<thead>
			<tr>
					<th>Alert</th>
					<th>Fires on</th>
					<th>Severity</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td><code>StageSetReconcileErrorsHigh</code></td>
					<td>per-StageSet Ready=False rate (excludes the healthy <code>Succeeded</code>/<code>Suspended</code> reasons)</td>
					<td>warning</td>
			</tr>
			<tr>
					<td><code>StageSetControllerWorkqueueDepthHigh</code></td>
					<td>the reconcile queue not draining</td>
					<td>warning</td>
			</tr>
			<tr>
					<td><code>StageSetReconcileLatencyHigh</code></td>
					<td>reconcile p99 latency over threshold</td>
					<td>warning</td>
			</tr>
			<tr>
					<td><code>StageSetControllerPodDown</code></td>
					<td>a controller pod NotReady</td>
					<td>critical</td>
			</tr>
			<tr>
					<td><code>StageSetWebhookCertRenewalFailing</code></td>
					<td>self-signed cert rotation failing</td>
					<td>critical</td>
			</tr>
	</tbody>
</table>
<p>Every threshold is a knob under <code>metrics.prometheusRule.thresholds</code>, and
<code>extraAlertLabels</code> is merged onto every rendered alert so all stageset alerts can
route through one Alertmanager receiver. Each alert carries a <code>runbook_url</code>
annotation pointing at the matching <a href="/runbooks/">runbook</a> page on this site
(<code>metrics.prometheusRule.runbookBaseURL</code>); the reconcile-errors alert templates the
URL on <code>$labels.reason</code>. Append your own rules under
<code>metrics.prometheusRule.extraRules</code>, and silence a built-in alert by raising its
threshold rather than forking the chart.</p>
<h2 id="events">Events</h2>
<p>The controller emits Kubernetes Events on every Ready-condition transition, so
<code>kubectl describe stageset &lt;name&gt;</code> and <a href="https://fluxcd.io/">Flux</a>&rsquo;s
<code>notification-controller</code> (via an <code>Alert</code> targeting <code>kind: StageSet</code>) both
surface what happened. Normal events
include <code>Succeeded</code>, <code>UpdateDeferred</code>, <code>MigrationStarted</code>, and
<code>MigrationCompleted</code>; warnings include <code>StageFailed</code>, <code>DriftCorrected</code>,
<code>RolledBack</code>, <code>MigrationFailed</code>, <code>OnFailureAction</code>, and <code>RollbackStoreFailed</code>.</p>
<h2 id="runbooks">Runbooks</h2>
<p>Every actionable Ready-condition reason has a <a href="/runbooks/">runbook</a> covering the
symptom, cause, diagnosis, and remediation. Set <code>--runbook-base-url</code> (the chart&rsquo;s
<code>controller.runbookBaseURL</code>, which defaults to this docs site) to a published copy
of those pages and the controller appends <code>(runbook: &lt;base&gt;/&lt;reason&gt;/)</code> to the
Ready message (the reason lower-cased into a path segment), so a <code>kubectl describe</code>
links straight to the fix. Healthy reasons (<code>Succeeded</code>, <code>Suspended</code>) get no link.</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"># values.yaml — point at your own mirror, or set &#34;&#34; to drop the links</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">controller</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">runbookBaseURL</span><span class="p">:</span><span class="w"> </span><span class="l">https://runbooks.internal/stageset</span><span class="w">
</span></span></span></code></pre></div><p>For example, a <code>StageFailed</code> StageSet then shows:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">Message:  stage &#34;application&#34; failed: … (runbook: https://runbooks.internal/stageset/stagefailed/)
</span></span></code></pre></div><h2 id="forcing-a-reconcile">Forcing a reconcile</h2>
<p>The controller reconciles on its <code>spec.interval</code>, on source changes, and on
demand. To trigger an out-of-band run, stamp the standard annotation — which is
what <code>flux reconcile</code> and <a href="/cli/reconcile/"><code>stagesetctl reconcile</code></a> do for you:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">kubectl annotate stageset my-app <span class="se">\
</span></span></span><span class="line"><span class="cl">  reconcile.fluxcd.io/requestedAt<span class="o">=</span><span class="s2">&#34;</span><span class="k">$(</span>date -u +%FT%TZ<span class="k">)</span><span class="s2">&#34;</span> --overwrite
</span></span></code></pre></div><p>The handled token is recorded in <code>status.lastHandledReconcileAt</code>.</p>
<h2 id="drift-correction">Drift correction</h2>
<p>On a steady-state reconcile the controller re-asserts the desired state, healing
out-of-band changes to managed objects. Each correction emits a <code>DriftCorrected</code>
event and increments <code>stageset_drift_corrected_total</code>. Tighten the cadence with
<code>spec.driftDetectionInterval</code> when you need faster healing than <code>spec.interval</code>.</p>
]]></content><category scheme="https://stageset.projects.metio.wtf/tags/operations" term="operations" label="operations"/><category scheme="https://stageset.projects.metio.wtf/tags/metrics" term="metrics" label="metrics"/><category scheme="https://stageset.projects.metio.wtf/tags/alerts" term="alerts" label="alerts"/><category scheme="https://stageset.projects.metio.wtf/tags/runbooks" term="runbooks" label="runbooks"/></entry><entry><title type="html">Parameterizing a rollout</title><link href="https://stageset.projects.metio.wtf/tutorials/parameters/?utm_source=atom_feed" rel="alternate" type="text/html"/><link href="https://stageset.projects.metio.wtf/tutorials/jsonnet-to-rollout/?utm_source=atom_feed" rel="related" type="text/html" title="From Jsonnet to a gated rollout"/><link href="https://stageset.projects.metio.wtf/usage/producer-aware-sources/?utm_source=atom_feed" rel="related" type="text/html" title="Producer-aware sources"/><link href="https://stageset.projects.metio.wtf/tutorials/progressive-delivery/?utm_source=atom_feed" rel="related" type="text/html" title="Progressive delivery"/><link href="https://stageset.projects.metio.wtf/tutorials/flux-sources/?utm_source=atom_feed" rel="related" type="text/html" title="Stage sources — Git, OCI, Bucket"/><id>https://stageset.projects.metio.wtf/tutorials/parameters/</id><published>0001-01-01T00:00:00+00:00</published><updated>2026-06-16T13:41:17+02:00</updated><content type="html"><![CDATA[<blockquote>Two layers of parameters — JaaS TLAs/extVars at render time, StageSet postBuild substitution at delivery.</blockquote><p>A rollout takes parameters at two distinct layers, which serve different purposes:</p>
<ul>
<li><strong>Render-time parameters (JaaS).</strong> Change <em>what gets rendered</em>. The Jsonnet
computes its output from top-level arguments (<code>tlas</code>) and external variables
(<code>externalVariables</code>). Different values produce a different <code>ExternalArtifact</code>.</li>
<li><strong>Delivery-time parameters (StageSet <code>postBuild</code>).</strong> Inject values <em>into
already-rendered manifests</em>, per stage, by string substitution — the same
mechanism Flux&rsquo;s <code>kustomize-controller</code> uses.</li>
</ul>
<p>Use render-time parameters for structural logic; use delivery-time parameters to
stamp environment-specific values onto a shared artifact.</p>
<h2 id="render-time-jaas-tlas-and-external-variables">Render-time: JaaS TLAs and external variables</h2>
<p>Top-level arguments map to a Jsonnet <code>function(...)</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="c1">// main.jsonnet
</span></span></span><span class="line"><span class="cl"><span class="k">function</span><span class="p">(</span><span class="nv">name</span><span class="o">=</span><span class="s">&#39;web&#39;</span><span class="p">,</span><span class="w"> </span><span class="nv">replicas</span><span class="o">=</span><span class="s">&#39;2&#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 class="nv">apiVersion</span><span class="p">:</span><span class="w"> </span><span class="s">&#39;apps/v1&#39;</span><span class="p">,</span><span class="w"> </span><span class="nv">kind</span><span class="p">:</span><span class="w"> </span><span class="s">&#39;Deployment&#39;</span><span class="p">,</span><span class="w"> </span><span class="nv">metadata</span><span class="p">:</span><span class="w"> </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">name</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">spec</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nv">replicas</span><span class="p">:</span><span class="w"> </span><span class="nb">std.parseInt</span><span class="p">(</span><span class="nv">replicas</span><span class="p">)</span><span class="w"> </span><span class="c">/* … */</span><span class="w"> </span><span class="p">}</span><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-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">web</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">apps</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 class="w"> </span><span class="nt">kind: GitRepository, name: web-manifests, path</span><span class="p">:</span><span class="w"> </span><span class="l">./jsonnet }</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 class="c"># → function(name, replicas)</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="p">[</span><span class="s2">&#34;web&#34;</span><span class="p">]</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">replicas</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="s2">&#34;3&#34;</span><span class="p">]</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 class="c"># → std.extVar(&#39;environment&#39;)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">environment</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;production&#34;</span><span class="w">
</span></span></span></code></pre></div><p><code>tlas</code> is a map of name → list of values (a single-element list for a scalar
argument; multiple values become a JSON array). <code>externalVariables</code> are plain
strings read with <code>std.extVar</code>.</p>
<h2 id="delivery-time-stageset-postbuild-substitution">Delivery-time: StageSet postBuild substitution</h2>
<p>When the rendered manifests carry <code>${var}</code> placeholders, a stage substitutes them
at apply time — from inline values, ConfigMaps, and Secrets:</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">stages.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">StageSet</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">web</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">apps</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">stages</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">web</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">web</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">postBuild</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">substitute</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">cluster_name</span><span class="p">:</span><span class="w"> </span><span class="l">prod-eu</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">substituteFrom</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">ConfigMap</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-vars</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">Secret</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-secrets</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span><span class="nt">optional</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="w">
</span></span></span></code></pre></div><p>A manifest field like <code>value: &quot;${cluster_name}&quot;</code> becomes <code>value: &quot;prod-eu&quot;</code> for
this stage.</p>
<h2 id="reusing-one-artifact-across-environments">Reusing one artifact across environments</h2>
<p>The two layers combine into a common pattern: render an environment-<em>agnostic</em>
artifact once with JaaS, then have several StageSets — one per environment —
consume that same artifact and stamp their own values with <code>postBuild</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"># staging</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">stages</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">web</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 class="w"> </span><span class="nt">apiVersion: jaas.metio.wtf/v1, kind: JsonnetSnippet, name</span><span class="p">:</span><span class="w"> </span><span class="l">web }</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">postBuild</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">substituteFrom</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span>- {<span class="w"> </span><span class="nt">kind: ConfigMap, name</span><span class="p">:</span><span class="w"> </span><span class="l">staging-vars }</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"># production (same artifact, different values)</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">stages</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">web</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 class="w"> </span><span class="nt">apiVersion: jaas.metio.wtf/v1, kind: JsonnetSnippet, name</span><span class="p">:</span><span class="w"> </span><span class="l">web }</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">postBuild</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">substituteFrom</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span>- {<span class="w"> </span><span class="nt">kind: ConfigMap, name</span><span class="p">:</span><span class="w"> </span><span class="l">production-vars }</span><span class="w">
</span></span></span></code></pre></div><p>One render, many environments — each StageSet bounded by its own
<a href="/usage/multi-cluster/">ServiceAccount</a> and gated by its own
<a href="/usage/actions/">actions</a> and <a href="/usage/update-windows/">update windows</a>.</p>
]]></content><category scheme="https://stageset.projects.metio.wtf/tags/tutorials" term="tutorials" label="tutorials"/><category scheme="https://stageset.projects.metio.wtf/tags/tlas" term="tlas" label="tlas"/><category scheme="https://stageset.projects.metio.wtf/tags/ext-vars" term="ext-vars" label="ext-vars"/><category scheme="https://stageset.projects.metio.wtf/tags/jaas" term="jaas" label="jaas"/></entry><entry><title type="html">PreviousRevisionUnavailable</title><link href="https://stageset.projects.metio.wtf/runbooks/previousrevisionunavailable/?utm_source=atom_feed" rel="alternate" type="text/html"/><link href="https://stageset.projects.metio.wtf/runbooks/artifactnotfound/?utm_source=atom_feed" rel="related" type="text/html" title="ArtifactNotFound"/><link href="https://stageset.projects.metio.wtf/runbooks/controller-pod-down/?utm_source=atom_feed" rel="related" type="text/html" title="Controller pod down"/><link href="https://stageset.projects.metio.wtf/runbooks/dependencynotready/?utm_source=atom_feed" rel="related" type="text/html" title="DependencyNotReady"/><link href="https://stageset.projects.metio.wtf/runbooks/downgraderequiresmigration/?utm_source=atom_feed" rel="related" type="text/html" title="DowngradeRequiresMigration"/><link href="https://stageset.projects.metio.wtf/runbooks/invalidspec/?utm_source=atom_feed" rel="related" type="text/html" title="InvalidSpec"/><id>https://stageset.projects.metio.wtf/runbooks/previousrevisionunavailable/</id><published>0001-01-01T00:00:00+00:00</published><updated>2026-06-16T13:59:54+02:00</updated><content type="html"><![CDATA[<blockquote>rollbackOnFailure is set but the last-good revisions could not be restored.</blockquote><h2 id="symptom">Symptom</h2>
<p><code>READY=False</code>, <code>REASON=PreviousRevisionUnavailable</code>. The StageSet has <code>spec.rollbackOnFailure</code> set, a run failed, and the controller could not restore the last-good revisions.</p>
<h2 id="cause">Cause</h2>
<p><a href="/usage/rollback/"><code>rollbackOnFailure</code></a> restores the previously-applied artifact revisions by re-fetching their recorded URLs and verifying their digests. That only works while the <strong>producer still retains</strong> those revisions. This reason means a revision the rollback needs is no longer fetchable — the producer garbage-collected it.</p>
<p>Rollback is best-effort by contract: it works exactly when producers retain. Common triggers:</p>
<ul>
<li>a JaaS <code>JsonnetSnippet</code> with <code>spec.history: 1</code> (the default) — only the current revision is kept, so there is no previous revision to roll back to</li>
<li>a stock source-controller source, which retains only the current revision</li>
<li>the previous revision aged out of the producer&rsquo;s retention window</li>
<li>the run used <a href="/usage/encryption/">SOPS decryption</a> and the key Secret was rotated
or deleted — rollback re-runs decryption rather than restoring plaintext, so it
fails closed when the key is gone, even for a revision the rollback store holds</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 stageset &lt;name&gt; -n &lt;namespace&gt;   <span class="c1"># Message names the stage + revision</span>
</span></span></code></pre></div><p>Check the producer&rsquo;s retention. For a JaaS snippet:</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; -n &lt;namespace&gt; -o <span class="nv">jsonpath</span><span class="o">=</span><span class="s1">&#39;{.spec.history}&#39;</span>
</span></span></code></pre></div><h2 id="remediation">Remediation</h2>
<p>The cluster is left at the partially-applied failed state; resolve the underlying failure (see the failing stage&rsquo;s own runbook) and fix forward — the StageSet converges once the desired revision applies cleanly.</p>
<p>To make rollback reliable in future, either:</p>
<ul>
<li><strong>Increase producer retention</strong> so at least one previous revision is always fetchable — JaaS snippets used with <code>rollbackOnFailure</code> should set <code>spec.history: 2</code> (or more); sources that retain only the current revision cannot support the re-fetch path, so rely on source revert instead.</li>
<li><strong>Configure the external rollback store</strong> — a filesystem/RWX PVC (<code>--rollback-store-path</code>) or an S3 bucket (<code>--rollback-store-s3-*</code>); see <a href="/installation/operations/">operations</a>. When the controller pushes rendered output to a store it owns, rollback is bit-exact and <strong>independent of producer retention</strong> — this <code>PreviousRevisionUnavailable</code> state cannot occur for runs the store holds, unless the run used SOPS and the key Secret is no longer readable (see the SOPS trigger above).</li>
</ul>
]]></content><category scheme="https://stageset.projects.metio.wtf/tags/runbooks" term="runbooks" label="runbooks"/><category scheme="https://stageset.projects.metio.wtf/tags/rollback" term="rollback" label="rollback"/><category scheme="https://stageset.projects.metio.wtf/tags/troubleshooting" term="troubleshooting" label="troubleshooting"/></entry><entry><title type="html">Producer-aware sources</title><link href="https://stageset.projects.metio.wtf/usage/producer-aware-sources/?utm_source=atom_feed" rel="alternate" type="text/html"/><link href="https://stageset.projects.metio.wtf/usage/stages-and-sources/?utm_source=atom_feed" rel="related" type="text/html" title="Stages and sources"/><link href="https://stageset.projects.metio.wtf/runbooks/artifactnotfound/?utm_source=atom_feed" rel="related" type="text/html" title="ArtifactNotFound"/><link href="https://stageset.projects.metio.wtf/tutorials/jsonnet-to-rollout/?utm_source=atom_feed" rel="related" type="text/html" title="From Jsonnet to a gated rollout"/><link href="https://stageset.projects.metio.wtf/runbooks/resolvefailed/?utm_source=atom_feed" rel="related" type="text/html" title="ResolveFailed"/><link href="https://stageset.projects.metio.wtf/api/stageset/?utm_source=atom_feed" rel="related" type="text/html" title="StageSet"/><id>https://stageset.projects.metio.wtf/usage/producer-aware-sources/</id><published>0001-01-01T00:00:00+00:00</published><updated>2026-06-16T13:41:17+02:00</updated><content type="html"><![CDATA[<blockquote>Reference a producer like JaaS and let the controller find its ExternalArtifact.</blockquote><p><a href="/usage/stages-and-sources/#source-kinds">Stages and sources</a> covers the two
direct routes — an <code>ExternalArtifact</code> (the default <code>sourceRef.kind</code>) or a Flux
<code>GitRepository</code>/<code>OCIRepository</code>/<code>Bucket</code>. This page covers the third: naming the
thing that <em>produces</em> an artifact and letting the controller find it. This is useful
when an operator publishes an <code>ExternalArtifact</code> from a custom resource (for example
<a href="https://jaas.projects.metio.wtf/">JaaS</a> rendering Jsonnet).</p>
<h2 id="referencing-a-producer">Referencing a producer</h2>
<p>Set <code>kind</code> (and <code>apiVersion</code>) to a producer resource, and the controller resolves
it to the <code>ExternalArtifact</code> that producer publishes — the one whose
<code>spec.sourceRef</code> back-references the producer (matched on group, kind, and name).
For example, a <a href="https://jaas.projects.metio.wtf/">JaaS</a> <code>JsonnetSnippet</code>
renders Jsonnet and publishes an <code>ExternalArtifact</code>; reference the snippet and the
controller follows the link:</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">stages</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">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">grafana-dashboards</span><span class="w">
</span></span></span></code></pre></div><p>The controller also watches the common Flux source kinds (<code>GitRepository</code>,
<code>OCIRepository</code>, <code>Bucket</code>) so a stage re-reconciles when an upstream source
changes.</p>
<h2 id="related-projects">Related projects</h2>
<p>JOI, JaaS, and <code>StageSet</code> compose end to end:</p>
<ul>
<li><strong><a href="https://github.com/metio/jsonnet-oci-images">JOI</a></strong> publishes Jsonnet
libraries as single-layer OCI images (usable both as image-volume mounts and as
Flux <code>OCIRepository</code> sources).</li>
<li><strong><a href="https://jaas.projects.metio.wtf/">JaaS</a></strong> evaluates Jsonnet — optionally
importing those JOI libraries — and publishes the rendered JSON as an
<code>ExternalArtifact</code>.</li>
<li><strong><code>StageSet</code></strong> references the <code>JsonnetSnippet</code> (or its artifact) and rolls the
result out in ordered, gated stages.</li>
</ul>
<p>Each project is independently useful; a stage reads straight from a
<code>GitRepository</code>, <code>OCIRepository</code>, or <code>Bucket</code>, or from any <code>ExternalArtifact</code>
regardless of what produced it.</p>
]]></content><category scheme="https://stageset.projects.metio.wtf/tags/sources" term="sources" label="sources"/><category scheme="https://stageset.projects.metio.wtf/tags/externalartifact" term="externalartifact" label="externalartifact"/><category scheme="https://stageset.projects.metio.wtf/tags/jaas" term="jaas" label="jaas"/><category scheme="https://stageset.projects.metio.wtf/tags/stages" term="stages" label="stages"/></entry><entry><title type="html">Production</title><link href="https://stageset.projects.metio.wtf/installation/production/?utm_source=atom_feed" rel="alternate" type="text/html"/><link href="https://stageset.projects.metio.wtf/runbooks/webhook-cert-renewal/?utm_source=atom_feed" rel="related" type="text/html" title="Webhook cert renewal failing"/><link href="https://stageset.projects.metio.wtf/runbooks/controller-pod-down/?utm_source=atom_feed" rel="related" type="text/html" title="Controller pod down"/><link href="https://stageset.projects.metio.wtf/installation/operations/?utm_source=atom_feed" rel="related" type="text/html" title="Operations"/><link href="https://stageset.projects.metio.wtf/usage/encryption/?utm_source=atom_feed" rel="related" type="text/html" title="Secrets encryption (SOPS)"/><link href="https://stageset.projects.metio.wtf/runbooks/suspended/?utm_source=atom_feed" rel="related" type="text/html" title="Suspended"/><id>https://stageset.projects.metio.wtf/installation/production/</id><published>0001-01-01T00:00:00+00:00</published><updated>2026-06-16T13:41:17+02:00</updated><content type="html"><![CDATA[<blockquote>High availability, security hardening, the full flag reference, and an on-prem HA reference setup.</blockquote><h2 id="high-availability">High availability</h2>
<p>The controller supports leader-elected HA. Enable leader election and run more
than one replica; only the lease holder reconciles, while every replica answers
admission webhook calls (admission must stay available even on non-leaders).</p>
<ul>
<li>Leader election is toggled with <code>--leader-elect</code>. The binary defaults it to
<code>false</code>, but the <strong>Helm chart enables it by default</strong> (<code>controller.leaderElect: true</code>), so a default install is already lease-guarded even at one replica.</li>
<li>The lease is named <code>stageset-controller.stages.metio.wtf</code> and lives in the
controller&rsquo;s namespace. It uses controller-runtime&rsquo;s default timing (~15 s
lease duration). The lease is <strong>not</strong> released eagerly on shutdown, so after a
rolling update the new leader takes over when the old lease expires — budget a
few seconds of reconcile pause on restart (admission and the gate endpoint are
unaffected).</li>
<li>Scaling: when the chart&rsquo;s <code>replicas.max</code> exceeds <code>replicas.min</code> it renders a
<code>HorizontalPodAutoscaler</code> (CPU target 80%) and a <code>PodDisruptionBudget</code>
(<code>minAvailable: 1</code>). At the default 1/1 it sets neither and leaves
<code>spec.replicas</code> unmanaged.</li>
</ul>
<p>The controller watches every namespace by default. Multi-tenancy is enforced per
<code>StageSet</code> through impersonation (see below). You can additionally scope the
controller to a namespace set with <code>controller.watchNamespaces</code> — one controller
instance per tenant-group — and run it under <code>cluster-admin</code> for single-tenant
clusters; both are covered in
<a href="/usage/multi-cluster/">multi-cluster and tenancy</a>.</p>
<h2 id="hardening">Hardening</h2>
<p>Each option below is shown as the Helm values that configure it. Several are
already the chart&rsquo;s defaults, shown so you can see what is applied and override
it for a stricter policy.</p>
<h3 id="tenant-impersonation">Tenant impersonation</h3>
<p>The controller never applies your manifests with its own identity. Every cluster
operation for a <code>StageSet</code> — building, applying, pruning, running actions — is
performed impersonating the <code>StageSet</code>&rsquo;s <code>spec.serviceAccountName</code> (the chart
grants the controller <code>impersonate</code>, not write access). A <code>StageSet</code> can only do
what its tenant SA permits; an over-broad or missing SA fails closed.</p>
<p>This one lives on the <code>StageSet</code>, not in the chart — give every production
<code>StageSet</code> a scoped <code>ServiceAccount</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">stages.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">StageSet</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 class="w"> </span><span class="nt">name: payments, namespace</span><span class="p">:</span><span class="w"> </span><span class="l">payments }</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">payments-deployer  </span><span class="w"> </span><span class="c"># scoped to exactly this release&#39;s needs</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="c"># …</span><span class="w">
</span></span></span></code></pre></div><h3 id="pod-security-context">Pod security context</h3>
<p>The chart runs a non-root, read-only-root-filesystem pod with all capabilities
dropped, on a <code>gcr.io/distroless/static:nonroot</code> image (no shell or package
manager). These are the rendered defaults:</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">podSecurityContext</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">runAsNonRoot</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">seccompProfile</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">RuntimeDefault</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">securityContext</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">runAsNonRoot</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">runAsUser</span><span class="p">:</span><span class="w"> </span><span class="m">65532</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">runAsGroup</span><span class="p">:</span><span class="w"> </span><span class="m">65532</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">allowPrivilegeEscalation</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">readOnlyRootFilesystem</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">capabilities</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">drop</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="l">ALL]</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">seccompProfile</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">RuntimeDefault</span><span class="w">
</span></span></span></code></pre></div><h3 id="resource-limits">Resource limits</h3>
<p>Requests equal limits, so the pod is fully constrained:</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">cpu</span><span class="p">:</span><span class="w"> </span><span class="l">50m</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">ephemeralStorage</span><span class="p">:</span><span class="w"> </span><span class="l">32Mi  </span><span class="w"> </span><span class="c"># /tmp and the self-signed cert dir are emptyDirs</span><span class="w">
</span></span></span></code></pre></div><h3 id="pod-security-standards-namespace">Pod-Security Standards namespace</h3>
<p>Have the chart create the install namespace with restricted PSS labels:</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">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">pssLevel: restricted     # or</span><span class="p">:</span><span class="w"> </span><span class="l">baseline / privileged</span><span class="w">
</span></span></span></code></pre></div><h3 id="network-policy">Network policy</h3>
<p>The gate endpoint is <strong>unauthenticated</strong> (read-only
<code>GET /gate/{namespace}/{stageset}/{stage}</code>). Turn on the ingress-only NetworkPolicy
to fence it — and the webhook/metrics ports — to only the peers that need 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">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 class="c"># admits the webhook (9443), metrics (8080), gate (8082)</span><span class="w">
</span></span></span></code></pre></div><p>The policy is <strong>ingress-only</strong>, so it does not restrict egress — the controller can
still fetch stage artifacts over HTTP from source-controller (an <code>ExternalArtifact</code>
or a <code>GitRepository</code>/<code>OCIRepository</code>/<code>Bucket</code> is served from the same artifact
endpoint). If your cluster default-denies egress, add an egress allowance to
source-controller (and DNS) so those fetches succeed.</p>
<h3 id="admission-webhook-tls">Admission webhook TLS</h3>
<p><code>webhook.certMode</code> chooses how the webhook serving certificate is obtained:</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">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">certMode</span><span class="p">:</span><span class="w"> </span><span class="l">cert-manager  </span><span class="w"> </span><span class="c"># cert-manager issues + rotates the cert (requires cert-manager)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="c"># certMode: self-signed  # chart default: in-pod CA + serving cert, rotated at</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="c">#                          validity/3, with no cert-manager dependency</span><span class="w">
</span></span></span></code></pre></div><h2 id="reference-setups">Reference setups</h2>
<p>Two HA shapes — on-prem with shared RWX storage, and AWS/EKS with S3 — over the
same backbone: a leader-elected pair (or trio), a rollback store reachable from
whichever pod holds the lease, cert-manager for the webhook, a <code>NetworkPolicy</code>
fencing the unauthenticated gate, and a <code>ServiceMonitor</code> if you run Prometheus.</p>
<p>Both run two replicas for <a href="#high-availability">HA</a> (<code>replicas.max</code> above
<code>replicas.min</code> also renders a PDB and an HPA) and set
<code>webhook.certMode: cert-manager</code>, so <a href="https://cert-manager.io/">cert-manager</a> must
be installed in the cluster.</p>
<h3 id="on-prem-rwx-storage">On-prem (RWX storage)</h3>
<p>The rollback store gives bit-exact rollbacks that outlive producer GC. With HA
replicas it must be reachable from whichever pod holds the lease, so use a
<code>ReadWriteMany</code> PVC on your on-prem storage class — every replica mounts the same
volume.</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"># values-onprem.yaml</span><span class="w">
</span></span></span><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 class="c"># leader-elected HA; the non-leader still serves admission</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">3</span><span class="w">                 </span><span class="c"># &gt; min renders an HPA (CPU 80%) and a PodDisruptionBudget</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">controller</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">leaderElect</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="nt">rollbackStore</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">pvc</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">pvc</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">accessModes</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="l">ReadWriteMany]</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">storageClass</span><span class="p">:</span><span class="w"> </span><span class="l">nfs-client    </span><span class="w"> </span><span class="c"># your RWX class (NFS, CephFS, …)</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><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><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">certMode</span><span class="p">:</span><span class="w"> </span><span class="l">cert-manager        </span><span class="w"> </span><span class="c"># requires cert-manager in the cluster</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">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 class="c"># fences the unauthenticated gate endpoint</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">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">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></code></pre></div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">helm upgrade --install stageset-controller <span class="se">\
</span></span></span><span class="line"><span class="cl">  oci://ghcr.io/metio/helm-charts/stageset-controller <span class="se">\
</span></span></span><span class="line"><span class="cl">  --namespace stageset-system --create-namespace <span class="se">\
</span></span></span><span class="line"><span class="cl">  -f values-onprem.yaml
</span></span></code></pre></div><h3 id="aws--eks-s3">AWS / EKS (S3)</h3>
<p>On EKS, back the rollback store with S3 and let the controller assume an IAM role
through <a href="https://docs.aws.amazon.com/eks/latest/userguide/iam-roles-for-service-accounts.html">IRSA</a>
— no static keys. Annotate the controller&rsquo;s ServiceAccount with the role ARN and
leave the S3 credentials empty; the store&rsquo;s minio-go client picks the role up from
the pod&rsquo;s web-identity token.</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"># values-eks.yaml</span><span class="w">
</span></span></span><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">3</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">controller</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">leaderElect</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="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="c"># an IAM role granting s3:GetObject/PutObject/ListBucket/DeleteObject on the bucket</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::123456789012:role/stageset-controller</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">rollbackStore</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.eu-west-1.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-org-stageset-rollback</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="c"># no existingSecret → credentials come from the IRSA role above</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">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">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></span><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></span><span class="line"><span class="cl"><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">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></code></pre></div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">helm upgrade --install stageset-controller <span class="se">\
</span></span></span><span class="line"><span class="cl">  oci://ghcr.io/metio/helm-charts/stageset-controller <span class="se">\
</span></span></span><span class="line"><span class="cl">  --namespace stageset-system --create-namespace <span class="se">\
</span></span></span><span class="line"><span class="cl">  -f values-eks.yaml
</span></span></code></pre></div><h3 id="alongside-the-other-flux-controllers">Alongside the other Flux controllers</h3>
<p><code>stageset-controller</code> is a <a href="https://fluxcd.io/">Flux</a> citizen and needs no special
wiring to coexist with <code>source-controller</code>, <code>kustomize-controller</code>,
<code>helm-controller</code>, and <code>notification-controller</code>. It reads <code>ExternalArtifact</code> (and
the standard <code>GitRepository</code>, <code>OCIRepository</code>, and <code>Bucket</code> sources) from
<code>source-controller</code>, and <code>notification-controller</code> routes its events through an
<code>Alert</code> that targets <code>kind: StageSet</code> — no Provider/Alert plumbing of its own.
Install it in its own namespace (e.g. <code>stageset-system</code>) next to <code>flux-system</code>;
the only cluster-scoped pieces are its CRDs, <code>ClusterRole</code>, and webhook
configuration.</p>
<h3 id="alongside-jaas">Alongside JaaS</h3>
<p><a href="https://jaas.projects.metio.wtf/">JaaS</a> renders Jsonnet and publishes the result
as an <code>ExternalArtifact</code>, which is what a <code>StageSet</code> stage consumes — so the two
compose directly. Reference the artifact by name, or name the producing
<code>JsonnetSnippet</code> and let <code>stageset-controller</code> resolve it (see
<a href="/usage/producer-aware-sources/">producer-aware sources</a>). They can share a
cluster and namespace or stay separate; both are reconciled by Flux and both apply
under per-tenant impersonation, so the security model is consistent end to end.</p>
<h2 id="settings-you-can-tune">Settings you can tune</h2>
<p>The chart wires the controller; you set Helm values. The set worth thinking about
is below — each row is the value, its default, and when you&rsquo;d change it.
Everything else the chart configures for you (see
<a href="#what-the-chart-manages">what the chart manages</a>).</p>
<table>
	<thead>
			<tr>
					<th>Helm value</th>
					<th>Default</th>
					<th>When to change</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td><code>replicas.min</code> / <code>replicas.max</code></td>
					<td><code>1</code> / <code>1</code></td>
					<td>Raise both to ≥ 2 for HA; set <code>max &gt; min</code> to also render an HPA + PDB.</td>
			</tr>
			<tr>
					<td><code>controller.leaderElect</code></td>
					<td><code>true</code></td>
					<td>Leave on — harmless at one replica, required for HA.</td>
			</tr>
			<tr>
					<td><code>controller.defaultInterval</code></td>
					<td><code>10m</code></td>
					<td>The reconcile cadence StageSets inherit when they omit <code>spec.interval</code>. Lower for faster drift correction cluster-wide.</td>
			</tr>
			<tr>
					<td><code>controller.inventoryMode</code></td>
					<td><code>hybrid</code></td>
					<td><code>applyset</code> for ApplySet-native tooling; <code>entries</code> to drop the ApplySet labels.</td>
			</tr>
			<tr>
					<td><code>controller.inventoryShardCap</code></td>
					<td><code>5000</code></td>
					<td>Lower only if a stage applies a huge object count and you want smaller inventory objects.</td>
			</tr>
			<tr>
					<td><code>controller.allowedActionHosts</code></td>
					<td><code>[]</code></td>
					<td>Add host globs your <code>http</code> <a href="/usage/actions/">actions</a> must reach (loopback/link-local are always denied).</td>
			</tr>
			<tr>
					<td><code>controller.noCrossNamespaceRefs</code></td>
					<td><code>false</code></td>
					<td><code>true</code> to hard-isolate namespaces (deny cross-namespace <code>sourceRef</code>/<code>dependsOn</code>).</td>
			</tr>
			<tr>
					<td><code>controller.watchNamespaces</code></td>
					<td><code>[]</code></td>
					<td>Restrict the controller to a namespace list (cache + RBAC pivot to per-namespace bindings); empty watches cluster-wide. See <a href="/usage/multi-cluster/#scoping-the-controller-to-a-namespace-set">tenancy</a>.</td>
			</tr>
			<tr>
					<td><code>rbac.clusterAdmin</code></td>
					<td><code>false</code></td>
					<td><code>true</code> on <strong>single-tenant</strong> clusters to bind the controller SA to <code>cluster-admin</code> so StageSets apply without <code>serviceAccountName</code>. See <a href="/usage/multi-cluster/#single-tenant-cluster-admin">single-tenant</a>.</td>
			</tr>
			<tr>
					<td><code>controller.runbookBaseURL</code></td>
					<td>the docs site</td>
					<td>Point at a fork/mirror, or empty to drop the runbook links from Ready messages.</td>
			</tr>
			<tr>
					<td><code>webhook.certMode</code></td>
					<td><code>self-signed</code></td>
					<td><code>cert-manager</code> if you run cert-manager — see <a href="#reference-setups">reference setups</a>.</td>
			</tr>
			<tr>
					<td><code>gate.enabled</code></td>
					<td><code>true</code></td>
					<td>Leave on for <a href="/tutorials/progressive-delivery/">progressive delivery</a> (the Flagger/Argo gate); set <code>false</code> to drop the gate Service and endpoint.</td>
			</tr>
			<tr>
					<td><code>rollbackStore.backend</code></td>
					<td><code>none</code></td>
					<td><code>pvc</code> (RWX) or <code>s3</code> to enable <a href="/usage/rollback/"><code>spec.rollbackOnFailure</code></a>; the two are mutually exclusive.</td>
			</tr>
			<tr>
					<td><code>rollbackStore.s3.sse</code></td>
					<td><code>s3</code></td>
					<td>At-rest encryption for the S3 store (it holds rendered Secret data): <code>s3</code> (SSE-S3), <code>kms</code> (+<code>sseKmsKeyId</code>), or <code>none</code>. See <a href="/usage/rollback/#encryption-at-rest">encryption at rest</a>.</td>
			</tr>
			<tr>
					<td><code>networkPolicy.enabled</code></td>
					<td><code>false</code></td>
					<td><code>true</code> to fence the controller and the unauthenticated gate.</td>
			</tr>
			<tr>
					<td><code>metrics.serviceMonitor.enabled</code></td>
					<td><code>false</code></td>
					<td><code>true</code> if you scrape with the Prometheus operator.</td>
			</tr>
			<tr>
					<td><code>metrics.prometheusRule.enabled</code></td>
					<td><code>false</code></td>
					<td><code>true</code> for the bundled <a href="/installation/operations/#alerts">alerts</a>.</td>
			</tr>
			<tr>
					<td><code>serviceAccount.annotations</code></td>
					<td><code>{}</code></td>
					<td>An IRSA role ARN on EKS so the S3 store uses an IAM role.</td>
			</tr>
			<tr>
					<td><code>namespace.create</code></td>
					<td><code>false</code></td>
					<td><code>true</code> to have the chart create the install namespace with Pod-Security labels.</td>
			</tr>
			<tr>
					<td><code>resources</code></td>
					<td>requests = limits</td>
					<td>Raise for very large or very busy releases.</td>
			</tr>
	</tbody>
</table>
<p>Every option is set the same way — in your values file, applied with
<code>helm upgrade --install … -f values.yaml</code>. The <a href="#reference-setups">reference setups</a>
above are complete, copy-pasteable examples.</p>
<h2 id="what-the-chart-manages">What the chart manages</h2>
<p>You do <strong>not</strong> configure these — the chart wires them so the controller behaves
correctly out of the box:</p>
<ul>
<li><strong>Leader election and HA plumbing</strong> — the lease, and the PDB/HPA when
<code>replicas.max &gt; replicas.min</code>.</li>
<li><strong>The admission webhook</strong> — the server, its Service, the
<code>ValidatingWebhookConfiguration</code>, and the certificate (cert-manager <code>Certificate</code>
or the in-pod self-signed renewer, per <code>webhook.certMode</code>).</li>
<li><strong>Endpoints</strong> — metrics, health probes, and the gate, on their Services.</li>
<li><strong>RBAC</strong> — the ClusterRole/bindings the controller needs, including the
<code>impersonate</code> verb (it never applies as itself).</li>
<li><strong>A hardened pod</strong> — non-root, read-only root filesystem, dropped capabilities,
seccomp <code>RuntimeDefault</code> (see <a href="#pod-security-context">pod security context</a>).</li>
<li><strong>Per-tenant impersonation</strong> — every apply runs as the StageSet&rsquo;s
<code>spec.serviceAccountName</code>.</li>
</ul>
<h2 id="controller-flags-reference">Controller flags reference</h2>
<p>You never pass these directly — the chart sets them from your Helm values and its
own defaults — but it helps to know what each does and which value drives it.</p>
<table>
	<thead>
			<tr>
					<th>Flag</th>
					<th>Default</th>
					<th>Purpose</th>
					<th>Driven by</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td><code>--metrics-bind-address</code></td>
					<td><code>:8080</code></td>
					<td>Prometheus metrics endpoint.</td>
					<td><em>chart-managed</em></td>
			</tr>
			<tr>
					<td><code>--health-probe-bind-address</code></td>
					<td><code>:8081</code></td>
					<td><code>/healthz</code> + <code>/readyz</code>.</td>
					<td><em>chart-managed</em></td>
			</tr>
			<tr>
					<td><code>--leader-elect</code></td>
					<td><code>false</code></td>
					<td>Enable leader election.</td>
					<td><code>controller.leaderElect</code></td>
			</tr>
			<tr>
					<td><code>--inventory-mode</code></td>
					<td><code>hybrid</code></td>
					<td>Inventory strategy: <code>entries</code>, <code>hybrid</code>, <code>applyset</code>.</td>
					<td><code>controller.inventoryMode</code></td>
			</tr>
			<tr>
					<td><code>--inventory-shard-cap</code></td>
					<td><code>5000</code></td>
					<td>Max entries per <code>StageInventory</code> shard.</td>
					<td><code>controller.inventoryShardCap</code></td>
			</tr>
			<tr>
					<td><code>--allowed-action-hosts</code></td>
					<td><em>(none)</em></td>
					<td>Host glob permitted for <code>http</code> actions (loopback/link-local always denied).</td>
					<td><code>controller.allowedActionHosts</code></td>
			</tr>
			<tr>
					<td><code>--no-cross-namespace-refs</code></td>
					<td><code>false</code></td>
					<td>Deny cross-namespace <code>sourceRef</code> / <code>dependsOn</code>.</td>
					<td><code>controller.noCrossNamespaceRefs</code></td>
			</tr>
			<tr>
					<td><code>--watch-namespaces</code></td>
					<td><em>(all)</em></td>
					<td>Comma-separated namespaces to watch; empty is cluster-wide. Falls back to <code>STAGESET_WATCH_NAMESPACES</code>.</td>
					<td><code>controller.watchNamespaces</code></td>
			</tr>
			<tr>
					<td><code>--enable-webhook</code></td>
					<td><code>true</code></td>
					<td>Run the validating admission webhook.</td>
					<td><em>chart-managed</em></td>
			</tr>
			<tr>
					<td><code>--webhook-cert-mode</code></td>
					<td><code>cert-manager</code></td>
					<td><code>cert-manager</code> or <code>self-signed</code>.</td>
					<td><code>webhook.certMode</code></td>
			</tr>
			<tr>
					<td><code>--webhook-cert-dir</code></td>
					<td><code>/tmp/k8s-webhook-server/serving-certs</code></td>
					<td>Where <code>tls.crt</code>/<code>tls.key</code> live.</td>
					<td><em>chart-managed</em></td>
			</tr>
			<tr>
					<td><code>--webhook-port</code></td>
					<td><code>9443</code></td>
					<td>Webhook server port.</td>
					<td><em>chart-managed</em></td>
			</tr>
			<tr>
					<td><code>--webhook-cert-validity</code></td>
					<td><code>8760h</code></td>
					<td>Self-signed cert lifetime (rotates at ⅓).</td>
					<td><code>webhook.*</code></td>
			</tr>
			<tr>
					<td><code>--webhook-service-name</code></td>
					<td><code>stageset-controller-webhook</code></td>
					<td>Service used to build cert SANs.</td>
					<td><em>chart-managed</em></td>
			</tr>
			<tr>
					<td><code>--webhook-service-namespace</code></td>
					<td><em>(in-cluster ns)</em></td>
					<td>Namespace of the webhook Service.</td>
					<td><em>chart-managed</em></td>
			</tr>
			<tr>
					<td><code>--webhook-validating-config-name</code></td>
					<td><em>(none)</em></td>
					<td>The <code>ValidatingWebhookConfiguration</code> to patch.</td>
					<td><em>chart-managed</em></td>
			</tr>
			<tr>
					<td><code>--gate-bind-address</code></td>
					<td><code>:8082</code></td>
					<td>Read-only gate endpoint; empty disables.</td>
					<td><code>gate.enabled</code></td>
			</tr>
			<tr>
					<td><code>--runbook-base-url</code></td>
					<td><em>(none)</em></td>
					<td>URL prefix on actionable Ready messages.</td>
					<td><code>controller.runbookBaseURL</code></td>
			</tr>
			<tr>
					<td><code>--rollback-store-path</code></td>
					<td><em>(none)</em></td>
					<td>Filesystem (RWX PVC) rollback store.</td>
					<td><code>rollbackStore.backend: pvc</code></td>
			</tr>
			<tr>
					<td><code>--rollback-store-s3-*</code></td>
					<td><em>(off)</em></td>
					<td>S3-compatible rollback store.</td>
					<td><code>rollbackStore.backend: s3</code></td>
			</tr>
			<tr>
					<td><code>--rollback-store-s3-sse</code></td>
					<td><code>s3</code></td>
					<td>At-rest encryption for the S3 store: <code>none</code>, <code>s3</code>, <code>kms</code>.</td>
					<td><code>rollbackStore.s3.sse</code></td>
			</tr>
	</tbody>
</table>
<p>The PVC and S3 rollback stores are mutually exclusive — the chart enforces it via
<code>rollbackStore.backend</code>. The controller also accepts controller-runtime&rsquo;s <code>--zap-*</code>
logging flags.</p>
]]></content><category scheme="https://stageset.projects.metio.wtf/tags/production" term="production" label="production"/><category scheme="https://stageset.projects.metio.wtf/tags/security" term="security" label="security"/><category scheme="https://stageset.projects.metio.wtf/tags/operations" term="operations" label="operations"/></entry><entry><title type="html">Progressive delivery</title><link href="https://stageset.projects.metio.wtf/tutorials/progressive-delivery/?utm_source=atom_feed" rel="alternate" type="text/html"/><link href="https://stageset.projects.metio.wtf/comparisons/argo-rollouts/?utm_source=atom_feed" rel="related" type="text/html" title="StageSet vs Argo Rollouts"/><link href="https://stageset.projects.metio.wtf/tutorials/jsonnet-to-rollout/?utm_source=atom_feed" rel="related" type="text/html" title="From Jsonnet to a gated rollout"/><link href="https://stageset.projects.metio.wtf/usage/actions/?utm_source=atom_feed" rel="related" type="text/html" title="Actions"/><link href="https://stageset.projects.metio.wtf/usage/conflict-policies/?utm_source=atom_feed" rel="related" type="text/html" title="Conflict policies"/><link href="https://stageset.projects.metio.wtf/runbooks/dependencynotready/?utm_source=atom_feed" rel="related" type="text/html" title="DependencyNotReady"/><id>https://stageset.projects.metio.wtf/tutorials/progressive-delivery/</id><published>0001-01-01T00:00:00+00:00</published><updated>2026-06-16T13:41:17+02:00</updated><content type="html"><![CDATA[<blockquote>Gate a Flagger or Argo Rollouts promotion on a StageSet stage, and gate a StageSet stage on a Rollout.</blockquote><p><code>StageSet</code> integrates with both progressive-delivery controllers:
<a href="https://flagger.app/">Flagger</a> and
<a href="https://argoproj.github.io/argo-rollouts/">Argo Rollouts</a>. The controller exposes
a read-only gate endpoint and a readiness gauge so either one can hold a promotion
until a <code>StageSet</code> stage is healthy; ready checks let a stage wait on a Rollout in
return. Pick the section for your controller below — see also
<a href="/comparisons/argo-rollouts/">StageSet vs Argo Rollouts</a>.</p>
<h2 id="the-gate-contract">The gate contract</h2>
<p>The gate endpoint backs the Flagger integration and the Argo Rollouts JSON-metric
option.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">GET /gate/{namespace}/{stageset}/{stage}
</span></span><span class="line"><span class="cl">  200  — the stage is Ready at the currently pinned revision
</span></span><span class="line"><span class="cl">  403  — the stage is not Ready (or not found / not gateable)
</span></span></code></pre></div><p>It is served on <code>--gate-bind-address</code> (default <code>:8082</code>) and exposed by the chart&rsquo;s
<code>stageset-controller-gate</code> Service (<code>gate.enabled</code>, on by default). The endpoint is
<strong>unauthenticated and read-only</strong>, so fence it with a <code>NetworkPolicy</code>
(<a href="/installation/production/#network-policy">production</a>) to admit only your
delivery controller.</p>
<h2 id="flagger">Flagger</h2>
<p>Add a <code>confirm-promotion</code> (or <code>confirm-rollout</code>) webhook to a Flagger <code>Canary</code>
pointing at the gate. Flagger blocks the promotion until the gate returns <code>200</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">flagger.app/v1beta1</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">Canary</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">web</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">apps</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">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">apiVersion</span><span class="p">:</span><span class="w"> </span><span class="l">apps/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">Deployment</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">web</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">analysis</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">1m</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">threshold</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 class="nt">stepWeight</span><span class="p">:</span><span class="w"> </span><span class="m">10</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">maxWeight</span><span class="p">:</span><span class="w"> </span><span class="m">50</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">webhooks</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">stageset-stage-ready</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">confirm-promotion</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="c"># gate this canary&#39;s promotion on a StageSet stage being Ready</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">http://stageset-controller-gate.stageset-system:8082/gate/apps/web/web</span><span class="w">
</span></span></span></code></pre></div><p>This is independent of the Flagger <em>strategy</em>: the same webhook gates a weighted
<strong>canary</strong>, an <strong>A/B test</strong> (header/cookie routing), or a <strong>blue-green</strong> promotion
— the gate only answers &ldquo;is this stage Ready,&rdquo; and Flagger decides what to do with
that answer.</p>
<p>This coordinates two moving parts: Flagger shifts traffic to a new version only once
a StageSet stage that applied the supporting config (a CRD, a migration, a sibling
component) reports Ready.</p>
<h2 id="argo-rollouts">Argo Rollouts</h2>
<p>Argo Rollouts gates on <strong>analysis metrics</strong> (a query that returns a value to
compare) rather than a webhook&rsquo;s HTTP status, so the controller meets it on its own
terms in two ways.</p>
<h3 id="gate-on-the-readiness-gauge-recommended">Gate on the readiness gauge (recommended)</h3>
<p>The controller exports <code>stageset_stage_ready{namespace,stageset,stage}</code> (<code>1</code> when
the stage is Ready, <code>0</code> otherwise). Argo&rsquo;s <strong>Prometheus</strong> metric provider gates on
it directly — no gate endpoint, no Job:</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">argoproj.io/v1alpha1</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">AnalysisTemplate</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">stageset-stage-ready</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">apps</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">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">name</span><span class="p">:</span><span class="w"> </span><span class="l">stage-ready</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">successCondition</span><span class="p">:</span><span class="w"> </span><span class="l">result == 1</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></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">prometheus</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">address</span><span class="p">:</span><span class="w"> </span><span class="l">http://prometheus.monitoring:9090</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">query</span><span class="p">:</span><span class="w"> </span><span class="l">max(stageset_stage_ready{namespace=&#34;apps&#34;,stageset=&#34;web&#34;,stage=&#34;web&#34;})</span><span class="w">
</span></span></span></code></pre></div><h3 id="gate-on-the-json-endpoint">Gate on the JSON endpoint</h3>
<p>The same gate endpoint also answers JSON when asked
(<code>Accept: application/json</code>), returning <code>{&quot;ready&quot;: true, …}</code> with a <code>200</code> so Argo&rsquo;s
<strong>web</strong> metric can parse it (Argo treats a non-2xx as an error, so readiness has to
live in the body):</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">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">name</span><span class="p">:</span><span class="w"> </span><span class="l">stage-ready</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">successCondition</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;result.ready == true&#34;</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></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">web</span><span class="p">:</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">http://stageset-controller-gate.stageset-system:8082/gate/apps/web/web</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">headers</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">Accept</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">              </span><span class="nt">value</span><span class="p">:</span><span class="w"> </span><span class="l">application/json</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">jsonPath</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;{$}&#34;</span><span class="w">
</span></span></span></code></pre></div><p>A <strong>Job-based metric</strong> (<code>curl -fsS …</code> against the gate, succeeding only on <code>200</code>)
is the fallback when the analysis has no Prometheus or web access.</p>
<h2 id="the-reverse-direction-gate-a-stageset-on-a-rollout">The reverse direction: gate a StageSet on a Rollout</h2>
<p>The coordination also works the other way. Because
<a href="/usage/ready-checks/">ready checks</a> accept CEL, a StageSet stage can wait on an
Argo <code>Rollout</code> finishing its own progressive rollout before the next stage runs:</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">readyChecks</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">exprs</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">argoproj.io/v1alpha1</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">Rollout</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">current</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;status.phase == &#39;Healthy&#39;&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">inProgress</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;status.phase in [&#39;Progressing&#39;, &#39;Paused&#39;]&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">failed</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;status.phase == &#39;Degraded&#39;&#34;</span><span class="w">
</span></span></span></code></pre></div><p>So StageSet can gate Argo (via the gauge/gate) and Argo&rsquo;s outcome can gate
StageSet (via ready checks) — pick whichever direction your release needs.</p>
]]></content><category scheme="https://stageset.projects.metio.wtf/tags/tutorials" term="tutorials" label="tutorials"/><category scheme="https://stageset.projects.metio.wtf/tags/progressive-delivery" term="progressive-delivery" label="progressive-delivery"/><category scheme="https://stageset.projects.metio.wtf/tags/flagger" term="flagger" label="flagger"/><category scheme="https://stageset.projects.metio.wtf/tags/argo" term="argo" label="argo"/><category scheme="https://stageset.projects.metio.wtf/tags/stages" term="stages" label="stages"/></entry><entry><title type="html">Ready checks</title><link href="https://stageset.projects.metio.wtf/usage/ready-checks/?utm_source=atom_feed" rel="alternate" type="text/html"/><link href="https://stageset.projects.metio.wtf/usage/actions/?utm_source=atom_feed" rel="related" type="text/html" title="Actions"/><link href="https://stageset.projects.metio.wtf/runbooks/succeeded/?utm_source=atom_feed" rel="related" type="text/html" title="Succeeded"/><link href="https://stageset.projects.metio.wtf/usage/conflict-policies/?utm_source=atom_feed" rel="related" type="text/html" title="Conflict policies"/><link href="https://stageset.projects.metio.wtf/runbooks/dependencynotready/?utm_source=atom_feed" rel="related" type="text/html" title="DependencyNotReady"/><link href="https://stageset.projects.metio.wtf/tutorials/jsonnet-to-rollout/?utm_source=atom_feed" rel="related" type="text/html" title="From Jsonnet to a gated rollout"/><id>https://stageset.projects.metio.wtf/usage/ready-checks/</id><published>0001-01-01T00:00:00+00:00</published><updated>2026-06-16T13:41:17+02:00</updated><content type="html"><![CDATA[<blockquote>Gate a stage on health with kstatus and CEL expressions.</blockquote><p>Ready checks decide when a stage is healthy enough to let the next stage start.
They are purely observational — the controller waits and reports, but takes no
action (active steps are <a href="/usage/actions/">actions</a>).</p>
<p>By default, with no <code>readyChecks</code> block, the controller waits for <strong>every</strong> object
the stage applied to report ready via
<a href="https://github.com/kubernetes-sigs/cli-utils/tree/master/pkg/kstatus">kstatus</a>.
<code>readyChecks</code> lets you narrow that to specific objects (<code>checks</code>), add custom
health for resources kstatus doesn&rsquo;t understand (<code>exprs</code>, <a href="https://github.com/google/cel-spec">CEL</a>),
bound the wait (<code>timeout</code>), or skip it entirely (<code>disableWait</code>). <code>checks</code> and
<code>exprs</code> may be set together.</p>
<h2 id="explicit-objects">Explicit objects</h2>
<p>Wait for named objects only — useful when a stage applies many objects but only a
few gate the next stage:</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">stages</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">infrastructure</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">name</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="nt">readyChecks</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">timeout</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">checks</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">apiextensions.k8s.io/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">CustomResourceDefinition</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">ledgers.payments.example</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">apps/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">Deployment</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">ledger-operator</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">platform-system</span><span class="w">
</span></span></span></code></pre></div><h2 id="custom-health-with-cel">Custom health with CEL</h2>
<p>For custom resources kstatus doesn&rsquo;t understand, describe readiness with CEL
expressions. The shape matches <code>kustomize-controller</code>&rsquo;s <code>healthCheckExprs</code>, so
expressions are portable.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="w">      </span><span class="nt">readyChecks</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">exprs</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">db.example/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">Database</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span><span class="nt">current</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;status.phase == &#39;Running&#39;&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span><span class="nt">inProgress</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;status.phase in [&#39;Pending&#39;, &#39;Provisioning&#39;]&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span><span class="nt">failed</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;status.phase == &#39;Failed&#39;&#34;</span><span class="w">
</span></span></span></code></pre></div><h2 id="opting-out">Opting out</h2>
<p>To apply a stage without waiting for readiness (fire-and-forget), disable the
wait:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="w">      </span><span class="nt">readyChecks</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">disableWait</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="w">
</span></span></span></code></pre></div>]]></content><category scheme="https://stageset.projects.metio.wtf/tags/ready-checks" term="ready-checks" label="ready-checks"/><category scheme="https://stageset.projects.metio.wtf/tags/health" term="health" label="health"/><category scheme="https://stageset.projects.metio.wtf/tags/stages" term="stages" label="stages"/></entry><entry><title type="html">Reconcile latency high</title><link href="https://stageset.projects.metio.wtf/runbooks/reconcile-latency/?utm_source=atom_feed" rel="alternate" type="text/html"/><link href="https://stageset.projects.metio.wtf/runbooks/workqueue-saturation/?utm_source=atom_feed" rel="related" type="text/html" title="Workqueue saturation"/><link href="https://stageset.projects.metio.wtf/runbooks/controller-pod-down/?utm_source=atom_feed" rel="related" type="text/html" title="Controller pod down"/><link href="https://stageset.projects.metio.wtf/installation/operations/?utm_source=atom_feed" rel="related" type="text/html" title="Operations"/><link href="https://stageset.projects.metio.wtf/runbooks/artifactnotfound/?utm_source=atom_feed" rel="related" type="text/html" title="ArtifactNotFound"/><link href="https://stageset.projects.metio.wtf/runbooks/dependencynotready/?utm_source=atom_feed" rel="related" type="text/html" title="DependencyNotReady"/><id>https://stageset.projects.metio.wtf/runbooks/reconcile-latency/</id><published>0001-01-01T00:00:00+00:00</published><updated>2026-06-16T13:41:17+02:00</updated><content type="html"><![CDATA[<blockquote>Reconcile p99 latency for the StageSet controller is above threshold.</blockquote><h2 id="symptom">Symptom</h2>
<p><code>controller_runtime_reconcile_time_seconds</code> p99 for <code>controller=&quot;stageset&quot;</code> exceeds
the configured threshold; the <code>StageSetReconcileLatencyHigh</code> alert fires (see
<a href="/installation/operations/">operations</a> for the alert set and its thresholds).</p>
<h2 id="cause">Cause</h2>
<p>A single reconcile does a lot of work — resolve and fetch every stage&rsquo;s artifact,
kustomize-build, server-side apply, prune, verify readiness, and run actions — all
impersonating the tenant <code>ServiceAccount</code>. Latency climbs when any of those is slow:</p>
<ul>
<li>large artifacts or slow artifact servers,</li>
<li>many objects per stage (apply + prune scale with object count),</li>
<li>readiness waits and <code>wait</code>/<code>http</code>/<code>job</code> actions with long timeouts,</li>
<li>apiserver or tenant-authorization slowness.</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 -n stageset-system logs deploy/stageset-controller --tail<span class="o">=</span><span class="m">200</span> <span class="p">|</span> grep -i <span class="s1">&#39;slow\|timeout\|took&#39;</span>
</span></span></code></pre></div><p>Break the latency down by stage count and artifact size; a single StageSet with
many large stages dominates p99.</p>
<h2 id="remediation">Remediation</h2>
<ul>
<li>Split a very large StageSet into smaller ones, or fewer objects per stage.</li>
<li>Tighten action <code>timeout</code>s so a slow gate fails fast instead of stretching the
reconcile.</li>
<li>Raise <code>spec.interval</code> where freshness isn&rsquo;t critical.</li>
<li>Address upstream artifact-server or apiserver latency.</li>
</ul>
<p>If the queue itself is backing up, see <a href="/runbooks/workqueue-saturation/">workqueue saturation</a>.</p>
]]></content><category scheme="https://stageset.projects.metio.wtf/tags/runbooks" term="runbooks" label="runbooks"/><category scheme="https://stageset.projects.metio.wtf/tags/metrics" term="metrics" label="metrics"/><category scheme="https://stageset.projects.metio.wtf/tags/alerts" term="alerts" label="alerts"/><category scheme="https://stageset.projects.metio.wtf/tags/troubleshooting" term="troubleshooting" label="troubleshooting"/></entry><entry><title type="html">ResolveFailed</title><link href="https://stageset.projects.metio.wtf/runbooks/resolvefailed/?utm_source=atom_feed" rel="alternate" type="text/html"/><link href="https://stageset.projects.metio.wtf/runbooks/artifactnotfound/?utm_source=atom_feed" rel="related" type="text/html" title="ArtifactNotFound"/><link href="https://stageset.projects.metio.wtf/runbooks/sourcenotready/?utm_source=atom_feed" rel="related" type="text/html" title="SourceNotReady"/><link href="https://stageset.projects.metio.wtf/runbooks/controller-pod-down/?utm_source=atom_feed" rel="related" type="text/html" title="Controller pod down"/><link href="https://stageset.projects.metio.wtf/runbooks/dependencynotready/?utm_source=atom_feed" rel="related" type="text/html" title="DependencyNotReady"/><link href="https://stageset.projects.metio.wtf/runbooks/downgraderequiresmigration/?utm_source=atom_feed" rel="related" type="text/html" title="DowngradeRequiresMigration"/><id>https://stageset.projects.metio.wtf/runbooks/resolvefailed/</id><published>0001-01-01T00:00:00+00:00</published><updated>2026-06-16T13:41:17+02:00</updated><content type="html"><![CDATA[<blockquote>A source reference could not be resolved to a ready ExternalArtifact.</blockquote><h2 id="symptom">Symptom</h2>
<p><code>READY=False</code>, <code>REASON=ResolveFailed</code>. The Message describes why resolution failed.</p>
<h2 id="cause">Cause</h2>
<p>A stage&rsquo;s <code>sourceRef</code> could not be resolved to an <code>ExternalArtifact</code> for a spec/config or API reason (distinct from &ldquo;not published yet&rdquo;, which is <a href="/runbooks/sourcenotready/"><code>SourceNotReady</code></a>, and &ldquo;no such object&rdquo;, which is <a href="/runbooks/artifactnotfound/"><code>ArtifactNotFound</code></a>). Common cases:</p>
<ul>
<li>an <strong>ambiguous producer</strong> — more than one <code>ExternalArtifact</code> back-points at the same producer object, so the target is undefined;</li>
<li>a <strong>cross-namespace ref rejected</strong> by <code>--no-cross-namespace-refs</code>;</li>
<li>an <strong>API error</strong> reading the source or artifact (RBAC denial, the artifact CRD not installed).</li>
</ul>
<p>When the failing <code>sourceRef</code> targets another namespace, the Message is deliberately scrubbed to <code>cross-namespace &lt;kind&gt; %q is not reachable</code> so tenants cannot fingerprint other namespaces — check that source CR&rsquo;s status in its own namespace.</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 stageset &lt;name&gt; -n &lt;namespace&gt;
</span></span><span class="line"><span class="cl"><span class="c1"># Ambiguity: are there multiple artifacts pointing at the producer?</span>
</span></span><span class="line"><span class="cl">kubectl get externalartifact -n &lt;namespace&gt; -o yaml <span class="p">|</span> grep -A3 sourceRef
</span></span></code></pre></div><h2 id="remediation">Remediation</h2>
<ul>
<li><strong>Ambiguous producer:</strong> ensure exactly one <code>ExternalArtifact</code> back-points at the producer, or reference the <code>ExternalArtifact</code> directly by name.</li>
<li><strong>Cross-namespace rejected:</strong> move the source into the StageSet&rsquo;s namespace, or run the controller without <code>--no-cross-namespace-refs</code> if your <a href="/usage/multi-cluster/">tenancy model</a> allows it.</li>
<li><strong>RBAC / missing CRD:</strong> grant the controller (or the impersonated <code>serviceAccountName</code>) read on the source kind, or install the <code>source-controller</code> CRDs.</li>
</ul>
]]></content><category scheme="https://stageset.projects.metio.wtf/tags/runbooks" term="runbooks" label="runbooks"/><category scheme="https://stageset.projects.metio.wtf/tags/sources" term="sources" label="sources"/><category scheme="https://stageset.projects.metio.wtf/tags/externalartifact" term="externalartifact" label="externalartifact"/><category scheme="https://stageset.projects.metio.wtf/tags/troubleshooting" term="troubleshooting" label="troubleshooting"/></entry><entry><title type="html">Rollback</title><link href="https://stageset.projects.metio.wtf/usage/rollback/?utm_source=atom_feed" rel="alternate" type="text/html"/><link href="https://stageset.projects.metio.wtf/usage/actions/?utm_source=atom_feed" rel="related" type="text/html" title="Actions"/><link href="https://stageset.projects.metio.wtf/usage/conflict-policies/?utm_source=atom_feed" rel="related" type="text/html" title="Conflict policies"/><link href="https://stageset.projects.metio.wtf/runbooks/dependencynotready/?utm_source=atom_feed" rel="related" type="text/html" title="DependencyNotReady"/><link href="https://stageset.projects.metio.wtf/runbooks/downgraderequiresmigration/?utm_source=atom_feed" rel="related" type="text/html" title="DowngradeRequiresMigration"/><link href="https://stageset.projects.metio.wtf/tutorials/jsonnet-to-rollout/?utm_source=atom_feed" rel="related" type="text/html" title="From Jsonnet to a gated rollout"/><id>https://stageset.projects.metio.wtf/usage/rollback/</id><published>0001-01-01T00:00:00+00:00</published><updated>2026-06-16T13:41:17+02:00</updated><content type="html"><![CDATA[<blockquote>Restore the last good artifact revision when a run fails.</blockquote><p>When a run fails, the controller can restore the last successfully-applied artifact
revisions instead of leaving you on a broken release. Rollback is opt-in and needs
somewhere to keep prior revisions.</p>
<h2 id="enabling-it">Enabling it</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">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">rollbackOnFailure</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">stages</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">app</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">name</span><span class="p">:</span><span class="w"> </span><span class="l">my-app</span><span class="w">
</span></span></span></code></pre></div><p>On a failed run the controller restores each stage&rsquo;s last-good artifact revision,
best-effort, and emits a <code>RolledBack</code> event. The coordinates it restores from are
recorded in <code>status.lastAppliedSnapshot</code>.</p>
<h2 id="the-rollback-store">The rollback store</h2>
<p>Rollback needs the prior revision to still be fetchable, so the controller keeps a
copy in a <strong>rollback store</strong>. Configure one on the controller (cluster-wide), via
either a shared filesystem or S3:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl"># filesystem (an RWX PersistentVolumeClaim)
</span></span><span class="line"><span class="cl">--rollback-store-path=/var/lib/stageset/rollback
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"># or S3-compatible object storage
</span></span><span class="line"><span class="cl">--rollback-store-s3-endpoint=s3.example.com
</span></span><span class="line"><span class="cl">--rollback-store-s3-bucket=stageset-rollback
</span></span></code></pre></div><p>The two are mutually exclusive. With no store configured, rollback can only use a
prior revision the producer itself still retains; a dedicated store makes rollback
reliable across producer pruning.</p>
<h3 id="encryption-at-rest">Encryption at rest</h3>
<p>The store keeps each stage&rsquo;s rendered output, which includes any <code>Secret</code>&rsquo;s data —
including <a href="https://github.com/getsops/sops">SOPS</a>-decrypted values (see
<a href="/usage/encryption/">secrets encryption</a>). Treat it as sensitive and keep it
encrypted at rest:</p>
<ul>
<li><strong>S3</strong> encrypts by default. <code>--rollback-store-s3-sse</code> (chart:
<code>rollbackStore.s3.sse</code>) is <code>s3</code> (SSE-S3) out of the box; set <code>kms</code> with
<code>rollbackStore.s3.sseKmsKeyId</code> for SSE-KMS, or <code>none</code> only for a backend that
cannot honor an SSE header. A rejected SSE write is non-fatal — it warns via a
<code>RollbackStoreFailed</code> event and skips the store write; the rollout still
succeeds.</li>
<li><strong>Filesystem</strong> can&rsquo;t encrypt itself — back the PVC with an <strong>encrypted volume</strong>
(an encrypted <code>StorageClass</code>, LUKS, or cloud-disk encryption). The controller
logs a reminder at startup when the file store is enabled.</li>
</ul>
<p>If a restore can&rsquo;t proceed because the previous revision is gone, the run fails
with the <code>PreviousRevisionUnavailable</code> reason (see its
<a href="/runbooks/previousrevisionunavailable/">runbook</a>), and a store problem surfaces as
a <code>RollbackStoreFailed</code> event.</p>
]]></content><category scheme="https://stageset.projects.metio.wtf/tags/rollback" term="rollback" label="rollback"/><category scheme="https://stageset.projects.metio.wtf/tags/stages" term="stages" label="stages"/><category scheme="https://stageset.projects.metio.wtf/tags/versioning" term="versioning" label="versioning"/></entry><entry><title type="html">Secrets encryption (SOPS)</title><link href="https://stageset.projects.metio.wtf/usage/encryption/?utm_source=atom_feed" rel="alternate" type="text/html"/><link href="https://stageset.projects.metio.wtf/installation/production/?utm_source=atom_feed" rel="related" type="text/html" title="Production"/><link href="https://stageset.projects.metio.wtf/runbooks/webhook-cert-renewal/?utm_source=atom_feed" rel="related" type="text/html" title="Webhook cert renewal failing"/><id>https://stageset.projects.metio.wtf/usage/encryption/</id><published>0001-01-01T00:00:00+00:00</published><updated>2026-06-16T13:59:54+02:00</updated><content type="html"><![CDATA[<blockquote>Decrypt SOPS-encrypted files in a stage&rsquo;s source before they are applied.</blockquote><p>A stage&rsquo;s source can carry <a href="https://github.com/getsops/sops">SOPS</a>-encrypted
files — typically a <code>Secret</code> whose values are encrypted — and the controller
decrypts them in memory, before building and applying the manifests. This mirrors
Flux&rsquo;s <code>kustomize-controller</code> decryption contract, so an existing SOPS-encrypted
repository works unchanged.</p>
<p>Set <code>spec.decryption</code> and point it at a Secret holding the keys:</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">stages.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">StageSet</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">payments</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">payments</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">payments-deployer</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">decryption</span><span class="p">:</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">sops         </span><span class="w"> </span><span class="c"># the only provider</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">sops-age       </span><span class="w"> </span><span class="c"># a Secret in this namespace holding the age key</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">stages</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">app</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">payments-config  </span><span class="w"> </span><span class="c"># contains an encrypted secret.yaml</span><span class="w">
</span></span></span></code></pre></div><h2 id="walkthrough--age">Walkthrough — age</h2>
<p><a href="https://age-encryption.org/">age</a> is the simplest key type and needs no external
service. Take a <code>Secret</code> from plaintext to a GitOps-safe rollout in four steps.</p>
<p><strong>1. Generate an age key.</strong> The file holds the private key; the printed <code>age1…</code>
line is the public recipient to encrypt to.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">age-keygen -o age.agekey
</span></span><span class="line"><span class="cl"><span class="c1"># public key: age1qz…</span>
</span></span></code></pre></div><p><strong>2. Encrypt a Secret.</strong> Encrypt only its values, so the file stays a valid
Kubernetes object, then commit <code>secret.enc.yaml</code> (never the plaintext):</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"># secret.yaml</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">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">Secret</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">payments-db</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">payments</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">stringData</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">password</span><span class="p">:</span><span class="w"> </span><span class="l">s3cr3t-do-not-commit-plaintext</span><span class="w">
</span></span></span></code></pre></div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">sops --encrypt --age age1qz… <span class="se">\
</span></span></span><span class="line"><span class="cl">  --encrypted-regex <span class="s1">&#39;^(data|stringData)$&#39;</span> <span class="se">\
</span></span></span><span class="line"><span class="cl">  secret.yaml &gt; secret.enc.yaml
</span></span></code></pre></div><p><strong>3. Put the private key in the cluster</strong> under a <code>.agekey</code> data entry. Store
<code>age.agekey</code> itself somewhere safe — it is the only thing that can decrypt the
Secret.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">kubectl create secret generic sops-age <span class="se">\
</span></span></span><span class="line"><span class="cl">  --namespace payments <span class="se">\
</span></span></span><span class="line"><span class="cl">  --from-file<span class="o">=</span>keys.agekey<span class="o">=</span>age.agekey
</span></span></code></pre></div><p><strong>4. Decrypt on rollout.</strong> Point a <code>StageSet</code> at the source holding
<code>secret.enc.yaml</code> and set <code>spec.decryption</code> (as in the example above). On reconcile
the controller fetches the source, decrypts every SOPS file in memory, builds, and
applies — so the cluster holds the plaintext <code>payments-db</code> Secret while Git only
ever held ciphertext. Grant the deployer ServiceAccount read access to the key
Secret (see <a href="#how-keys-are-read--tenancy">tenancy</a> below).</p>
<h2 id="pairing-with-jaas-rendered-manifests">Pairing with JaaS-rendered manifests</h2>
<p>A realistic app renders its config from Jsonnet with
<a href="https://jaas.projects.metio.wtf/">JaaS</a> and keeps only its Secret encrypted. The
two compose cleanly because each owns one concern:</p>
<ul>
<li><strong>JaaS renders the non-secret manifests.</strong> It evaluates Jsonnet server-side and
cannot hold secret values: SOPS ciphertext carries a MAC over the whole encrypted
document, so it can&rsquo;t be authored in Jsonnet — and routing plaintext secrets
through a render service is what you are avoiding.</li>
<li><strong>The Secret stays SOPS-encrypted in Git</strong>, as in the walkthrough.</li>
<li><strong>The controller decrypts and orders both</strong> under one <code>spec.decryption</code>:</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">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">payments-deployer</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">decryption</span><span class="p">:</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">sops</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">sops-age</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">stages</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">secrets                </span><span class="w"> </span><span class="c"># decrypt + apply the SOPS Secret first</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">payments-secrets</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">app                    </span><span class="w"> </span><span class="c"># then the JaaS-rendered app that mounts it</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">payments-app</span><span class="w">
</span></span></span></code></pre></div><p>The <code>secrets</code> stage runs first; only once the <code>Secret</code> is applied does the <code>app</code>
stage roll out the rendered manifests that mount it. The encrypted Secret and the
rendered config live in separate sources, so the Jsonnet author never touches secret
material.</p>
<h2 id="the-fields">The fields</h2>
<ul>
<li><strong><code>provider</code></strong> — the decryption backend. Only <code>sops</code> is supported.</li>
<li><strong><code>secretRef.name</code></strong> — a Secret in the <code>StageSet</code>&rsquo;s namespace holding the keys,
using the SOPS conventions: age private keys under data entries ending in
<code>.agekey</code>, armored PGP private keys under <code>.asc</code>. Optional — omit it for a
<a href="#cloud-kms">cloud-KMS-only</a> setup.</li>
</ul>
<h2 id="how-keys-are-read--tenancy">How keys are read — tenancy</h2>
<p>The key Secret is read in the <code>StageSet</code>&rsquo;s namespace <strong>under its
<code>serviceAccountName</code></strong>, exactly like the manifests it applies. A tenant can only
decrypt with key material its own ServiceAccount is allowed to read, so a key in one
namespace is never reachable from another. Grant the deployer SA <code>get</code> on the key
Secret:</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">name</span><span class="p">:</span><span class="w"> </span><span class="l">payments-deployer-sops</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">payments</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="s2">&#34;&#34;</span><span class="p">]</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">secrets]</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">resourceNames</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="l">sops-age]</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>In a <a href="/usage/multi-cluster/#single-tenant-cluster-admin">single-tenant cluster-admin</a>
install (no <code>serviceAccountName</code>), the controller reads the key Secret under its
own identity instead.</p>
<h2 id="decryption-and-the-rollback-store">Decryption and the rollback store</h2>
<p>Decrypted bytes exist only in memory on the apply path. The one place rendered
output is persisted is the optional <a href="/usage/rollback/">rollback store</a>, which is
<strong>encrypted at rest</strong> (S3 SSE by default; an encrypted volume for the file store) —
so a decrypted <code>Secret</code> never lands in plaintext on disk. See
<a href="/usage/rollback/#encryption-at-rest">encryption at rest</a>.</p>
<p>A rollback re-fetches the previous source and <strong>runs decryption again</strong> rather than
restoring plaintext, so the key Secret must still exist when a rollback fires. If
the key was rotated or deleted in the meantime, the rollback <strong>fails closed</strong> with
<code>PreviousRevisionUnavailable</code> instead of applying a stale or unreadable Secret — an
encrypted store cannot avoid this, and it is the safe failure direction.</p>
<h2 id="cloud-kms">Cloud KMS</h2>
<p>SOPS files encrypted with a cloud KMS key (AWS KMS, GCP KMS, Azure Key Vault, or
HashiCorp Vault) decrypt through the <strong>controller&rsquo;s ambient credentials</strong> — e.g. an
IRSA role on EKS, wired via <code>serviceAccount.annotations</code>. No in-cluster key Secret
is needed, so <code>secretRef</code> may be omitted for a KMS-only <code>StageSet</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">decryption</span><span class="p">:</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">sops         </span><span class="w"> </span><span class="c"># secretRef omitted; KMS uses the controller&#39;s identity</span><span class="w">
</span></span></span></code></pre></div><p>One consequence to weigh in a multi-tenant cluster: unlike age (read under the
tenant SA), <strong>cloud KMS uses the controller&rsquo;s identity</strong>, so any <code>StageSet</code> can
decrypt a file encrypted with a KMS key the controller&rsquo;s role can access. This
matches Flux&rsquo;s <code>kustomize-controller</code>. Scope the controller&rsquo;s KMS grant
accordingly, or use age keys for hard per-tenant isolation.</p>
<h2 id="whats-supported">What&rsquo;s supported</h2>
<ul>
<li><strong>age</strong> keys via <code>secretRef</code> — read under the tenant SA. The resource-level
pattern (<code>--encrypted-regex '^(data|stringData)$'</code>) is the tested path.</li>
<li><strong>PGP</strong> keys via <code>secretRef</code> (<code>.asc</code> entries) — read under the tenant SA, pure
Go, no <code>gpg</code> binary or keyring needed. See <a href="#pgp-keys">PGP keys</a>.</li>
<li><strong>Cloud KMS</strong> (AWS/GCP/Azure/Vault) via the controller&rsquo;s ambient credentials.</li>
<li><strong>Encrypted files feeding a <code>secretGenerator</code></strong> — an encrypted <code>.env</code> (or other
file) referenced by a kustomize <code>secretGenerator</code> is decrypted before the build,
so the generated <code>Secret</code> carries the plaintext.</li>
<li>A file with no SOPS metadata passes through untouched, so encrypted and plain
manifests can sit side by side in one source.</li>
</ul>
<h2 id="pgp-keys">PGP keys</h2>
<p>PGP works <strong>tenant-scoped</strong>, like age: put one or more armored private keys in the
<code>secretRef</code> Secret under data entries suffixed <code>.asc</code>. The data key is decrypted in
pure Go (<code>ProtonMail/go-crypto</code>) directly from those keys — <strong>no <code>gpg</code> binary, no
GnuPG keyring, and no <code>GNUPGHOME</code></strong> — and the keys are read under the <code>StageSet</code>&rsquo;s
<code>serviceAccountName</code>, so a tenant can only use material its ServiceAccount can read.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># export the armored private key and load it into the key Secret</span>
</span></span><span class="line"><span class="cl">gpg --export-secret-keys --armor 0xYOURFINGERPRINT &gt; key.asc
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">kubectl create secret generic sops-keys <span class="se">\
</span></span></span><span class="line"><span class="cl">  --namespace payments <span class="se">\
</span></span></span><span class="line"><span class="cl">  --from-file<span class="o">=</span>pgp.asc<span class="o">=</span>key.asc
</span></span></code></pre></div><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">decryption</span><span class="p">:</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">sops</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">sops-keys     </span><span class="w"> </span><span class="c"># holds the *.asc private key(s)</span><span class="w">
</span></span></span></code></pre></div><p>One Secret can carry both age (<code>*.agekey</code>) and PGP (<code>*.asc</code>) keys; the right one is
used per file. For a fresh setup, age is simpler and the recommended default, but an
existing PGP-encrypted repository needs no migration.</p>
]]></content><category scheme="https://stageset.projects.metio.wtf/tags/secrets" term="secrets" label="secrets"/><category scheme="https://stageset.projects.metio.wtf/tags/encryption" term="encryption" label="encryption"/><category scheme="https://stageset.projects.metio.wtf/tags/sops" term="sops" label="sops"/><category scheme="https://stageset.projects.metio.wtf/tags/security" term="security" label="security"/></entry><entry><title type="html">SourceNotReady</title><link href="https://stageset.projects.metio.wtf/runbooks/sourcenotready/?utm_source=atom_feed" rel="alternate" type="text/html"/><link href="https://stageset.projects.metio.wtf/runbooks/artifactnotfound/?utm_source=atom_feed" rel="related" type="text/html" title="ArtifactNotFound"/><link href="https://stageset.projects.metio.wtf/runbooks/resolvefailed/?utm_source=atom_feed" rel="related" type="text/html" title="ResolveFailed"/><link href="https://stageset.projects.metio.wtf/runbooks/controller-pod-down/?utm_source=atom_feed" rel="related" type="text/html" title="Controller pod down"/><link href="https://stageset.projects.metio.wtf/runbooks/dependencynotready/?utm_source=atom_feed" rel="related" type="text/html" title="DependencyNotReady"/><link href="https://stageset.projects.metio.wtf/runbooks/downgraderequiresmigration/?utm_source=atom_feed" rel="related" type="text/html" title="DowngradeRequiresMigration"/><id>https://stageset.projects.metio.wtf/runbooks/sourcenotready/</id><published>0001-01-01T00:00:00+00:00</published><updated>2026-06-16T13:41:17+02:00</updated><content type="html"><![CDATA[<blockquote>The source exists but has not published a ready artifact yet (transient).</blockquote><h2 id="symptom">Symptom</h2>
<p><code>READY=False</code>, <code>REASON=SourceNotReady</code>. Transient: the controller requeues and clears the condition once the source publishes.</p>
<h2 id="cause">Cause</h2>
<p>A stage&rsquo;s <code>sourceRef</code> resolved to an <code>ExternalArtifact</code> (directly, or via a producer&rsquo;s RFC-0012 back-pointer such as a JaaS <code>JsonnetSnippet</code>), but that artifact&rsquo;s <code>status.conditions[Ready]</code> is not yet <code>True</code> — its producer has not finished publishing a revision. The StageSet gates on <code>Ready=True</code> so it never builds against a half-written artifact.</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 artifact, and is it Ready?</span>
</span></span><span class="line"><span class="cl">kubectl get externalartifact -n &lt;namespace&gt;
</span></span><span class="line"><span class="cl">kubectl describe externalartifact &lt;name&gt; -n &lt;namespace&gt;
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># If the producer is a JsonnetSnippet (or other producer kind), check it:</span>
</span></span><span class="line"><span class="cl">kubectl describe jsonnetsnippet &lt;name&gt; -n &lt;namespace&gt;
</span></span></code></pre></div><h2 id="remediation">Remediation</h2>
<p>This usually clears on its own when the producer publishes. If it persists:</p>
<ul>
<li>confirm the producing controller (e.g. the JaaS operator, or <a href="https://fluxcd.io/">Flux</a> <code>source-controller</code>) is running and reconciling the producer object;</li>
<li>check the producer&rsquo;s own Ready condition for an upstream error (a failed render, an unreachable <code>GitRepository</code>/<code>OCIRepository</code> source);</li>
<li>once the producer reports <code>Ready=True</code> with a <code>status.artifact</code>, the StageSet converges on the next reconcile.</li>
</ul>
<p>If the artifact never appears at all, the reason is <a href="/runbooks/artifactnotfound/"><code>ArtifactNotFound</code></a>; a spec/API resolution failure is <a href="/runbooks/resolvefailed/"><code>ResolveFailed</code></a>. See <a href="/usage/stages-and-sources/">stages and sources</a>.</p>
]]></content><category scheme="https://stageset.projects.metio.wtf/tags/runbooks" term="runbooks" label="runbooks"/><category scheme="https://stageset.projects.metio.wtf/tags/sources" term="sources" label="sources"/><category scheme="https://stageset.projects.metio.wtf/tags/troubleshooting" term="troubleshooting" label="troubleshooting"/></entry><entry><title type="html">Stage sources — Git, OCI, Bucket</title><link href="https://stageset.projects.metio.wtf/tutorials/flux-sources/?utm_source=atom_feed" rel="alternate" type="text/html"/><link href="https://stageset.projects.metio.wtf/usage/stages-and-sources/?utm_source=atom_feed" rel="related" type="text/html" title="Stages and sources"/><link href="https://stageset.projects.metio.wtf/runbooks/artifactnotfound/?utm_source=atom_feed" rel="related" type="text/html" title="ArtifactNotFound"/><link href="https://stageset.projects.metio.wtf/tutorials/jsonnet-to-rollout/?utm_source=atom_feed" rel="related" type="text/html" title="From Jsonnet to a gated rollout"/><link href="https://stageset.projects.metio.wtf/tutorials/parameters/?utm_source=atom_feed" rel="related" type="text/html" title="Parameterizing a rollout"/><link href="https://stageset.projects.metio.wtf/usage/producer-aware-sources/?utm_source=atom_feed" rel="related" type="text/html" title="Producer-aware sources"/><id>https://stageset.projects.metio.wtf/tutorials/flux-sources/</id><published>0001-01-01T00:00:00+00:00</published><updated>2026-06-16T13:41:17+02:00</updated><content type="html"><![CDATA[<blockquote>Point a stage straight at a Git/OCI/Bucket source, or render manifests first into an ExternalArtifact.</blockquote><p>A stage resolves its <code>sourceRef</code> to a <a href="https://fluxcd.io/">Flux</a> artifact. You have
two routes:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">manifests in Git / OCI / Bucket  ──────────────────────────►  StageSet   (direct)
</span></span><span class="line"><span class="cl">manifests in Git / OCI / Bucket  ──►  a renderer (JaaS)  ──►  ExternalArtifact  ──►  StageSet
</span></span></code></pre></div><p>Use the <strong>direct</strong> route when the source already holds ready-to-apply manifests
(the same thing Flux&rsquo;s <code>kustomize-controller</code> consumes). Use the <strong>renderer</strong> route
when you generate manifests first — e.g. evaluating Jsonnet with
<a href="https://jaas.projects.metio.wtf/">JaaS</a>.</p>
<p>This page is the copy-pasteable recipe per source kind. For how <code>sourceRef</code>
resolution works as a concept — and the <code>path</code>, <code>prune</code>, <code>patches</code>, and
<code>postBuild</code> knobs that shape a stage — see
<a href="/usage/stages-and-sources/">stages and sources</a>.</p>
<h2 id="direct-git">Direct: Git</h2>
<p>Point a stage straight at a <code>GitRepository</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">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">web-manifests</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">apps</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">1m</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/acme/web-manifests</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">stages.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">StageSet</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">web</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">apps</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">stages</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">web</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">web-manifests</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">./manifests       </span><span class="w"> </span><span class="c"># build a sub-path of the repo</span><span class="w">
</span></span></span></code></pre></div><h2 id="direct-oci">Direct: OCI</h2>
<p>Manifests pushed as an OCI artifact (e.g. with <code>flux push artifact</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">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">web-manifests</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">apps</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/acme/web-manifests</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="s2">&#34;2.1.0&#34;</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">stages.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">StageSet</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">web</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">apps</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">stages</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">web</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">web-manifests</span><span class="w">
</span></span></span></code></pre></div><h2 id="direct-bucket">Direct: Bucket</h2>
<p>Object storage works the same way:</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">web-manifests</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">apps</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">manifests</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">minio.storage.svc:9000</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">minio-credentials</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">stages.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">StageSet</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">web</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">apps</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">stages</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">web</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">web-manifests</span><span class="w">
</span></span></span></code></pre></div><h2 id="via-a-renderer-jaas">Via a renderer (JaaS)</h2>
<p>When the source holds <em>Jsonnet</em> rather than plain manifests, render it first.
<a href="https://jaas.projects.metio.wtf/">JaaS</a> reads a Flux source with a <code>JsonnetSnippet</code>
and publishes the rendered result as an <code>ExternalArtifact</code>, which the stage then
consumes:</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">web</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">apps</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">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">web-manifests</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">./jsonnet</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="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">stages.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">StageSet</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">web</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">apps</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">stages</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">web</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 class="c"># resolve the producer to its ExternalArtifact</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">web</span><span class="w">
</span></span></span></code></pre></div><p>Shared libraries arrive the same way:
<a href="https://github.com/metio/jsonnet-oci-images">JOI</a> publishes Jsonnet libraries as
single-layer OCI images, surfaced as <code>OCIRepository</code> + <code>JsonnetLibrary</code> pairs a
snippet imports:</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">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">k8s-libsonnet</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">k8s         </span><span class="w"> </span><span class="c"># import &#39;k8s/...&#39; in your Jsonnet</span><span class="w">
</span></span></span></code></pre></div><p>For small or generated snippets, skip the external source and inline the Jsonnet on
the <code>JsonnetSnippet</code> (<code>spec.files</code>). The end-to-end render-and-roll-out flow is in
<a href="/tutorials/jsonnet-to-rollout/">From Jsonnet to a gated rollout</a>.</p>
]]></content><category scheme="https://stageset.projects.metio.wtf/tags/tutorials" term="tutorials" label="tutorials"/><category scheme="https://stageset.projects.metio.wtf/tags/sources" term="sources" label="sources"/><category scheme="https://stageset.projects.metio.wtf/tags/flux" term="flux" label="flux"/><category scheme="https://stageset.projects.metio.wtf/tags/git" term="git" label="git"/></entry><entry><title type="html">StageFailed</title><link href="https://stageset.projects.metio.wtf/runbooks/stagefailed/?utm_source=atom_feed" rel="alternate" type="text/html"/><link href="https://stageset.projects.metio.wtf/runbooks/dependencynotready/?utm_source=atom_feed" rel="related" type="text/html" title="DependencyNotReady"/><link href="https://stageset.projects.metio.wtf/runbooks/stalled/?utm_source=atom_feed" rel="related" type="text/html" title="Stalled"/><link href="https://stageset.projects.metio.wtf/usage/actions/?utm_source=atom_feed" rel="related" type="text/html" title="Actions"/><link href="https://stageset.projects.metio.wtf/runbooks/artifactnotfound/?utm_source=atom_feed" rel="related" type="text/html" title="ArtifactNotFound"/><link href="https://stageset.projects.metio.wtf/runbooks/controller-pod-down/?utm_source=atom_feed" rel="related" type="text/html" title="Controller pod down"/><id>https://stageset.projects.metio.wtf/runbooks/stagefailed/</id><published>0001-01-01T00:00:00+00:00</published><updated>2026-06-16T13:41:17+02:00</updated><content type="html"><![CDATA[<blockquote>A stage failed to fetch, build, apply, verify, or run an action; the run halts there.</blockquote><h2 id="symptom">Symptom</h2>
<p><code>READY=False</code>, <code>REASON=StageFailed</code>. The Message names the stage and the operation that failed (<code>fetch artifact</code>, <code>build</code>, <code>apply</code>, <code>verify</code>, a pre/post action, or <code>connect to target cluster</code>). The run halts at that stage; later stages keep their previous revisions.</p>
<h2 id="cause">Cause</h2>
<p>A stage failed during execution. By operation:</p>
<ul>
<li><strong>fetch artifact</strong> — the artifact URL was unreachable, or its bytes failed digest verification.</li>
<li><strong>build</strong> — kustomize build or post-build substitution failed (a missing <code>substituteFrom</code> source, an invalid patch, a malformed manifest).</li>
<li><strong>apply</strong> — the server-side apply was rejected: an immutable-field conflict, or an <strong>RBAC denial</strong> under the impersonated <code>serviceAccountName</code>.</li>
<li><strong>verify</strong> — applied objects did not become Ready within the stage timeout (kstatus).</li>
<li><strong>pre/post action</strong> — a <code>patch</code>/<code>http</code>/<code>wait</code>/<code>job</code>/<code>delete</code>/<code>apply</code> action failed or timed out.</li>
<li><strong>connect to target cluster</strong> — a <code>spec.kubeConfig</code> Secret was missing, unparseable, or used the unsupported cloud-provider <code>configMapRef</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">kubectl describe stageset &lt;name&gt; -n &lt;namespace&gt;     <span class="c1"># Message: which stage + operation</span>
</span></span><span class="line"><span class="cl">kubectl -n stageset-system logs deploy/stageset-controller --tail<span class="o">=</span><span class="m">200</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># For apply/verify failures, inspect what the stage tried to apply:</span>
</span></span><span class="line"><span class="cl">kubectl get stageinventory -n &lt;namespace&gt; <span class="se">\
</span></span></span><span class="line"><span class="cl">  -l stages.metio.wtf/stage-set<span class="o">=</span>&lt;name&gt;,stages.metio.wtf/stage<span class="o">=</span>&lt;stage&gt;
</span></span></code></pre></div><h2 id="remediation">Remediation</h2>
<p>Match the operation in the Message:</p>
<ul>
<li><strong>fetch / digest</strong> — confirm the producer republished cleanly; a digest mismatch means the artifact changed mid-flight or is corrupt.</li>
<li><strong>build</strong> — validate the manifests/patches locally; ensure every <code>substituteFrom</code> ConfigMap/Secret exists.</li>
<li><strong>apply RBAC</strong> — grant the impersonated <code>serviceAccountName</code> (or the controller) the verbs it was denied; the Message names the resource.</li>
<li><strong>apply immutable conflict</strong> — set a per-stage <a href="/usage/conflict-policies/"><code>conflictPolicy</code></a> (or <code>force: true</code>, its blunt <code>Recreate</code>-everything form) so the controller deletes and recreates the conflicting object; for objects holding data (<code>PersistentVolumeClaim</code>/<code>PersistentVolume</code>) a <code>Recreate</code> rule additionally requires <code>allowDataLoss: true</code>. Alternatively, use content-hash-suffixed names so a change is a new object rather than a mutation.</li>
<li><strong>verify timeout</strong> — raise the stage <code>timeout</code>, or fix why the workload is not becoming Ready.</li>
<li><strong>action</strong> — read the action&rsquo;s error; for <code>http</code>, confirm the host is in <code>--allowed-action-hosts</code>.</li>
</ul>
<p>Retries re-run the same pinned snapshot idempotently — actions already recorded in the stage&rsquo;s ledger do not re-fire. See <a href="/usage/stages-and-sources/">stages and sources</a> for how a stage resolves and applies.</p>
]]></content><category scheme="https://stageset.projects.metio.wtf/tags/runbooks" term="runbooks" label="runbooks"/><category scheme="https://stageset.projects.metio.wtf/tags/stages" term="stages" label="stages"/><category scheme="https://stageset.projects.metio.wtf/tags/actions" term="actions" label="actions"/><category scheme="https://stageset.projects.metio.wtf/tags/troubleshooting" term="troubleshooting" label="troubleshooting"/></entry><entry><title type="html">StageInventory</title><link href="https://stageset.projects.metio.wtf/api/stageinventory/?utm_source=atom_feed" rel="alternate" type="text/html"/><link href="https://stageset.projects.metio.wtf/api/stageset/?utm_source=atom_feed" rel="related" type="text/html" title="StageSet"/><link href="https://stageset.projects.metio.wtf/runbooks/dependencynotready/?utm_source=atom_feed" rel="related" type="text/html" title="DependencyNotReady"/><link href="https://stageset.projects.metio.wtf/runbooks/invalidspec/?utm_source=atom_feed" rel="related" type="text/html" title="InvalidSpec"/><link href="https://stageset.projects.metio.wtf/runbooks/stagefailed/?utm_source=atom_feed" rel="related" type="text/html" title="StageFailed"/><link href="https://stageset.projects.metio.wtf/runbooks/stalled/?utm_source=atom_feed" rel="related" type="text/html" title="Stalled"/><id>https://stageset.projects.metio.wtf/api/stageinventory/</id><published>0001-01-01T00:00:00+00:00</published><updated>2026-06-16T13:41:17+02:00</updated><content type="html"><![CDATA[<blockquote>The controller-managed inventory of objects each stage applied.</blockquote><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">stages.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">StageInventory</span><span class="w">
</span></span></span></code></pre></div><p>A <code>StageInventory</code> records the set of objects a single stage has applied, so the
controller can prune precisely and tear stages down in reverse order. <strong>You do not
author these</strong> — the controller creates, updates, and deletes them. They are
documented here so you can read them when debugging and back them up.</p>
<p>One stage may be backed by several <code>StageInventory</code> shards once it exceeds
<code>--inventory-shard-cap</code> entries (default 5000). Shard <code>0</code> doubles as the ApplySet
(<a href="https://github.com/kubernetes/enhancements/tree/master/keps/sig-cli/3659-kubectl-applyset">KEP-3659</a>)
parent object for the stage.</p>
<h2 id="spec">spec</h2>
<p>A <code>StageInventory</code> as the controller writes it (read-only — never hand-author one):</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">stages.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">StageInventory</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">payments-application-0         </span><span class="w"> </span><span class="c"># &lt;stageset&gt;-&lt;stage&gt;-&lt;shard&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">payments</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">stages.metio.wtf/stage-set</span><span class="p">:</span><span class="w"> </span><span class="l">payments</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">stages.metio.wtf/stage</span><span class="p">:</span><span class="w"> </span><span class="l">application</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">stages.metio.wtf/shard</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;0&#34;</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">stagePosition</span><span class="p">:</span><span class="w"> </span><span class="m">1</span><span class="w">                       </span><span class="c"># the stage&#39;s index in spec.stages</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">entries</span><span class="p">:</span><span class="w">                               </span><span class="c"># identifiers only — never object contents</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span>- <span class="nt">id</span><span class="p">:</span><span class="w"> </span><span class="l">payments_web_apps_Deployment  </span><span class="w"> </span><span class="c"># namespace_name_group_kind</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">v</span><span class="p">:</span><span class="w"> </span><span class="l">apps/v1                        </span><span class="w"> </span><span class="c"># the applied API version</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span>- <span class="nt">id</span><span class="p">:</span><span class="w"> </span><span class="l">payments_web__Service         </span><span class="w"> </span><span class="c"># empty group → core/v1</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">v</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 inventory is stored in <code>spec</code> (not <code>status</code>) on purpose: backup tooling that
restores <code>spec</code> preserves the prune history, so a restored controller does not
orphan or wrongly prune objects.</p>
<table>
	<thead>
			<tr>
					<th>Field</th>
					<th>Meaning</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td><code>stagePosition</code></td>
					<td>The stage&rsquo;s index in <code>spec.stages</code> at write time. Teardown walks inventories in reverse position order.</td>
			</tr>
			<tr>
					<td><code>entries[].id</code></td>
					<td>An applied object&rsquo;s identifier, form <code>namespace_name_group_kind</code> (empty group for core).</td>
			</tr>
			<tr>
					<td><code>entries[].v</code></td>
					<td>The API version the object was applied at.</td>
			</tr>
	</tbody>
</table>
<h2 id="well-known-labels-and-annotations">Well-known labels and annotations</h2>
<p>The controller stamps these onto inventories and managed objects:</p>
<table>
	<thead>
			<tr>
					<th>Key</th>
					<th>On</th>
					<th>Meaning</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td><code>stages.metio.wtf/stage-set</code></td>
					<td>inventory</td>
					<td>Owning StageSet name.</td>
			</tr>
			<tr>
					<td><code>stages.metio.wtf/stage</code></td>
					<td>inventory</td>
					<td>Stage name.</td>
			</tr>
			<tr>
					<td><code>stages.metio.wtf/shard</code></td>
					<td>inventory</td>
					<td>Shard index.</td>
			</tr>
			<tr>
					<td><code>stages.metio.wtf/prune</code></td>
					<td>managed object</td>
					<td>Set to <code>disabled</code> to opt an object out of pruning.</td>
			</tr>
	</tbody>
</table>
<p>Other annotations the controller honors on a StageSet or its objects. Each has a
<a href="/cli/reconcile/"><code>stagesetctl reconcile</code></a> equivalent:</p>
<ul>
<li><code>reconcile.fluxcd.io/requestedAt</code> — request an out-of-band reconcile.</li>
<li><code>stages.metio.wtf/reconcile-stage</code> — force a single stage to re-run.</li>
<li><code>stages.metio.wtf/update-now</code> — push a window-held rollout through immediately.</li>
</ul>
<h2 id="inventory-modes">Inventory modes</h2>
<p><code>--inventory-mode</code> selects how applied state is tracked:</p>
<ul>
<li><code>entries</code> — identifiers recorded in <code>StageInventory</code> only.</li>
<li><code>hybrid</code> (default) — identifiers plus ApplySet labels for tooling
compatibility.</li>
<li><code>applyset</code> — ApplySet-native.</li>
</ul>
<p>The mode satisfied by the stored inventory is surfaced on
<code>StageSet.status.inventoryMode</code>.</p>
]]></content><category scheme="https://stageset.projects.metio.wtf/tags/api" term="api" label="api"/><category scheme="https://stageset.projects.metio.wtf/tags/stages" term="stages" label="stages"/><category scheme="https://stageset.projects.metio.wtf/tags/crd" term="crd" label="crd"/><category scheme="https://stageset.projects.metio.wtf/tags/troubleshooting" term="troubleshooting" label="troubleshooting"/></entry><entry><title type="html">Stages and sources</title><link href="https://stageset.projects.metio.wtf/usage/stages-and-sources/?utm_source=atom_feed" rel="alternate" type="text/html"/><link href="https://stageset.projects.metio.wtf/usage/producer-aware-sources/?utm_source=atom_feed" rel="related" type="text/html" title="Producer-aware sources"/><link href="https://stageset.projects.metio.wtf/runbooks/artifactnotfound/?utm_source=atom_feed" rel="related" type="text/html" title="ArtifactNotFound"/><link href="https://stageset.projects.metio.wtf/runbooks/resolvefailed/?utm_source=atom_feed" rel="related" type="text/html" title="ResolveFailed"/><link href="https://stageset.projects.metio.wtf/tutorials/flux-sources/?utm_source=atom_feed" rel="related" type="text/html" title="Stage sources — Git, OCI, Bucket"/><link href="https://stageset.projects.metio.wtf/api/stageset/?utm_source=atom_feed" rel="related" type="text/html" title="StageSet"/><id>https://stageset.projects.metio.wtf/usage/stages-and-sources/</id><published>0001-01-01T00:00:00+00:00</published><updated>2026-06-16T13:41:17+02:00</updated><content type="html"><![CDATA[<blockquote>The core — ordered stages applying Flux sources, with path, prune, patches, and substitution.</blockquote><p>A <code>StageSet</code> is an ordered list of stages. Each stage resolves a
<a href="https://fluxcd.io/">Flux</a> source — a <code>GitRepository</code>, <code>OCIRepository</code>, <code>Bucket</code>,
or an <code>ExternalArtifact</code> (the default) — applies its manifests, waits for them to
become healthy, and only then lets the next stage start.</p>
<h2 id="one-stage">One stage</h2>
<p>The minimum is one stage pointing at one artifact in the same 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">stages.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">StageSet</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-app</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">stages</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">app</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">name</span><span class="p">:</span><span class="w"> </span><span class="l">my-app         </span><span class="w"> </span><span class="c"># an ExternalArtifact</span><span class="w">
</span></span></span></code></pre></div><p><code>sourceRef.kind</code> defaults to <code>ExternalArtifact</code>, so the common case is a single
line. The controller fetches the artifact, applies every manifest in it, and marks
the stage <code>Ready</code> once the applied objects report healthy.</p>
<h2 id="source-kinds">Source kinds</h2>
<p>A <code>sourceRef</code> resolves to a Flux artifact three ways. Point it at whichever you
already have:</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"># 1. an ExternalArtifact (the default — kind omitted)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><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">name</span><span class="p">:</span><span class="w"> </span><span class="l">my-app</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"># 2. a classic Flux source, consumed directly</span><span class="w">
</span></span></span><span class="line"><span class="cl"><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">my-app-manifests</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"># 3. a producer that publishes an ExternalArtifact (resolved via its back-pointer)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><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">my-app</span><span class="w">
</span></span></span></code></pre></div><p><code>GitRepository</code>, <code>OCIRepository</code>, and <code>Bucket</code> carry the same <code>status.artifact</code>
contract as <code>ExternalArtifact</code>, so the controller reads them directly — no producer
in between. A stage can apply manifests straight from a Git repo or an OCI artifact,
like Flux&rsquo;s own <code>kustomize-controller</code>. For the producer case (for example
rendering Jsonnet with <a href="https://jaas.projects.metio.wtf/">JaaS</a>), see
<a href="/usage/producer-aware-sources/">producer-aware sources</a>.</p>
<h2 id="ordered-stages">Ordered stages</h2>
<p>Add more stages and they run top to bottom — each one waits for the previous to be
<code>Ready</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">stages</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">crds         </span><span class="w"> </span><span class="c"># 1 ── install the CRDs first</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">name</span><span class="p">:</span><span class="w"> </span><span class="l">platform-crds</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">operator     </span><span class="w"> </span><span class="c"># 2 ── then the operator that needs them</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">name</span><span class="p">:</span><span class="w"> </span><span class="l">platform-operator</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">workloads    </span><span class="w"> </span><span class="c"># 3 ── then the workloads it manages</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">name</span><span class="p">:</span><span class="w"> </span><span class="l">team-workloads</span><span class="w">
</span></span></span></code></pre></div><p>This is the core of a <code>StageSet</code>: <code>operator</code> is never applied until <code>crds</code> is
healthy, so the operator never crash-loops waiting for a CRD that isn&rsquo;t there yet.</p>
<h2 id="shaping-a-stages-manifests">Shaping a stage&rsquo;s manifests</h2>
<p>A stage can build from a sub-path of the artifact, customize with patches, and
substitute variables — the <a href="https://kubectl.docs.kubernetes.io/">kustomize</a>-style
surface:</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">stages</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">app</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">name</span><span class="p">:</span><span class="w"> </span><span class="l">my-app</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">./overlays/production     </span><span class="w"> </span><span class="c"># build a sub-path of the artifact</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">prune</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="w">                      </span><span class="c"># GC objects that leave this stage (default)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">patches</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span>- <span class="nt">patch</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">            - op: replace
</span></span></span><span class="line"><span class="cl"><span class="sd">              path: /spec/replicas
</span></span></span><span class="line"><span class="cl"><span class="sd">              value: 6</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">kind</span><span class="p">:</span><span class="w"> </span><span class="l">Deployment</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">web</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">postBuild</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">substitute</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">cluster_name</span><span class="p">:</span><span class="w"> </span><span class="l">prod-eu</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">substituteFrom</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">ConfigMap</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-vars</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">Secret</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-secrets</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span><span class="nt">optional</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="w">
</span></span></span></code></pre></div><ul>
<li><strong><code>path</code></strong> builds from a directory inside the artifact (default <code>./</code>).</li>
<li><strong><code>prune</code></strong> (default <code>true</code>) garbage-collects objects that fall out of the stage
between reconciles, tracked precisely via the stage&rsquo;s
<a href="/api/stageinventory/"><code>StageInventory</code></a>.</li>
<li><strong><code>patches</code></strong> are strategic-merge or JSON6902 patches applied after the build.</li>
<li><strong><code>postBuild</code></strong> substitutes <code>${var}</code> references from inline values, ConfigMaps,
and Secrets at delivery time — see <a href="/tutorials/parameters/">parameterizing a rollout</a>
for the full render-time-vs-delivery-time treatment.</li>
</ul>
<p>From here, layer on <a href="/usage/actions/">actions</a> to gate the stage, or
<a href="/usage/ready-checks/">ready checks</a> to define what &ldquo;healthy&rdquo; means.</p>
]]></content><category scheme="https://stageset.projects.metio.wtf/tags/stages" term="stages" label="stages"/><category scheme="https://stageset.projects.metio.wtf/tags/sources" term="sources" label="sources"/><category scheme="https://stageset.projects.metio.wtf/tags/flux" term="flux" label="flux"/><category scheme="https://stageset.projects.metio.wtf/tags/externalartifact" term="externalartifact" label="externalartifact"/></entry><entry><title type="html">StageSet</title><link href="https://stageset.projects.metio.wtf/api/stageset/?utm_source=atom_feed" rel="alternate" type="text/html"/><link href="https://stageset.projects.metio.wtf/api/stageinventory/?utm_source=atom_feed" rel="related" type="text/html" title="StageInventory"/><link href="https://stageset.projects.metio.wtf/usage/producer-aware-sources/?utm_source=atom_feed" rel="related" type="text/html" title="Producer-aware sources"/><link href="https://stageset.projects.metio.wtf/usage/stages-and-sources/?utm_source=atom_feed" rel="related" type="text/html" title="Stages and sources"/><link href="https://stageset.projects.metio.wtf/cli/build/?utm_source=atom_feed" rel="related" type="text/html" title="stagesetctl build"/><link href="https://stageset.projects.metio.wtf/usage/actions/?utm_source=atom_feed" rel="related" type="text/html" title="Actions"/><id>https://stageset.projects.metio.wtf/api/stageset/</id><published>0001-01-01T00:00:00+00:00</published><updated>2026-06-16T13:41:17+02:00</updated><content type="html"><![CDATA[<blockquote>Every field of the StageSet resource — the one you author.</blockquote><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">stages.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">StageSet</span><span class="w">
</span></span></span></code></pre></div><p>A <code>StageSet</code> is a namespaced <a href="https://kubernetes.io/docs/">Kubernetes</a> resource
describing an ordered set of stages. Only <code>spec.stages</code> is required; everything else
refines scheduling, security, gating, versioning, and rollback. Every field below is
shown in YAML at least once.</p>
<p>The smallest valid StageSet:</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">stages.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">StageSet</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-app</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">stages</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">app</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">name</span><span class="p">:</span><span class="w"> </span><span class="l">my-app</span><span class="w">
</span></span></span></code></pre></div><hr>
<h2 id="scheduling">Scheduling</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">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: 5m                  # optional: reconcile cadence (default</span><span class="p">:</span><span class="w"> </span>--<span class="l">default-interval)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">retryInterval: 1m             # cadence after a failed run (default</span><span class="p">:</span><span class="w"> </span><span class="l">interval)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">driftDetectionInterval</span><span class="p">:</span><span class="w"> </span><span class="l">2m   </span><span class="w"> </span><span class="c"># faster drift correction than interval (optional)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">timeout</span><span class="p">:</span><span class="w"> </span><span class="l">5m                  </span><span class="w"> </span><span class="c"># default per-stage timeout (optional)</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 class="c"># pause reconciliation without deleting (default false)</span><span class="w">
</span></span></span></code></pre></div><ul>
<li><strong><code>interval</code></strong> (optional) — steady-state reconcile cadence; each reconcile
re-resolves sources, re-asserts desired state (correcting drift), and prunes.
<strong>When omitted, the controller&rsquo;s <code>--default-interval</code> is used</strong> (the chart&rsquo;s
<code>controller.defaultInterval</code>, default <code>10m</code>), so most StageSets can leave it out.</li>
<li><strong><code>retryInterval</code></strong> — retry cadence after a failure; falls back to <code>interval</code>.</li>
<li><strong><code>driftDetectionInterval</code></strong> — a shorter cadence dedicated to healing out-of-band
drift when you need it tighter than <code>interval</code>.</li>
<li><strong><code>timeout</code></strong> — how long any one stage may take before it fails; override per
stage with <code>stages[].timeout</code>.</li>
<li><strong><code>suspend</code></strong> — short-circuits to <code>Ready=False / Suspended</code>, leaving applied state
running. Use <a href="/cli/reconcile/"><code>stagesetctl reconcile --force</code></a> to run once while
suspended. See the <a href="/runbooks/suspended/"><code>Suspended</code> runbook</a>.</li>
</ul>
<h2 id="ordering-between-stagesets">Ordering between StageSets</h2>
<p><code>dependsOn</code> gates this StageSet on others being Ready at their observed generation
— cross-release ordering. (Ordering <em>within</em> a StageSet is the order of <code>stages</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">dependsOn</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">platform</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">platform-system</span><span class="w">
</span></span></span></code></pre></div><h2 id="security-and-targeting">Security and targeting</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">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">payments-deployer  </span><span class="w"> </span><span class="c"># impersonated for every cluster operation</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">kubeConfig</span><span class="p">:</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">prod-eu-kubeconfig           </span><span class="w"> </span><span class="c"># apply to a remote cluster</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">decryption</span><span class="p">:</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">sops                       </span><span class="w"> </span><span class="c"># decrypt SOPS files in stage sources</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">sops-age                     </span><span class="w"> </span><span class="c"># holds an age key under *.agekey</span><span class="w">
</span></span></span></code></pre></div><ul>
<li><strong><code>serviceAccountName</code></strong> — the ServiceAccount the controller impersonates; the
StageSet can do exactly what its RBAC allows. See
<a href="/usage/multi-cluster/">multi-cluster and tenancy</a>.</li>
<li><strong><code>kubeConfig.secretRef</code></strong> — a Secret holding a kubeconfig for a remote cluster.
Only <code>secretRef</code> is accepted.</li>
<li><strong><code>decryption</code></strong> — decrypt SOPS-encrypted files (<code>age</code>) in every stage&rsquo;s source
before they are built. <code>provider</code> is <code>sops</code>; <code>secretRef</code> names the key Secret,
read under <code>serviceAccountName</code>. See <a href="/usage/encryption/">secrets encryption</a>.</li>
</ul>
<h2 id="versioning-and-migrations">Versioning and migrations</h2>
<p>Versioning is off unless <code>spec.version</code> is set. Set <strong>exactly one</strong> of
<code>value</code> / <code>fromObject</code> / <code>fromArtifact</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">version</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="c"># fromObject reads the version from a rendered object — by default the</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="c"># app.kubernetes.io/version label, so it travels in the manifests (works for</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="c"># every source kind, including JaaS). The recommended default.</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">fromObject</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">stage</span><span class="p">:</span><span class="w"> </span><span class="l">app</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">Deployment</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">web</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="c"># apiVersion: apps/v1            # optional; narrows an ambiguous Kind+Name</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="c"># fieldPath: &#34;{.data.version}&#34;   # optional JSONPath; defaults to the version label</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="c"># value: &#34;2.1.0&#34;                   # …or pin it inline</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="c"># fromArtifact: { stage: app, path: VERSION }   # …or read a VERSION file (Git/OCI/Bucket)</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">migrations</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">backfill-ledger-2-0</span><span class="w"> </span><span class="c"># idempotency-ledger / Events name</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="s2">&#34;1.*&#34;</span><span class="w">               </span><span class="c"># optional: constrain the version it applies from</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 class="s2">&#34;2.0.0&#34;</span><span class="w">             </span><span class="c"># required: the boundary this migration crosses</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">stage</span><span class="p">:</span><span class="w"> </span><span class="l">app               </span><span class="w"> </span><span class="c"># runs before this stage&#39;s pre-actions</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">actions</span><span class="p">:</span><span class="w">                  </span><span class="c"># the same Action shape used by stages (see below)</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">backfill</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">job</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">name</span><span class="p">:</span><span class="w"> </span><span class="l">ledger-backfill-job</span><span class="w">
</span></span></span></code></pre></div><p>See <a href="/usage/versioned-migrations/">versioned migrations</a>.</p>
<h2 id="rollback">Rollback</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">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">rollbackOnFailure</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="w">       </span><span class="c"># restore last-good revisions on a failed run</span><span class="w">
</span></span></span></code></pre></div><p>Needs a rollback store configured; see <a href="/usage/rollback/">rollback</a>.</p>
<h2 id="update-windows">Update windows</h2>
<p>Gate <em>when</em> new revisions roll out. Each window is <code>Allow</code> or <code>Deny</code>, recurring
(cron) <strong>or</strong> absolute (from/to). <code>windowScope</code> controls how strict a closed window
is.</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">windowScope: Updates          # Updates (default)</span><span class="p">:</span><span class="w"> </span><span class="l">hold rollouts, keep correcting</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">                                </span><span class="c"># drift. All: a hard freeze — no applies at all.</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">updateWindows</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">Deny               </span><span class="w"> </span><span class="c"># Deny always wins over Allow</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">schedule</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;0 9 * * MON-FRI&#34;</span><span class="w">   </span><span class="c"># 5-field cron: window start</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">duration</span><span class="p">:</span><span class="w"> </span><span class="l">8h</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">timeZone</span><span class="p">:</span><span class="w"> </span><span class="l">Europe/Berlin  </span><span class="w"> </span><span class="c"># IANA tz (default UTC)</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">Deny               </span><span class="w"> </span><span class="c"># an absolute one-off freeze</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="ld">2026-12-24T00:00:00Z</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 class="ld">2026-12-27T00:00:00Z</span><span class="w">
</span></span></span></code></pre></div><p>A recurring window uses <code>schedule</code> + <code>duration</code>; an absolute window uses
<code>from</code> + <code>to</code>. See <a href="/usage/update-windows/">update windows</a>.</p>
<hr>
<h2 id="stages">Stages</h2>
<p><code>stages</code> (required, min 1) is the ordered list. A stage with every field set:</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">stages</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">app                </span><span class="w"> </span><span class="c"># required; DNS-label, unique in the StageSet</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">name</span><span class="p">:</span><span class="w"> </span><span class="l">my-app           </span><span class="w"> </span><span class="c"># required</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 class="c"># default; also GitRepository/OCIRepository/Bucket</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">                                </span><span class="c"># directly, or a producer (e.g. JsonnetSnippet)</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">source.toolkit.fluxcd.io/v1  </span><span class="w"> </span><span class="c"># required for a producer kind</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">namespace: other-ns     # default</span><span class="p">:</span><span class="w"> </span><span class="l">the StageSet&#39;s namespace</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">./overlays/prod    </span><span class="w"> </span><span class="c"># path inside the artifact (default ./)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">prune</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="w">               </span><span class="c"># GC objects that leave the stage (default true)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">timeout: 3m               # per-stage timeout (default</span><span class="p">:</span><span class="w"> </span><span class="l">spec.timeout)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">force: false              # sugar for conflictPolicy.default</span><span class="p">:</span><span class="w"> </span><span class="l">Recreate</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">applyHelmHookResources</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="w">  </span><span class="c"># apply helm.sh/hook objects as ordinary ones</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">patches</span><span class="p">:</span><span class="w"> </span><span class="p">[]</span><span class="w">               </span><span class="c"># Kustomize patches applied after build</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">conflictPolicy</span><span class="p">:</span><span class="w"> </span>{}<span class="w">        </span><span class="c"># see below</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">postBuild</span><span class="p">:</span><span class="w"> </span>{}<span class="w">             </span><span class="c"># see below</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">actions</span><span class="p">:</span><span class="w"> </span>{}<span class="w">               </span><span class="c"># see below</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">readyChecks</span><span class="p">:</span><span class="w"> </span>{}<span class="w">           </span><span class="c"># see below</span><span class="w">
</span></span></span></code></pre></div><p><code>sourceRef.kind</code> defaults to <code>ExternalArtifact</code>, so the common case is just
<code>sourceRef: { name: … }</code>. A <code>sourceRef</code> resolves to a <a href="https://fluxcd.io/">Flux</a>
artifact in one of three ways: an <code>ExternalArtifact</code>
(<a href="https://github.com/fluxcd/flux2/tree/main/rfcs">RFC-0012</a>, the default), a classic
Flux source — <code>GitRepository</code>, <code>OCIRepository</code>, or <code>Bucket</code> — consumed <strong>directly</strong>,
or any other kind treated as a <em>producer</em> and resolved to its <code>ExternalArtifact</code> via
the back-pointer index. See
<a href="/usage/stages-and-sources/#source-kinds">stages and sources</a> and
<a href="/usage/producer-aware-sources/">producer-aware sources</a>.</p>
<h3 id="patches">patches</h3>
<p><a href="https://kubectl.docs.kubernetes.io/">kustomize</a> strategic-merge or JSON6902
patches, applied after the build:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="w">      </span><span class="nt">patches</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span>- <span class="nt">patch</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">            - op: replace
</span></span></span><span class="line"><span class="cl"><span class="sd">              path: /spec/replicas
</span></span></span><span class="line"><span class="cl"><span class="sd">              value: 6</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">kind</span><span class="p">:</span><span class="w"> </span><span class="l">Deployment</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">web</span><span class="w">
</span></span></span></code></pre></div><h3 id="postbuild">postBuild</h3>
<p>Variable substitution after build and patching:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="w">      </span><span class="nt">postBuild</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">substitute</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">cluster_name</span><span class="p">:</span><span class="w"> </span><span class="l">prod-eu       </span><span class="w"> </span><span class="c"># inline key/value</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">substituteFrom</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: ConfigMap            # required</span><span class="p">:</span><span class="w"> </span><span class="l">ConfigMap or Secret</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-vars</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">Secret</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-secrets</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span><span class="nt">optional</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="w">             </span><span class="c"># tolerate a missing source</span><span class="w">
</span></span></span></code></pre></div><h3 id="conflictpolicy">conflictPolicy</h3>
<p>Per-resource answers to apply conflicts (immutable fields, ownership):</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="w">      </span><span class="nt">conflictPolicy</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">default</span><span class="p">:</span><span class="w"> </span><span class="l">Fail                 </span><span class="w"> </span><span class="c"># Fail (default) | Recreate | KeepExisting</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">target</span><span class="p">:</span><span class="w">                    </span><span class="c"># partial selector; unset fields match all</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">batch/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">Job</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span><span class="nt">action</span><span class="p">:</span><span class="w"> </span><span class="l">Recreate</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">kind</span><span class="p">:</span><span class="w"> </span><span class="l">PersistentVolumeClaim</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">scratch</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span><span class="nt">action</span><span class="p">:</span><span class="w"> </span><span class="l">Recreate</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span><span class="nt">allowDataLoss</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="w">        </span><span class="c"># required to Recreate a PVC/PV</span><span class="w">
</span></span></span></code></pre></div><p>See <a href="/usage/conflict-policies/">conflict policies</a>.</p>
<h3 id="readychecks">readyChecks</h3>
<p>Gate when the stage counts as complete:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="w">      </span><span class="nt">readyChecks</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">timeout</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">disableWait</span><span class="p">:</span><span class="w"> </span><span class="kc">false</span><span class="w">             </span><span class="c"># true = apply without waiting for readiness</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">checks</span><span class="p">:</span><span class="w">                        </span><span class="c"># explicit objects, evaluated with kstatus</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">apiextensions.k8s.io/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">CustomResourceDefinition</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">ledgers.example</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">exprs</span><span class="p">:</span><span class="w">                         </span><span class="c"># custom health via CEL expressions (healthCheckExprs shape)</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">db.example/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">Database</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span><span class="nt">current</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;status.phase == &#39;Running&#39;&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span><span class="nt">inProgress</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;status.phase in [&#39;Pending&#39;,&#39;Provisioning&#39;]&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span><span class="nt">failed</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;status.phase == &#39;Failed&#39;&#34;</span><span class="w">
</span></span></span></code></pre></div><p>Health expressions use <a href="https://github.com/google/cel-spec">CEL</a>. See
<a href="/usage/ready-checks/">ready checks</a>.</p>
<hr>
<h2 id="actions">Actions</h2>
<p><code>stages[].actions</code> (and <code>migrations[].actions</code>) carry typed steps. Each <code>Action</code>
has a <code>name</code>, optional <code>timeout</code>/<code>retries</code>, and <strong>exactly one</strong> operation block.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="w">      </span><span class="nt">actions</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">pre</span><span class="p">:</span><span class="w">        </span><span class="c"># before apply; failure aborts the stage with nothing applied</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">db-migrate</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span><span class="nt">timeout</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">retries</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">job</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 class="w"> </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">my-app-migrations }</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">./jobs</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">post</span><span class="p">:</span><span class="w">       </span><span class="c"># after verify; the stage is Ready only if these pass</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">smoke-test</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">url</span><span class="p">:</span><span class="w"> </span><span class="l">https://my-app.internal/healthz</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">              </span><span class="nt">method</span><span class="p">:</span><span class="w"> </span><span class="l">GET                   </span><span class="w"> </span><span class="c"># default POST</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">              </span><span class="nt">expectedStatus: [200]          # default</span><span class="p">:</span><span class="w"> </span><span class="l">any 2xx</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">              </span><span class="nt">headersFrom</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">gate-token</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">token</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">onFailure</span><span class="p">:</span><span class="w">  </span><span class="c"># best-effort on any failure from apply onward</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">page-oncall</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">url</span><span class="p">:</span><span class="w"> </span><span class="l">https://alerts.internal/stageset-failed</span><span class="w">
</span></span></span></code></pre></div><p>The six operation types — one per Action:</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"># patch — patch an existing object</span><span class="w">
</span></span></span><span class="line"><span class="cl">- <span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">enable-traffic</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">patch</span><span class="p">:</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 class="w"> </span><span class="nt">apiVersion: v1, kind: Service, name</span><span class="p">:</span><span class="w"> </span><span class="l">web }</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">merge               </span><span class="w"> </span><span class="c"># merge (default) | json6902</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">patch</span><span class="p">:</span><span class="w"> </span><span class="s1">&#39;{ &#34;spec&#34;: { &#34;selector&#34;: { &#34;release&#34;: &#34;green&#34; } } }&#39;</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"># http — call an endpoint (hosts gated by --allowed-action-hosts)</span><span class="w">
</span></span></span><span class="line"><span class="cl">- <span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">approve</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">url</span><span class="p">:</span><span class="w"> </span><span class="l">https://gate.internal/approve</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">bodyFrom</span><span class="p">:</span><span class="w"> </span>{<span class="w"> </span><span class="nt">name: approve-secret, key</span><span class="p">:</span><span class="w"> </span><span class="l">body }</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"># wait — block for a duration or until a CEL expr holds</span><span class="w">
</span></span></span><span class="line"><span class="cl">- <span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">settle</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">wait</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">duration</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="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">until-available</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">wait</span><span class="p">:</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 class="w"> </span><span class="nt">apiVersion: apps/v1, kind: Deployment, name</span><span class="p">:</span><span class="w"> </span><span class="l">web }</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">expr</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;status.availableReplicas &gt;= 3&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">timeout</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></span><span class="line"><span class="cl"><span class="c"># job — render and await Jobs from an artifact</span><span class="w">
</span></span></span><span class="line"><span class="cl">- <span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">migrate</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">job</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 class="w"> </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">my-app-migrations }</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">./jobs</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"># delete — remove an existing object (missing = success)</span><span class="w">
</span></span></span><span class="line"><span class="cl">- <span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">drop-legacy</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">delete</span><span class="p">:</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 class="w"> </span><span class="nt">apiVersion: batch/v1, kind: Job, name</span><span class="p">:</span><span class="w"> </span><span class="l">legacy-migration }</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"># apply — transient, rollout-scoped manifests (NOT inventory-tracked, never pruned)</span><span class="w">
</span></span></span><span class="line"><span class="cl">- <span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">canary</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">apply</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 class="w"> </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">my-app-canary }</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">./</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">wait</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="w">                 </span><span class="c"># block until applied objects report Ready</span><span class="w">
</span></span></span></code></pre></div><p>See <a href="/usage/actions/">actions</a>.</p>
<hr>
<h2 id="status">status</h2>
<p><code>status</code> is controller-owned and read-only. A representative snapshot:</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">observedGeneration</span><span class="p">:</span><span class="w"> </span><span class="m">7</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">All 2 stages applied</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">lastHandledReconcileAt</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;2026-06-15T09:21:04Z&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">lastAttemptedRevisions</span><span class="p">:</span><span class="w"> </span>{<span class="w"> </span><span class="nt">payments/payments-app</span><span class="p">:</span><span class="w"> </span><span class="l">sha256:1a2b }</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">lastAppliedRevisions</span><span class="p">:</span><span class="w">   </span>{<span class="w"> </span><span class="nt">payments/payments-app</span><span class="p">:</span><span class="w"> </span><span class="l">sha256:1a2b }</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">version</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;2.1.0&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">pendingMigrations</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="nt">executedMigrations</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="nt">inventoryMode</span><span class="p">:</span><span class="w"> </span><span class="l">hybrid</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">stages</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">infrastructure</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">phase</span><span class="p">:</span><span class="w"> </span><span class="l">Ready            </span><span class="w"> </span><span class="c"># Pending|Applying|Pruning|Verifying|Ready|Failed</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">appliedRevision</span><span class="p">:</span><span class="w"> </span><span class="l">sha256:9f3c</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">entriesCount</span><span class="p">:</span><span class="w"> </span><span class="m">12</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">shards</span><span class="p">:</span><span class="w"> </span><span class="m">1</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="s2">&#34;&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">executedActions</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="nt">ledgerRevision</span><span class="p">:</span><span class="w"> </span><span class="l">sha256:9f3c</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">lastAppliedSnapshot</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span>- <span class="nt">stage</span><span class="p">:</span><span class="w"> </span><span class="l">infrastructure</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">http://source-controller.../infra.tar.gz</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">digest</span><span class="p">:</span><span class="w"> </span><span class="l">sha256:9f3c</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">pendingUpdate</span><span class="p">:</span><span class="w">               </span><span class="c"># set only when a window holds a rollout</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">revisions</span><span class="p">:</span><span class="w"> </span>{<span class="w"> </span><span class="nt">payments/payments-app</span><span class="p">:</span><span class="w"> </span><span class="l">sha256:cafe }</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">nextWindowOpens</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;2026-06-16T08:00:00Z&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">lastHandledUpdateOverride</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;2026-06-15T09:30:00Z&#34;</span><span class="w">
</span></span></span></code></pre></div><p>The <code>Ready</code> condition&rsquo;s reason is one of the wire-stable values documented in the
<a href="/runbooks/">runbooks</a>.</p>
]]></content><category scheme="https://stageset.projects.metio.wtf/tags/api" term="api" label="api"/><category scheme="https://stageset.projects.metio.wtf/tags/stages" term="stages" label="stages"/><category scheme="https://stageset.projects.metio.wtf/tags/crd" term="crd" label="crd"/><category scheme="https://stageset.projects.metio.wtf/tags/sources" term="sources" label="sources"/></entry><entry><title type="html">StageSet vs Argo Rollouts</title><link href="https://stageset.projects.metio.wtf/comparisons/argo-rollouts/?utm_source=atom_feed" rel="alternate" type="text/html"/><link href="https://stageset.projects.metio.wtf/tutorials/progressive-delivery/?utm_source=atom_feed" rel="related" type="text/html" title="Progressive delivery"/><link href="https://stageset.projects.metio.wtf/comparisons/flux/?utm_source=atom_feed" rel="related" type="text/html" title="StageSet vs Flux kustomize-controller"/><link href="https://stageset.projects.metio.wtf/comparisons/helm/?utm_source=atom_feed" rel="related" type="text/html" title="StageSet vs Helm"/><link href="https://stageset.projects.metio.wtf/comparisons/kustomize/?utm_source=atom_feed" rel="related" type="text/html" title="StageSet vs Kustomize"/><link href="https://stageset.projects.metio.wtf/comparisons/tanka-kubecfg/?utm_source=atom_feed" rel="related" type="text/html" title="StageSet vs Tanka and kubecfg"/><id>https://stageset.projects.metio.wtf/comparisons/argo-rollouts/</id><published>0001-01-01T00:00:00+00:00</published><updated>2026-06-16T13:41:17+02:00</updated><content type="html"><![CDATA[<blockquote>Different layers — staged multi-artifact delivery versus single-workload traffic shifting.</blockquote><p><a href="https://argoproj.github.io/argo-rollouts/">Argo Rollouts</a> and <code>StageSet</code> are easy
to mention in the same breath because both roll things out gradually, but they
operate at different layers and are complementary rather than competing.</p>
<h2 id="what-argo-rollouts-does">What Argo Rollouts does</h2>
<p><code>Argo Rollouts</code> replaces a <code>Deployment</code> with a <code>Rollout</code> that shifts traffic to a new
version <strong>progressively</strong> — canary or blue-green — pausing at weighted steps and
promoting based on <strong>metric analysis</strong> (Prometheus queries, web/Job providers).
Its unit of work is a <strong>single workload&rsquo;s</strong> version transition and the traffic in
front of it.</p>
<h2 id="what-stageset-does">What StageSet does</h2>
<p><code>StageSet</code> orchestrates a <strong>whole release</strong> as an ordered list of stages, each built
from a Flux <code>ExternalArtifact</code> — CRDs before the operator that needs them, a
migration before the app, config before the workload — gating each stage on health
and running typed <a href="/usage/actions/">actions</a> between them. It does not shift
traffic or run metric analysis; its unit of work is the <strong>multi-component release</strong>
and the order things apply in.</p>
<table>
	<thead>
			<tr>
					<th></th>
					<th>Argo Rollouts</th>
					<th>StageSet</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td>Unit of work</td>
					<td>one workload&rsquo;s version + its traffic</td>
					<td>a multi-stage release of artifacts</td>
			</tr>
			<tr>
					<td>Mechanism</td>
					<td>weighted traffic shifting + metric analysis</td>
					<td>ordered apply with readiness gates + actions</td>
			</tr>
			<tr>
					<td>Promotion driver</td>
					<td>analysis metrics (Prometheus, web, Job)</td>
					<td>stage readiness (kstatus, CEL) and actions</td>
			</tr>
			<tr>
					<td>Pruning / inventory</td>
					<td>no (owns the Rollout&rsquo;s pods)</td>
					<td>yes (ApplySet inventory, per-stage prune)</td>
			</tr>
			<tr>
					<td>GitOps reconcile</td>
					<td>via Argo CD / a GitOps tool</td>
					<td>native (Flux controller)</td>
			</tr>
	</tbody>
</table>
<h2 id="they-compose">They compose</h2>
<p>A realistic setup uses both: <code>StageSet</code> rolls out the release in order, and a
workload <em>inside</em> one stage is itself an Argo <code>Rollout</code> doing a canary. <code>StageSet</code>
gets the supporting pieces (CRDs, config, migrations) in place and healthy;
<code>Argo Rollouts</code> handles the fine-grained traffic progression for that one service.</p>
<h2 id="integrating-them">Integrating them</h2>
<p>Both directions are supported:</p>
<ul>
<li><strong>Argo gating on StageSet.</strong> The controller exports a
<code>stageset_stage_ready{namespace,stageset,stage}</code> gauge that Argo&rsquo;s Prometheus
metric reads directly, and the stage <a href="/tutorials/progressive-delivery/">gate endpoint</a>
also answers JSON for Argo&rsquo;s web metric. So an Argo <code>Rollout</code> can hold its
promotion until a <code>StageSet</code> stage is Ready — no Job bridge needed.</li>
<li><strong>StageSet gating on Argo.</strong> A <code>StageSet</code> stage&rsquo;s <a href="/usage/ready-checks/">ready checks</a>
can wait (via CEL) on an Argo <code>Rollout</code> reaching <code>Healthy</code> before the next stage
runs.</li>
</ul>
<p>The full, worked examples for both are in the
<a href="/tutorials/progressive-delivery/#argo-rollouts">progressive-delivery tutorial</a>.
Where the gate&rsquo;s HTTP-status contract is a native fit for
<a href="https://flagger.app/">Flagger</a>, the readiness gauge and JSON endpoint make
<code>Argo Rollouts</code> a first-class consumer too.</p>
]]></content><category scheme="https://stageset.projects.metio.wtf/tags/comparison" term="comparison" label="comparison"/><category scheme="https://stageset.projects.metio.wtf/tags/argo" term="argo" label="argo"/><category scheme="https://stageset.projects.metio.wtf/tags/progressive-delivery" term="progressive-delivery" label="progressive-delivery"/><category scheme="https://stageset.projects.metio.wtf/tags/stages" term="stages" label="stages"/></entry><entry><title type="html">StageSet vs Flux kustomize-controller</title><link href="https://stageset.projects.metio.wtf/comparisons/flux/?utm_source=atom_feed" rel="alternate" type="text/html"/><link href="https://stageset.projects.metio.wtf/comparisons/jsonnet-controller/?utm_source=atom_feed" rel="related" type="text/html" title="StageSet vs jsonnet-controller"/><link href="https://stageset.projects.metio.wtf/comparisons/argo-rollouts/?utm_source=atom_feed" rel="related" type="text/html" title="StageSet vs Argo Rollouts"/><link href="https://stageset.projects.metio.wtf/comparisons/helm/?utm_source=atom_feed" rel="related" type="text/html" title="StageSet vs Helm"/><link href="https://stageset.projects.metio.wtf/comparisons/kustomize/?utm_source=atom_feed" rel="related" type="text/html" title="StageSet vs Kustomize"/><link href="https://stageset.projects.metio.wtf/comparisons/tanka-kubecfg/?utm_source=atom_feed" rel="related" type="text/html" title="StageSet vs Tanka and kubecfg"/><id>https://stageset.projects.metio.wtf/comparisons/flux/</id><published>0001-01-01T00:00:00+00:00</published><updated>2026-06-16T13:59:54+02:00</updated><content type="html"><![CDATA[<blockquote>Intra-release stages and gates versus per-Kustomization ordering.</blockquote><p>This is the closest comparison — <code>StageSet</code> is built <em>for</em>
<a href="https://fluxcd.io/">Flux</a> and borrows its conventions. Flux&rsquo;s <code>kustomize-controller</code>
(and <code>helm-controller</code>) reconcile a source into the cluster continuously, exactly
like <code>StageSet</code>. The difference is granularity.</p>
<h2 id="what-kustomize-controller-gives-you">What kustomize-controller gives you</h2>
<ul>
<li>Continuous reconciliation of a <code>Kustomization</code> from a Flux source, with pruning,
health checks, drift correction, and <code>dependsOn</code> ordering <strong>between</strong>
Kustomizations.</li>
<li>Impersonation via <code>serviceAccountName</code>, <code>postBuild</code> substitution, patches — the
same surface <code>StageSet</code> deliberately mirrors.</li>
</ul>
<h2 id="where-stageset-differs">Where StageSet differs</h2>
<ul>
<li><strong>Ordering within a release.</strong> <code>kustomize-controller</code> applies one Kustomization
as a unit; ordering exists only <em>between</em> Kustomizations via <code>dependsOn</code>. To
sequence three steps you create three Kustomizations and wire their
dependencies. <code>StageSet</code> expresses that as one resource with ordered <code>stages</code> —
and the controller waits for each stage&rsquo;s health before the next.</li>
<li><strong>Typed actions between steps.</strong> Migrations, HTTP gates, waits, and transient
applies are first-class <a href="/usage/actions/">actions</a>; in plain Flux you&rsquo;d model
these as extra Kustomizations and Jobs.</li>
<li><strong>Release-level features.</strong> <a href="/usage/update-windows/">Update windows</a>,
<a href="/usage/versioned-migrations/">versioned migrations</a>, and
<a href="/usage/rollback/">rollback</a> operate across the whole staged release.</li>
<li><strong>Source-native.</strong> A stage consumes a <code>GitRepository</code>/<code>OCIRepository</code>/<code>Bucket</code>
directly (just like <code>kustomize-controller</code>), or an <code>ExternalArtifact</code> (RFC-0012),
or a <em>producer</em> resolved to its artifact — which is how it also pairs with
renderers like <a href="https://jaas.projects.metio.wtf/">JaaS</a>.</li>
<li><strong>SOPS parity.</strong> Encrypted Secrets in a source decrypt the same way, via
<a href="/usage/encryption/"><code>spec.decryption</code></a> (age, PGP, or cloud KMS), so a SOPS-using
repo ports across unchanged.</li>
</ul>
<h2 id="using-them-together">Using them together</h2>
<p><code>StageSet</code> sits alongside the other Flux controllers and reuses Flux&rsquo;s source layer,
notifications (<code>Alert</code>/<code>Provider</code> targeting <code>kind: StageSet</code>), and reconcile
annotations. Use <code>kustomize-controller</code> for ordinary one-shot reconciliation and
reach for <code>StageSet</code> when a release needs ordered, gated stages.</p>
]]></content><category scheme="https://stageset.projects.metio.wtf/tags/comparison" term="comparison" label="comparison"/><category scheme="https://stageset.projects.metio.wtf/tags/flux" term="flux" label="flux"/><category scheme="https://stageset.projects.metio.wtf/tags/stages" term="stages" label="stages"/></entry><entry><title type="html">StageSet vs Helm</title><link href="https://stageset.projects.metio.wtf/comparisons/helm/?utm_source=atom_feed" rel="alternate" type="text/html"/><link href="https://stageset.projects.metio.wtf/comparisons/argo-rollouts/?utm_source=atom_feed" rel="related" type="text/html" title="StageSet vs Argo Rollouts"/><link href="https://stageset.projects.metio.wtf/comparisons/flux/?utm_source=atom_feed" rel="related" type="text/html" title="StageSet vs Flux kustomize-controller"/><link href="https://stageset.projects.metio.wtf/comparisons/kustomize/?utm_source=atom_feed" rel="related" type="text/html" title="StageSet vs Kustomize"/><link href="https://stageset.projects.metio.wtf/comparisons/tanka-kubecfg/?utm_source=atom_feed" rel="related" type="text/html" title="StageSet vs Tanka and kubecfg"/><link href="https://stageset.projects.metio.wtf/comparisons/jsonnet-controller/?utm_source=atom_feed" rel="related" type="text/html" title="StageSet vs jsonnet-controller"/><id>https://stageset.projects.metio.wtf/comparisons/helm/</id><published>0001-01-01T00:00:00+00:00</published><updated>2026-06-16T13:41:17+02:00</updated><content type="html"><![CDATA[<blockquote>How StageSet relates to Helm&rsquo;s templating and hook ordering.</blockquote><p><a href="https://helm.sh/">Helm</a> is two things: a templating engine (charts) and an
imperative release tool (<code>helm upgrade</code>). <code>StageSet</code> is neither — it&rsquo;s a declarative
delivery controller. The overlap is ordering: Helm&rsquo;s hooks and hook weights give you
<em>some</em> sequencing inside a single chart&rsquo;s install/upgrade.</p>
<h2 id="what-helm-gives-you">What Helm gives you</h2>
<ul>
<li>Templated, parameterized manifests (charts and values).</li>
<li>Install/upgrade ordering via <code>helm.sh/hook</code> (pre-install, post-upgrade, …) and
<code>hook-weight</code>.</li>
<li>A release history you can roll back to with <code>helm rollback</code>.</li>
</ul>
<h2 id="where-stageset-differs">Where StageSet differs</h2>
<ul>
<li><strong>Continuous reconciliation.</strong> <code>helm upgrade</code> is a point-in-time, imperative
action; nothing re-asserts the state afterward. <code>StageSet</code> reconciles on an
interval, corrects drift, and prunes — it&rsquo;s GitOps, not a one-shot.</li>
<li><strong>Ordering across artifacts, not just within one chart.</strong> Helm hooks order
resources <em>inside</em> a release. <code>StageSet</code> orders whole <em>stages</em>, each its own
artifact, with readiness gating between them.</li>
<li><strong>Typed gates between steps.</strong> Hooks run Jobs; <code>StageSet</code> stages can run Jobs,
HTTP gates, waits, patches, deletes, and transient applies, as pre/post/onFailure
<a href="/usage/actions/">actions</a>.</li>
<li><strong>Identity.</strong> A <code>StageSet</code> applies under an impersonated, per-tenant
<code>ServiceAccount</code>; <code>helm upgrade</code> runs as whoever ran it.</li>
</ul>
<h2 id="using-them-together">Using them together</h2>
<p>Render a chart to manifests (e.g. via a producer that publishes an
<code>ExternalArtifact</code>) and deliver it with <code>StageSet</code>. The controller understands
<code>helm.sh/hook</code> resources: <code>applyHelmHookResources</code> (default <code>true</code>) applies them as
ordinary objects, so a Helm-style chart&rsquo;s hook resources still get created — now
under <code>StageSet</code>&rsquo;s ordering and gating instead of Helm&rsquo;s.</p>
]]></content><category scheme="https://stageset.projects.metio.wtf/tags/comparison" term="comparison" label="comparison"/><category scheme="https://stageset.projects.metio.wtf/tags/helm" term="helm" label="helm"/><category scheme="https://stageset.projects.metio.wtf/tags/stages" term="stages" label="stages"/></entry><entry><title type="html">StageSet vs jsonnet-controller</title><link href="https://stageset.projects.metio.wtf/comparisons/jsonnet-controller/?utm_source=atom_feed" rel="alternate" type="text/html"/><link href="https://stageset.projects.metio.wtf/comparisons/flux/?utm_source=atom_feed" rel="related" type="text/html" title="StageSet vs Flux kustomize-controller"/><link href="https://stageset.projects.metio.wtf/comparisons/tanka-kubecfg/?utm_source=atom_feed" rel="related" type="text/html" title="StageSet vs Tanka and kubecfg"/><link href="https://stageset.projects.metio.wtf/tutorials/jsonnet-to-rollout/?utm_source=atom_feed" rel="related" type="text/html" title="From Jsonnet to a gated rollout"/><link href="https://stageset.projects.metio.wtf/comparisons/argo-rollouts/?utm_source=atom_feed" rel="related" type="text/html" title="StageSet vs Argo Rollouts"/><link href="https://stageset.projects.metio.wtf/comparisons/helm/?utm_source=atom_feed" rel="related" type="text/html" title="StageSet vs Helm"/><id>https://stageset.projects.metio.wtf/comparisons/jsonnet-controller/</id><published>0001-01-01T00:00:00+00:00</published><updated>2026-06-16T13:41:17+02:00</updated><content type="html"><![CDATA[<blockquote>A jsonnet-native Flux applier versus a renderer-agnostic staged-delivery layer.</blockquote><p><a href="https://github.com/pelotech/jsonnet-controller">jsonnet-controller</a> (pelotech) is a
Flux controller that evaluates Jsonnet (kubecfg- and Tanka-style) and applies the
result to the cluster. Its <code>Konfiguration</code> resource (<code>jsonnet.io/v1beta1</code>) is, in
effect, <em>kustomize-controller for Jsonnet</em>: point it at a <code>GitRepository</code> (or an
HTTP(S) URL), and it builds the Jsonnet and reconciles the manifests — with
pruning, health/revision tracking, TLA string/code variables, and <code>dependsOn</code>
ordering <strong>between</strong> Konfigurations.</p>
<p>The two projects sit at <strong>different layers</strong>, which is the whole comparison.</p>
<h2 id="what-jsonnet-controller-gives-you">What jsonnet-controller gives you</h2>
<ul>
<li><strong>Jsonnet rendering and applying in one resource.</strong> A <code>Konfiguration</code> both
evaluates the Jsonnet and applies the output — the rendering engine is part of
the controller. If your goal is &ldquo;get this Jsonnet/Tanka tree into the cluster,&rdquo;
it&rsquo;s a direct, single-resource answer.</li>
<li>The familiar Flux applier surface: prune, health, interval reconciliation, TLAs,
and <code>dependsOn</code> between Konfigurations.</li>
</ul>
<h2 id="where-stageset-differs">Where StageSet differs</h2>
<ul>
<li><strong>Renderer-agnostic.</strong> <code>StageSet</code> does <em>not</em> evaluate Jsonnet. A stage consumes a
Flux source — <code>GitRepository</code>, <code>OCIRepository</code>, <code>Bucket</code>, or an <code>ExternalArtifact</code>
— so it rolls out plain manifests <em>or</em> the output of any renderer. For Jsonnet
specifically, <a href="https://jaas.projects.metio.wtf/">JaaS</a> does the evaluation
(TLAs, ext vars, jb-vendored libraries, <a href="https://github.com/metio/jsonnet-oci-images">JOI</a>
images) and publishes an <code>ExternalArtifact</code> that <code>StageSet</code> consumes. Rendering and
delivery are separate concerns, owned by separate components.</li>
<li><strong>Ordering and gating <em>within</em> a release.</strong> A <code>Konfiguration</code> applies as one unit;
sequencing exists only <em>between</em> Konfigurations via <code>dependsOn</code>. <code>StageSet</code> expresses
a release as ordered <a href="/usage/stages-and-sources/">stages</a>, each waiting on the
previous stage&rsquo;s health, with typed <a href="/usage/actions/">actions</a> (migration Jobs,
HTTP gates, waits) <em>between</em> steps.</li>
<li><strong>Release-level machinery</strong> jsonnet-controller doesn&rsquo;t carry:
<a href="/usage/update-windows/">update windows</a>,
<a href="/usage/versioned-migrations/">versioned migrations</a>,
<a href="/usage/conflict-policies/">conflict policies</a>, and
<a href="/usage/rollback/">rollback</a> across the whole staged release.</li>
</ul>
<h2 id="which-to-reach-for">Which to reach for</h2>
<ul>
<li>You want <strong>Jsonnet rendered and applied as a single unit</strong>, no staging →
jsonnet-controller is a clean fit (as is JaaS paired with Flux&rsquo;s
<code>kustomize-controller</code>).</li>
<li>You want <strong>ordered, gated, multi-stage delivery</strong> of manifests — whatever renders
them → <code>StageSet</code>, with <code>JaaS</code> supplying the Jsonnet rendering when you need it.</li>
</ul>
<p>They are not mutually exclusive: jsonnet-controller answers <em>how Jsonnet becomes
manifests</em>, <code>StageSet</code> answers <em>how a release is sequenced and gated</em>. You can pick a
renderer independently — <code>JaaS</code> is one option that keeps the rendering reusable
(local <code>jsonnet</code>-parity, OCI libraries) and hands <code>StageSet</code> an artifact like any
other.</p>
<h2 id="using-them-together">Using them together</h2>
<p>Because a <code>Konfiguration</code> is a Kubernetes object, a <code>StageSet</code> stage can <strong>apply a
<code>Konfiguration</code> and gate on it</strong> — letting jsonnet-controller do the Jsonnet
rendering and applying while <code>StageSet</code> sequences it among other stages, with actions
and gates in between.</p>
<p>Put the <code>Konfiguration</code> manifest in a source the stage reads (here a
<code>GitRepository</code>), then gate the stage on the <code>Konfiguration</code> reaching <code>Ready</code> for
its current generation:</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">stages.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">StageSet</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">platform</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">platform</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">stages</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                    </span><span class="w"> </span><span class="c"># render + apply the Jsonnet via jsonnet-controller</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">platform-konfig       </span><span class="w"> </span><span class="c"># a repo holding the Konfiguration manifest</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">readyChecks</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">exprs</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">jsonnet.io/v1beta1</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">Konfiguration</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span><span class="c"># don&#39;t proceed until jsonnet-controller has reconciled THIS generation</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span><span class="nt">current</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;status.observedGeneration == metadata.generation &amp;&amp; status.conditions.exists(c, c.type == &#39;Ready&#39; &amp;&amp; c.status == &#39;True&#39;)&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span><span class="nt">inProgress</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;status.conditions.exists(c, c.type == &#39;Ready&#39; &amp;&amp; c.status == &#39;Unknown&#39;)&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span><span class="nt">failed</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;status.conditions.exists(c, c.type == &#39;Ready&#39; &amp;&amp; c.status == &#39;False&#39;)&#34;</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">smoke                    </span><span class="w"> </span><span class="c"># only runs once the Konfiguration is Ready</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">platform-smoke</span><span class="w">
</span></span></span></code></pre></div><p><code>StageSet</code> applies the <code>Konfiguration</code> and waits — the <code>current</code> expression holds the
rollout until jsonnet-controller has observed the latest generation <em>and</em> reports
<code>Ready</code>, so a later stage never starts against a half-rendered base.</p>
<h3 id="ownership-differs-from-the-jaas-path">Ownership differs from the JaaS path</h3>
<p>This is the important distinction, and it changes who prunes what:</p>
<ul>
<li><strong>Via jsonnet-controller (above).</strong> <code>StageSet</code>&rsquo;s inventory owns <strong>only the
<code>Konfiguration</code> object</strong>. The workloads rendered from the Jsonnet are owned and
<strong>pruned by jsonnet-controller</strong> — <code>StageSet</code> never sees them individually. Delete
the stage and <code>StageSet</code> removes the <code>Konfiguration</code>; jsonnet-controller then
cascades the prune of what it created. You get two nested owners.</li>
<li><strong>Via <a href="https://jaas.projects.metio.wtf/">JaaS</a> (or any source).</strong> <code>JaaS</code> only
<em>renders</em> — it publishes an <code>ExternalArtifact</code> and owns nothing in the target
cluster. <code>StageSet</code> fetches that artifact and applies the manifests itself, so
<strong><code>StageSet</code>&rsquo;s inventory owns every rendered object directly</strong> and prunes them with
its own ApplySet semantics. One owner, and <code>StageSet</code>&rsquo;s drift correction,
<a href="/usage/conflict-policies/">conflict policies</a>, and <a href="/usage/rollback/">rollback</a>
apply to the resources themselves.</li>
</ul>
<p>So if you want <code>StageSet</code> to be the single owner and pruner of the delivered
resources, render with <code>JaaS</code> (or read plain manifests from a source). Reach for the
<code>Konfiguration</code>-as-a-stage pattern when you specifically want jsonnet-controller to
keep owning what it renders, and only need <code>StageSet</code> to sequence and gate it.</p>
]]></content><category scheme="https://stageset.projects.metio.wtf/tags/comparison" term="comparison" label="comparison"/><category scheme="https://stageset.projects.metio.wtf/tags/jsonnet" term="jsonnet" label="jsonnet"/><category scheme="https://stageset.projects.metio.wtf/tags/flux" term="flux" label="flux"/><category scheme="https://stageset.projects.metio.wtf/tags/stages" term="stages" label="stages"/></entry><entry><title type="html">StageSet vs Kustomize</title><link href="https://stageset.projects.metio.wtf/comparisons/kustomize/?utm_source=atom_feed" rel="alternate" type="text/html"/><link href="https://stageset.projects.metio.wtf/comparisons/argo-rollouts/?utm_source=atom_feed" rel="related" type="text/html" title="StageSet vs Argo Rollouts"/><link href="https://stageset.projects.metio.wtf/comparisons/flux/?utm_source=atom_feed" rel="related" type="text/html" title="StageSet vs Flux kustomize-controller"/><link href="https://stageset.projects.metio.wtf/comparisons/helm/?utm_source=atom_feed" rel="related" type="text/html" title="StageSet vs Helm"/><link href="https://stageset.projects.metio.wtf/comparisons/tanka-kubecfg/?utm_source=atom_feed" rel="related" type="text/html" title="StageSet vs Tanka and kubecfg"/><link href="https://stageset.projects.metio.wtf/comparisons/jsonnet-controller/?utm_source=atom_feed" rel="related" type="text/html" title="StageSet vs jsonnet-controller"/><id>https://stageset.projects.metio.wtf/comparisons/kustomize/</id><published>0001-01-01T00:00:00+00:00</published><updated>2026-06-16T13:41:17+02:00</updated><content type="html"><![CDATA[<blockquote>Why StageSet delivers what Kustomize only builds.</blockquote><p><a href="https://kustomize.io/">Kustomize</a> (the <code>kustomize</code> CLI / <code>kubectl kustomize</code>) is a
manifest <em>builder</em>: it composes bases and overlays, applies patches, and emits YAML.
It does not apply anything, and it has no notion of ordering, readiness, or
reconciliation — that&rsquo;s <code>kubectl apply</code>&rsquo;s job, and <code>kubectl</code> applies everything at
once.</p>
<h2 id="what-kustomize-gives-you">What Kustomize gives you</h2>
<ul>
<li>Overlay composition, strategic-merge and JSON6902 patches, variable replacement,
generators.</li>
<li>A pure transformation: in goes a kustomization, out come manifests.</li>
</ul>
<h2 id="where-stageset-differs">Where StageSet differs</h2>
<ul>
<li><strong>It delivers, not just builds.</strong> Kustomize stops at YAML. <code>StageSet</code> applies it,
waits for health, prunes what&rsquo;s gone, and keeps doing so.</li>
<li><strong>Ordering and gates.</strong> <code>kubectl apply -k</code> has no stages and no gates. <code>StageSet</code>
sequences stages and runs <a href="/usage/actions/">actions</a> between them.</li>
<li><strong>Continuous reconciliation and drift correction</strong>, versus a one-shot <code>apply</code>.</li>
</ul>
<h2 id="using-them-together">Using them together</h2>
<p><code>StageSet</code> <em>includes</em> the parts of Kustomize you reach for at delivery time: a stage
has <code>path</code>, <code>patches</code>, and <code>postBuild</code> substitution
(<a href="/usage/stages-and-sources/">stages and sources</a>). So you can keep authoring with
Kustomize overlays and let a stage apply the right overlay, patched and
substituted — then add the ordering, gating, and reconciliation Kustomize alone
doesn&rsquo;t offer.</p>
]]></content><category scheme="https://stageset.projects.metio.wtf/tags/comparison" term="comparison" label="comparison"/><category scheme="https://stageset.projects.metio.wtf/tags/kubernetes" term="kubernetes" label="kubernetes"/><category scheme="https://stageset.projects.metio.wtf/tags/stages" term="stages" label="stages"/></entry><entry><title type="html">StageSet vs Tanka and kubecfg</title><link href="https://stageset.projects.metio.wtf/comparisons/tanka-kubecfg/?utm_source=atom_feed" rel="alternate" type="text/html"/><link href="https://stageset.projects.metio.wtf/comparisons/jsonnet-controller/?utm_source=atom_feed" rel="related" type="text/html" title="StageSet vs jsonnet-controller"/><link href="https://stageset.projects.metio.wtf/tutorials/jsonnet-to-rollout/?utm_source=atom_feed" rel="related" type="text/html" title="From Jsonnet to a gated rollout"/><link href="https://stageset.projects.metio.wtf/comparisons/argo-rollouts/?utm_source=atom_feed" rel="related" type="text/html" title="StageSet vs Argo Rollouts"/><link href="https://stageset.projects.metio.wtf/comparisons/flux/?utm_source=atom_feed" rel="related" type="text/html" title="StageSet vs Flux kustomize-controller"/><link href="https://stageset.projects.metio.wtf/comparisons/helm/?utm_source=atom_feed" rel="related" type="text/html" title="StageSet vs Helm"/><id>https://stageset.projects.metio.wtf/comparisons/tanka-kubecfg/</id><published>0001-01-01T00:00:00+00:00</published><updated>2026-06-16T13:41:17+02:00</updated><content type="html"><![CDATA[<blockquote>Reconciled, gated delivery versus CLI-driven Jsonnet apply.</blockquote><p><a href="https://tanka.dev/">Tanka</a> and <a href="https://github.com/kubecfg/kubecfg">kubecfg</a> are
Jsonnet-based config tools: you express your resources in Jsonnet, the tool renders
them, diffs against the cluster, and applies. They generate configuration and run a
CLI-driven apply, but they are imperative tools you run, not controllers that
reconcile.</p>
<h2 id="what-tanka--kubecfg-give-you">What Tanka / kubecfg give you</h2>
<ul>
<li>Jsonnet-powered, DRY manifest generation (libraries, abstractions, environments).</li>
<li>A <code>diff</code>/<code>apply</code> workflow with dependency-aware ordering of a single apply.</li>
</ul>
<h2 id="where-stageset-differs">Where StageSet differs</h2>
<ul>
<li><strong>Reconciliation, not invocation.</strong> Tanka/kubecfg apply when <em>you</em> run them.
<code>StageSet</code> runs in-cluster and continuously reconciles, corrects drift, and
prunes.</li>
<li><strong>Staged, gated delivery.</strong> They apply a rendered set (in dependency order);
they don&rsquo;t model multi-stage rollouts with readiness gates, update windows, or
versioned migrations between stages.</li>
<li><strong>GitOps identity and tenancy.</strong> <code>StageSet</code> applies under an impersonated tenant
<code>ServiceAccount</code> inside the cluster; Tanka/kubecfg use your local credentials.</li>
</ul>
<h2 id="using-them-together">Using them together</h2>
<p>The Jsonnet <em>generation</em> that Tanka and kubecfg do so well has a GitOps-native
equivalent in two related projects:</p>
<ul>
<li><strong><a href="https://github.com/metio/jsonnet-oci-images">JOI</a></strong> ships the Jsonnet
libraries as OCI images.</li>
<li><strong><a href="https://jaas.projects.metio.wtf/">JaaS</a></strong> evaluates the Jsonnet in-cluster
and publishes the rendered result as an <code>ExternalArtifact</code> — the rendering step,
as a service.</li>
<li><strong><code>StageSet</code></strong> delivers that artifact in ordered, gated stages — the apply step,
as a controller.</li>
</ul>
<p>So where you might run <code>tk apply</code> or <code>kubecfg update</code> from a laptop or CI, this
approach splits the same job into a producer (<code>JaaS</code>, importing <code>JOI</code> libraries) and
a delivery controller (<code>StageSet</code>), both reconciled by Flux. You can also keep using
Tanka/kubecfg to author and publish artifacts, and let <code>StageSet</code> handle delivery.</p>
]]></content><category scheme="https://stageset.projects.metio.wtf/tags/comparison" term="comparison" label="comparison"/><category scheme="https://stageset.projects.metio.wtf/tags/tanka" term="tanka" label="tanka"/><category scheme="https://stageset.projects.metio.wtf/tags/jsonnet" term="jsonnet" label="jsonnet"/><category scheme="https://stageset.projects.metio.wtf/tags/stages" term="stages" label="stages"/></entry><entry><title type="html">stagesetctl build</title><link href="https://stageset.projects.metio.wtf/cli/build/?utm_source=atom_feed" rel="alternate" type="text/html"/><link href="https://stageset.projects.metio.wtf/usage/producer-aware-sources/?utm_source=atom_feed" rel="related" type="text/html" title="Producer-aware sources"/><link href="https://stageset.projects.metio.wtf/api/stageset/?utm_source=atom_feed" rel="related" type="text/html" title="StageSet"/><link href="https://stageset.projects.metio.wtf/usage/stages-and-sources/?utm_source=atom_feed" rel="related" type="text/html" title="Stages and sources"/><link href="https://stageset.projects.metio.wtf/cli/diff/?utm_source=atom_feed" rel="related" type="text/html" title="stagesetctl diff"/><link href="https://stageset.projects.metio.wtf/cli/get/?utm_source=atom_feed" rel="related" type="text/html" title="stagesetctl get"/><id>https://stageset.projects.metio.wtf/cli/build/</id><published>0001-01-01T00:00:00+00:00</published><updated>2026-06-16T13:41:17+02:00</updated><content type="html"><![CDATA[<blockquote>Render a StageSet&rsquo;s manifests to stdout, exactly as the controller would.</blockquote><p>Runs the same resolve → fetch → build pipeline the controller uses and writes the
result — a multi-document YAML stream — to stdout. This is what would be applied,
before it is applied. To preview the change against live cluster state instead, use
<a href="/cli/diff/"><code>diff</code></a>.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">stagesetctl build NAME [flags]
</span></span></code></pre></div><table>
	<thead>
			<tr>
					<th>Flag</th>
					<th>Default</th>
					<th>Description</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td><code>--stage</code></td>
					<td><em>(all)</em></td>
					<td>Render only the named stage(s); repeatable.</td>
			</tr>
			<tr>
					<td><code>--source-dir</code></td>
					<td><em>(none)</em></td>
					<td>Use a local artifact tree as <code>[STAGE=]PATH</code> instead of fetching from the cluster; repeatable.</td>
			</tr>
			<tr>
					<td><code>--show-secrets</code></td>
					<td><code>false</code></td>
					<td>Reveal Secret values instead of masking them.</td>
			</tr>
			<tr>
					<td><code>--as-tenant</code></td>
					<td><code>false</code></td>
					<td>Render impersonating the StageSet&rsquo;s <code>spec.serviceAccountName</code> (see <a href="/usage/multi-cluster/">multi-cluster and tenancy</a>).</td>
			</tr>
	</tbody>
</table>
<p>Secret values are masked by default, so the output is safe to paste into a review.
<code>build</code> writes YAML unconditionally — there is no output-format flag.</p>
<h2 id="example">Example</h2>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">stagesetctl build payments --stage application
</span></span></code></pre></div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><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">apps/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">Deployment</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">web</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">payments</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">replicas</span><span class="p">:</span><span class="w"> </span><span class="m">6</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">selector</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 class="nt">app</span><span class="p">:</span><span class="w"> </span><span class="l">web}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">template</span><span class="p">:</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">labels</span><span class="p">:</span><span class="w"> </span>{<span class="nt">app</span><span class="p">:</span><span class="w"> </span><span class="l">web}</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">containers</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">web</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">image</span><span class="p">:</span><span class="w"> </span><span class="l">registry.internal/web:2.1.0</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">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">Secret</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">web-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">payments</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">type</span><span class="p">:</span><span class="w"> </span><span class="l">Opaque</span><span class="w">
</span></span></span><span class="line"><span class="cl"><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">token</span><span class="p">:</span><span class="w"> </span><span class="s1">&#39;***&#39;</span><span class="w">          </span><span class="c"># masked; pass --show-secrets to reveal</span><span class="w">
</span></span></span></code></pre></div><p><code>--source-dir</code> makes <code>build</code> work offline — point it at the directory an artifact
would have unpacked to and it skips the cluster fetch, for authoring and CI. The
value is <code>[STAGE=]PATH</code>: prefix a stage name to target one stage, or give a bare
path to feed every stage that has no entry of its own. Repeat the flag to map
each stage to its own tree:</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"># one stage from a local tree</span>
</span></span><span class="line"><span class="cl">stagesetctl build payments --stage application --source-dir <span class="nv">application</span><span class="o">=</span>./out
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># every stage from one tree (bare path), overriding just infrastructure</span>
</span></span><span class="line"><span class="cl">stagesetctl build payments <span class="se">\
</span></span></span><span class="line"><span class="cl">  --source-dir ./checkout <span class="se">\
</span></span></span><span class="line"><span class="cl">  --source-dir <span class="nv">infrastructure</span><span class="o">=</span>./infra-checkout
</span></span></code></pre></div>]]></content><category scheme="https://stageset.projects.metio.wtf/tags/cli" term="cli" label="cli"/><category scheme="https://stageset.projects.metio.wtf/tags/stages" term="stages" label="stages"/><category scheme="https://stageset.projects.metio.wtf/tags/sources" term="sources" label="sources"/></entry><entry><title type="html">stagesetctl diff</title><link href="https://stageset.projects.metio.wtf/cli/diff/?utm_source=atom_feed" rel="alternate" type="text/html"/><link href="https://stageset.projects.metio.wtf/cli/build/?utm_source=atom_feed" rel="related" type="text/html" title="stagesetctl build"/><link href="https://stageset.projects.metio.wtf/cli/get/?utm_source=atom_feed" rel="related" type="text/html" title="stagesetctl get"/><link href="https://stageset.projects.metio.wtf/cli/reconcile/?utm_source=atom_feed" rel="related" type="text/html" title="stagesetctl reconcile"/><link href="https://stageset.projects.metio.wtf/usage/actions/?utm_source=atom_feed" rel="related" type="text/html" title="Actions"/><link href="https://stageset.projects.metio.wtf/contributing/building/?utm_source=atom_feed" rel="related" type="text/html" title="Building and testing"/><id>https://stageset.projects.metio.wtf/cli/diff/</id><published>0001-01-01T00:00:00+00:00</published><updated>2026-06-16T13:41:17+02:00</updated><content type="html"><![CDATA[<blockquote>Preview what a StageSet would change in the cluster — usable as a CI gate.</blockquote><p>By default <code>diff</code> performs a
<a href="https://kubernetes.io/docs/reference/using-api/server-side-apply/">server-side</a>
dry-run apply and exits <code>1</code> when there are changes, so it works as a CI gate. It
shows, per object, what a reconcile would create, configure, or delete, plus the
<a href="/usage/actions/">actions</a> a rollout would run. To see the full rendered manifests
without comparing against the cluster, use <a href="/cli/build/"><code>build</code></a>.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">stagesetctl diff NAME [flags]
</span></span></code></pre></div><table>
	<thead>
			<tr>
					<th>Flag</th>
					<th>Default</th>
					<th>Description</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td><code>--stage</code></td>
					<td><em>(all)</em></td>
					<td>Diff only the named stage(s); repeatable.</td>
			</tr>
			<tr>
					<td><code>--source-dir</code></td>
					<td><em>(none)</em></td>
					<td>Use a local artifact tree as <code>[STAGE=]PATH</code>; repeatable. Skips the cluster fetch.</td>
			</tr>
			<tr>
					<td><code>--server-side</code></td>
					<td><code>true</code></td>
					<td>Server-side dry-run apply diff (needs update/patch RBAC). <code>false</code> renders client-side against live objects.</td>
			</tr>
			<tr>
					<td><code>--as-tenant</code></td>
					<td><code>false</code></td>
					<td>Render and dry-run impersonating <code>spec.serviceAccountName</code> (see <a href="/usage/multi-cluster/">multi-cluster and tenancy</a>).</td>
			</tr>
			<tr>
					<td><code>--show-secrets</code></td>
					<td><code>false</code></td>
					<td>Reveal Secret values instead of masking.</td>
			</tr>
			<tr>
					<td><code>--show-unchanged</code></td>
					<td><code>false</code></td>
					<td>Include objects with no change.</td>
			</tr>
			<tr>
					<td><code>--prune</code></td>
					<td><code>true</code></td>
					<td>Show resources that would be deleted (fell out of inventory).</td>
			</tr>
			<tr>
					<td><code>--color</code></td>
					<td><code>auto</code></td>
					<td>Colorize output: <code>auto</code>, <code>always</code>, or <code>never</code>.</td>
			</tr>
			<tr>
					<td><code>--exit-code</code></td>
					<td><code>true</code></td>
					<td>Exit <code>1</code> when changes are found. <code>false</code> always exits <code>0</code> on a clean run.</td>
			</tr>
	</tbody>
</table>
<h2 id="example">Example</h2>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">stagesetctl diff payments
</span></span></code></pre></div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">--- live
</span></span><span class="line"><span class="cl">+++ merged
</span></span><span class="line"><span class="cl">@@ Deployment payments/web @@
</span></span><span class="line"><span class="cl"> spec:
</span></span><span class="line"><span class="cl">-  replicas: 3
</span></span><span class="line"><span class="cl">+  replicas: 6
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">- ConfigMap payments/old-feature-flags (pruned: fell out of inventory)
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">Actions to run:
</span></span><span class="line"><span class="cl">  application:
</span></span><span class="line"><span class="cl">    pre   db-migrate   job ledger-migrations
</span></span><span class="line"><span class="cl">    post  smoke-test   http https://payments.internal/healthz
</span></span></code></pre></div><p>Objects that left the stage&rsquo;s <a href="/api/stageinventory/">inventory</a> show as deletions
(<code>pruned: …</code>); pass <code>--prune=false</code> to hide them. The trailing <code>Actions to run</code>
block lists the <a href="/usage/actions/">pre/post/onFailure actions</a> a real reconcile
would execute — <code>diff</code> never runs them, it only reports them.</p>
<p>A clean run prints nothing and exits <code>0</code>; pending changes exit <code>1</code>. To inspect
without failing the shell:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">stagesetctl diff payments --color<span class="o">=</span>never --exit-code<span class="o">=</span><span class="nb">false</span>
</span></span></code></pre></div><p>Use <code>--server-side=false</code> when you lack apply RBAC and only need a textual
render-versus-live comparison.</p>
]]></content><category scheme="https://stageset.projects.metio.wtf/tags/cli" term="cli" label="cli"/><category scheme="https://stageset.projects.metio.wtf/tags/stages" term="stages" label="stages"/><category scheme="https://stageset.projects.metio.wtf/tags/ci" term="ci" label="ci"/></entry><entry><title type="html">stagesetctl get</title><link href="https://stageset.projects.metio.wtf/cli/get/?utm_source=atom_feed" rel="alternate" type="text/html"/><link href="https://stageset.projects.metio.wtf/cli/reconcile/?utm_source=atom_feed" rel="related" type="text/html" title="stagesetctl reconcile"/><link href="https://stageset.projects.metio.wtf/cli/build/?utm_source=atom_feed" rel="related" type="text/html" title="stagesetctl build"/><link href="https://stageset.projects.metio.wtf/cli/diff/?utm_source=atom_feed" rel="related" type="text/html" title="stagesetctl diff"/><link href="https://stageset.projects.metio.wtf/usage/actions/?utm_source=atom_feed" rel="related" type="text/html" title="Actions"/><link href="https://stageset.projects.metio.wtf/usage/conflict-policies/?utm_source=atom_feed" rel="related" type="text/html" title="Conflict policies"/><id>https://stageset.projects.metio.wtf/cli/get/</id><published>0001-01-01T00:00:00+00:00</published><updated>2026-06-16T13:41:17+02:00</updated><content type="html"><![CDATA[<blockquote>Print human-readable StageSet status, or list StageSets.</blockquote><p>With no <code>NAME</code>, lists StageSets in the current namespace. With a <code>NAME</code>, prints that
StageSet&rsquo;s detail (Ready reason, per-stage phase, revisions, version) — a readable
view of <a href="/api/stageset/#status"><code>StageSet.status</code></a>.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">stagesetctl get [NAME] [flags]
</span></span></code></pre></div><table>
	<thead>
			<tr>
					<th>Flag</th>
					<th>Default</th>
					<th>Description</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td><code>-A</code>, <code>--all-namespaces</code></td>
					<td><code>false</code></td>
					<td>List StageSets across all namespaces.</td>
			</tr>
			<tr>
					<td><code>-o</code>, <code>--output</code></td>
					<td><em>(table)</em></td>
					<td>Output format: empty for the human table, or <code>yaml</code> / <code>json</code>.</td>
			</tr>
	</tbody>
</table>
<h2 id="listing">Listing</h2>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">stagesetctl get -A
</span></span></code></pre></div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">NAMESPACE   NAME       READY   REASON       STAGES   VERSION   PENDING
</span></span><span class="line"><span class="cl">payments    payments   True    Succeeded    2/2      2.1.0     -
</span></span><span class="line"><span class="cl">platform    platform   True    Succeeded    3/3      -         -
</span></span><span class="line"><span class="cl">staging     web        False   StageFailed  1/2      -         -
</span></span></code></pre></div><p><code>STAGES</code> is <code>ready/total</code>; <code>PENDING</code> shows <code>held until &lt;time&gt;</code> when an
<a href="/usage/update-windows/">update window</a> is holding a rollout. A <code>False</code> <code>READY</code>
maps to a <a href="/runbooks/">runbook</a> by its <code>REASON</code>.</p>
<h2 id="detail">Detail</h2>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">stagesetctl get payments -n payments
</span></span></code></pre></div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">Name:       payments
</span></span><span class="line"><span class="cl">Namespace:  payments
</span></span><span class="line"><span class="cl">Ready:      True (Succeeded)
</span></span><span class="line"><span class="cl">Message:    All 2 stages applied
</span></span><span class="line"><span class="cl">Version:    2.1.0
</span></span><span class="line"><span class="cl">Last handled reconcile: 2026-06-15T09:21:04Z
</span></span><span class="line"><span class="cl">Stages:
</span></span><span class="line"><span class="cl">  NAME            PHASE   REVISION        ENTRIES
</span></span><span class="line"><span class="cl">  infrastructure  Ready   sha256:9f3c1a   12
</span></span><span class="line"><span class="cl">  application     Ready   sha256:1a2b3c   8
</span></span></code></pre></div><p>Conditional lines fill in when the StageSet is in that state: <code>Suspended: true</code>
when <a href="/api/stageset/#scheduling"><code>spec.suspend</code></a> is set, <code>Pending migrations:</code>
when a <a href="/usage/versioned-migrations/">version boundary</a> is queued, and a
<code>Pending update:</code> block (next-window time plus the held revisions) when an
<a href="/usage/update-windows/">update window</a> is holding a rollout — for example:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">Ready:      False (UpdateDeferred)
</span></span><span class="line"><span class="cl">Pending update:
</span></span><span class="line"><span class="cl">  Next window opens: 2026-06-16T08:00:00Z
</span></span><span class="line"><span class="cl">  Held: payments/payments-app -&gt; sha256:cafe
</span></span></code></pre></div><p>Add <code>-o yaml</code> (or <code>-o json</code>) to print the full object instead of the summary — the
machine-readable form for scripting or piping into <code>jq</code>/<code>yq</code>.</p>
]]></content><category scheme="https://stageset.projects.metio.wtf/tags/cli" term="cli" label="cli"/><category scheme="https://stageset.projects.metio.wtf/tags/stages" term="stages" label="stages"/><category scheme="https://stageset.projects.metio.wtf/tags/operations" term="operations" label="operations"/></entry><entry><title type="html">stagesetctl reconcile</title><link href="https://stageset.projects.metio.wtf/cli/reconcile/?utm_source=atom_feed" rel="alternate" type="text/html"/><link href="https://stageset.projects.metio.wtf/cli/get/?utm_source=atom_feed" rel="related" type="text/html" title="stagesetctl get"/><link href="https://stageset.projects.metio.wtf/cli/build/?utm_source=atom_feed" rel="related" type="text/html" title="stagesetctl build"/><link href="https://stageset.projects.metio.wtf/cli/diff/?utm_source=atom_feed" rel="related" type="text/html" title="stagesetctl diff"/><link href="https://stageset.projects.metio.wtf/usage/actions/?utm_source=atom_feed" rel="related" type="text/html" title="Actions"/><link href="https://stageset.projects.metio.wtf/usage/conflict-policies/?utm_source=atom_feed" rel="related" type="text/html" title="Conflict policies"/><id>https://stageset.projects.metio.wtf/cli/reconcile/</id><published>0001-01-01T00:00:00+00:00</published><updated>2026-06-16T13:41:17+02:00</updated><content type="html"><![CDATA[<blockquote>Force an out-of-band reconcile, optionally waiting for it to be handled.</blockquote><p>Stamps the <code>reconcile.fluxcd.io/requestedAt</code>
<a href="/api/stageinventory/#well-known-labels-and-annotations">annotation</a> to trigger a
reconcile now, optionally waiting for the controller to report it handled.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">stagesetctl reconcile NAME [flags]
</span></span></code></pre></div><table>
	<thead>
			<tr>
					<th>Flag</th>
					<th>Default</th>
					<th>Description</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td><code>--stage</code></td>
					<td><em>(all)</em></td>
					<td>Force only this stage to re-run its actions (single-stage reconcile).</td>
			</tr>
			<tr>
					<td><code>--with-source</code></td>
					<td><code>false</code></td>
					<td>Also re-request the stage sources before reconciling.</td>
			</tr>
			<tr>
					<td><code>--update-now</code></td>
					<td><code>false</code></td>
					<td>Apply a window-held rollout immediately, bypassing update windows.</td>
			</tr>
			<tr>
					<td><code>--force</code></td>
					<td><code>false</code></td>
					<td>Proceed even when the StageSet is suspended.</td>
			</tr>
			<tr>
					<td><code>--wait</code></td>
					<td><code>false</code></td>
					<td>Block until the controller reports the request handled.</td>
			</tr>
			<tr>
					<td><code>--timeout</code></td>
					<td><code>5m</code></td>
					<td>How long to wait with <code>--wait</code>.</td>
			</tr>
	</tbody>
</table>
<h2 id="example">Example</h2>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">stagesetctl reconcile payments -n payments
</span></span></code></pre></div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">Reconcile requested for StageSet payments (token 2026-06-15T09:30:00Z)
</span></span></code></pre></div><p>Force just one stage to re-run its actions:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">stagesetctl reconcile payments --stage application
</span></span></code></pre></div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">Reconcile requested for stage &#34;application&#34; of StageSet payments (token 2026-06-15T09:31:12Z)
</span></span></code></pre></div><p>Re-pull sources, push a window-held rollout through, and wait for it:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">stagesetctl reconcile payments --with-source --update-now --wait --timeout 10m
</span></span></code></pre></div><p><code>--update-now</code> is the CLI equivalent of the <code>stages.metio.wtf/update-now</code>
annotation — the supported escape hatch when an <a href="/usage/update-windows/">update
window</a> is holding a rollout you need to ship now.</p>
]]></content><category scheme="https://stageset.projects.metio.wtf/tags/cli" term="cli" label="cli"/><category scheme="https://stageset.projects.metio.wtf/tags/stages" term="stages" label="stages"/><category scheme="https://stageset.projects.metio.wtf/tags/operations" term="operations" label="operations"/></entry><entry><title type="html">Stalled</title><link href="https://stageset.projects.metio.wtf/runbooks/stalled/?utm_source=atom_feed" rel="alternate" type="text/html"/><link href="https://stageset.projects.metio.wtf/runbooks/dependencynotready/?utm_source=atom_feed" rel="related" type="text/html" title="DependencyNotReady"/><link href="https://stageset.projects.metio.wtf/runbooks/stagefailed/?utm_source=atom_feed" rel="related" type="text/html" title="StageFailed"/><link href="https://stageset.projects.metio.wtf/runbooks/artifactnotfound/?utm_source=atom_feed" rel="related" type="text/html" title="ArtifactNotFound"/><link href="https://stageset.projects.metio.wtf/runbooks/controller-pod-down/?utm_source=atom_feed" rel="related" type="text/html" title="Controller pod down"/><link href="https://stageset.projects.metio.wtf/runbooks/downgraderequiresmigration/?utm_source=atom_feed" rel="related" type="text/html" title="DowngradeRequiresMigration"/><id>https://stageset.projects.metio.wtf/runbooks/stalled/</id><published>0001-01-01T00:00:00+00:00</published><updated>2026-06-16T13:41:17+02:00</updated><content type="html"><![CDATA[<blockquote>The run cannot make progress and will not retry until the spec changes.</blockquote><h2 id="symptom">Symptom</h2>
<p><code>READY=False</code>, <code>REASON=Stalled</code>. Terminal: the controller does not requeue until the spec changes.</p>
<h2 id="cause">Cause</h2>
<p>A condition that retrying cannot clear. Currently this is a <strong><code>spec.dependsOn</code> cycle</strong> — two or more StageSets depend on each other (directly or transitively), so none can ever become Ready first. The cycle is detected by a breadth-first walk over the <code>dependsOn</code> graph. A dependency that is merely not Ready yet (no cycle) reports <a href="/runbooks/dependencynotready/"><code>DependencyNotReady</code></a> instead.</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 stageset &lt;name&gt; -n &lt;namespace&gt;     <span class="c1"># Message states &#34;spec.dependsOn forms a cycle&#34;</span>
</span></span><span class="line"><span class="cl"><span class="c1"># Trace the edges:</span>
</span></span><span class="line"><span class="cl">kubectl get stageset -n &lt;namespace&gt; <span class="se">\
</span></span></span><span class="line"><span class="cl">  -o custom-columns<span class="o">=</span>NAME:.metadata.name,DEPENDSON:.spec.dependsOn<span class="o">[</span>*<span class="o">]</span>.name
</span></span></code></pre></div><p>Follow the <code>dependsOn</code> names until you find the loop (A → B → A, or longer).</p>
<h2 id="remediation">Remediation</h2>
<p>Break the cycle by removing one edge — drop a <code>dependsOn</code> entry from one StageSet, or restructure so the ordering is a strict chain. Dependencies must form a directed acyclic graph. After the edit, the next reconcile re-walks the graph and clears the condition.</p>
]]></content><category scheme="https://stageset.projects.metio.wtf/tags/runbooks" term="runbooks" label="runbooks"/><category scheme="https://stageset.projects.metio.wtf/tags/stages" term="stages" label="stages"/><category scheme="https://stageset.projects.metio.wtf/tags/troubleshooting" term="troubleshooting" label="troubleshooting"/></entry><entry><title type="html">Succeeded</title><link href="https://stageset.projects.metio.wtf/runbooks/succeeded/?utm_source=atom_feed" rel="alternate" type="text/html"/><link href="https://stageset.projects.metio.wtf/runbooks/dependencynotready/?utm_source=atom_feed" rel="related" type="text/html" title="DependencyNotReady"/><link href="https://stageset.projects.metio.wtf/usage/ready-checks/?utm_source=atom_feed" rel="related" type="text/html" title="Ready checks"/><link href="https://stageset.projects.metio.wtf/runbooks/stagefailed/?utm_source=atom_feed" rel="related" type="text/html" title="StageFailed"/><link href="https://stageset.projects.metio.wtf/runbooks/stalled/?utm_source=atom_feed" rel="related" type="text/html" title="Stalled"/><link href="https://stageset.projects.metio.wtf/usage/actions/?utm_source=atom_feed" rel="related" type="text/html" title="Actions"/><id>https://stageset.projects.metio.wtf/runbooks/succeeded/</id><published>0001-01-01T00:00:00+00:00</published><updated>2026-06-16T13:41:17+02:00</updated><content type="html"><![CDATA[<blockquote>All stages applied and verified the healthy steady state.</blockquote><h2 id="symptom">Symptom</h2>
<p><code>READY=True</code>, <code>REASON=Succeeded</code>. The Message names the applied revisions.</p>
<h2 id="cause">Cause</h2>
<p>This is the healthy steady state: every stage&rsquo;s artifact resolved, built, applied, and passed its readiness checks, and <code>status.lastAppliedRevisions</code> matches <code>status.lastAttemptedRevisions</code>.</p>
<h2 id="remediation">Remediation</h2>
<p>Nothing to remediate.</p>
<ul>
<li>The controller keeps reconciling at <code>spec.interval</code>; a re-render upstream (a new <code>ExternalArtifact</code> revision) re-applies automatically and the condition stays <code>Succeeded</code> once the new revision converges.</li>
<li><code>status.stages[]</code> reports per-stage <code>appliedRevision</code> and inventory entry counts to confirm what each stage owns.</li>
</ul>
]]></content><category scheme="https://stageset.projects.metio.wtf/tags/runbooks" term="runbooks" label="runbooks"/><category scheme="https://stageset.projects.metio.wtf/tags/stages" term="stages" label="stages"/><category scheme="https://stageset.projects.metio.wtf/tags/health" term="health" label="health"/></entry><entry><title type="html">Suspended</title><link href="https://stageset.projects.metio.wtf/runbooks/suspended/?utm_source=atom_feed" rel="alternate" type="text/html"/><link href="https://stageset.projects.metio.wtf/runbooks/controller-pod-down/?utm_source=atom_feed" rel="related" type="text/html" title="Controller pod down"/><link href="https://stageset.projects.metio.wtf/runbooks/webhook-cert-renewal/?utm_source=atom_feed" rel="related" type="text/html" title="Webhook cert renewal failing"/><link href="https://stageset.projects.metio.wtf/runbooks/artifactnotfound/?utm_source=atom_feed" rel="related" type="text/html" title="ArtifactNotFound"/><link href="https://stageset.projects.metio.wtf/runbooks/dependencynotready/?utm_source=atom_feed" rel="related" type="text/html" title="DependencyNotReady"/><link href="https://stageset.projects.metio.wtf/runbooks/downgraderequiresmigration/?utm_source=atom_feed" rel="related" type="text/html" title="DowngradeRequiresMigration"/><id>https://stageset.projects.metio.wtf/runbooks/suspended/</id><published>0001-01-01T00:00:00+00:00</published><updated>2026-06-16T13:41:17+02:00</updated><content type="html"><![CDATA[<blockquote>Reconciliation is paused via spec.suspend.</blockquote><h2 id="symptom">Symptom</h2>
<p><code>READY=False</code>, <code>REASON=Suspended</code>.</p>
<h2 id="cause">Cause</h2>
<p><code>spec.suspend: true</code> is set, so the controller short-circuits before any resolution, build, or apply. This is an intentional operator action, not a failure — applied objects are left exactly as they were at the last successful run.</p>
<h2 id="remediation">Remediation</h2>
<p>Resume by clearing the flag:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">kubectl patch stageset &lt;name&gt; -n &lt;namespace&gt; --type<span class="o">=</span>merge -p <span class="s1">&#39;{&#34;spec&#34;:{&#34;suspend&#34;:false}}&#39;</span>
</span></span></code></pre></div><p>The next reconcile picks up from the current artifact revisions.</p>
]]></content><category scheme="https://stageset.projects.metio.wtf/tags/runbooks" term="runbooks" label="runbooks"/><category scheme="https://stageset.projects.metio.wtf/tags/operations" term="operations" label="operations"/><category scheme="https://stageset.projects.metio.wtf/tags/troubleshooting" term="troubleshooting" label="troubleshooting"/></entry><entry><title type="html">Update windows</title><link href="https://stageset.projects.metio.wtf/usage/update-windows/?utm_source=atom_feed" rel="alternate" type="text/html"/><link href="https://stageset.projects.metio.wtf/runbooks/updatedeferred/?utm_source=atom_feed" rel="related" type="text/html" title="UpdateDeferred"/><link href="https://stageset.projects.metio.wtf/usage/actions/?utm_source=atom_feed" rel="related" type="text/html" title="Actions"/><link href="https://stageset.projects.metio.wtf/usage/conflict-policies/?utm_source=atom_feed" rel="related" type="text/html" title="Conflict policies"/><link href="https://stageset.projects.metio.wtf/runbooks/dependencynotready/?utm_source=atom_feed" rel="related" type="text/html" title="DependencyNotReady"/><link href="https://stageset.projects.metio.wtf/tutorials/jsonnet-to-rollout/?utm_source=atom_feed" rel="related" type="text/html" title="From Jsonnet to a gated rollout"/><id>https://stageset.projects.metio.wtf/usage/update-windows/</id><published>0001-01-01T00:00:00+00:00</published><updated>2026-06-16T13:41:17+02:00</updated><content type="html"><![CDATA[<blockquote>Allow or deny rollouts on a schedule, without pausing reconciliation.</blockquote><p>Update windows gate <em>when</em> new artifact revisions roll out, without pausing
reconciliation. Drift correction keeps running; only the rollout of a <em>new</em>
revision is held until a window allows it.</p>
<h2 id="deny-a-recurring-window">Deny a recurring window</h2>
<p>Freeze rollouts during business hours:</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">stages</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">app</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">name</span><span class="p">:</span><span class="w"> </span><span class="l">my-app</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">updateWindows</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">Deny</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">schedule</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;0 9 * * MON-FRI&#34;</span><span class="w">   </span><span class="c"># 5-field cron: start of the window</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">duration</span><span class="p">:</span><span class="w"> </span><span class="l">8h</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">timeZone</span><span class="p">:</span><span class="w"> </span><span class="l">Europe/Berlin</span><span class="w">
</span></span></span></code></pre></div><p>A new revision that arrives inside the window is held; <code>status.pendingUpdate</code>
records what is waiting and <code>nextWindowOpens</code> when it will ship. The controller
emits an <code>UpdateDeferred</code> event and increments <code>stageset_update_deferred_total</code>.</p>
<h2 id="allow-list-windows">Allow-list windows</h2>
<p>If any <code>Allow</code> window exists, rollouts happen <strong>only</strong> inside an active Allow with
no active Deny — <code>Deny</code> always wins. This expresses &ldquo;only deploy on Tuesday and
Thursday afternoons&rdquo;:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="w">  </span><span class="nt">updateWindows</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">Allow</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">schedule</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;0 14 * * TUE,THU&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">duration</span><span class="p">:</span><span class="w"> </span><span class="l">3h</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">timeZone</span><span class="p">:</span><span class="w"> </span><span class="l">America/New_York</span><span class="w">
</span></span></span></code></pre></div><h2 id="a-one-off-freeze">A one-off freeze</h2>
<p>Absolute windows use <code>from</code>/<code>to</code> instead of a schedule — for a planned event
freeze:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="w">  </span><span class="nt">updateWindows</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">Deny</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="ld">2026-12-24T00:00:00Z</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 class="ld">2026-12-27T00:00:00Z</span><span class="w">
</span></span></span></code></pre></div><h2 id="what-a-closed-window-blocks">What a closed window blocks</h2>
<p><code>windowScope</code> controls what a closed window holds back:</p>
<ul>
<li><strong><code>Updates</code></strong> (default) — hold only the rollout of a <em>new</em> artifact revision.
Drift correction keeps re-applying the pinned state, so the live cluster stays
on its last-approved revision but doesn&rsquo;t fall out of sync.</li>
<li><strong><code>All</code></strong> — a hard freeze: also pause drift correction, so the controller
applies nothing at all while the window is closed.</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="w">  </span><span class="nt">windowScope: Updates   # default</span><span class="p">:</span><span class="w"> </span><span class="l">hold new revisions, keep correcting drift</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="c"># windowScope: All     # hard freeze: also pause drift correction</span><span class="w">
</span></span></span></code></pre></div><h2 id="shipping-anyway">Shipping anyway</h2>
<p>To push a held rollout through immediately, override the window with
<a href="/cli/"><code>stagesetctl</code></a>:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">stagesetctl reconcile my-app --update-now
</span></span></code></pre></div><p>This stamps the <code>stages.metio.wtf/update-now</code> annotation; the honored value is
recorded in <code>status.lastHandledUpdateOverride</code>.</p>
]]></content><category scheme="https://stageset.projects.metio.wtf/tags/update-windows" term="update-windows" label="update-windows"/><category scheme="https://stageset.projects.metio.wtf/tags/scheduling" term="scheduling" label="scheduling"/><category scheme="https://stageset.projects.metio.wtf/tags/stages" term="stages" label="stages"/></entry><entry><title type="html">UpdateDeferred</title><link href="https://stageset.projects.metio.wtf/runbooks/updatedeferred/?utm_source=atom_feed" rel="alternate" type="text/html"/><link href="https://stageset.projects.metio.wtf/runbooks/artifactnotfound/?utm_source=atom_feed" rel="related" type="text/html" title="ArtifactNotFound"/><link href="https://stageset.projects.metio.wtf/runbooks/controller-pod-down/?utm_source=atom_feed" rel="related" type="text/html" title="Controller pod down"/><link href="https://stageset.projects.metio.wtf/runbooks/dependencynotready/?utm_source=atom_feed" rel="related" type="text/html" title="DependencyNotReady"/><link href="https://stageset.projects.metio.wtf/runbooks/downgraderequiresmigration/?utm_source=atom_feed" rel="related" type="text/html" title="DowngradeRequiresMigration"/><link href="https://stageset.projects.metio.wtf/runbooks/invalidspec/?utm_source=atom_feed" rel="related" type="text/html" title="InvalidSpec"/><id>https://stageset.projects.metio.wtf/runbooks/updatedeferred/</id><published>0001-01-01T00:00:00+00:00</published><updated>2026-06-16T13:41:17+02:00</updated><content type="html"><![CDATA[<blockquote>A new revision is held by a closed update window.</blockquote><h2 id="symptom">Symptom</h2>
<p><code>READY=False</code>, <code>REASON=UpdateDeferred</code> (initial deploy held), or <code>READY=True</code> with a message noting a deferral and a populated <code>status.pendingUpdate</code> (an already-deployed StageSet with a held update).</p>
<h2 id="cause">Cause</h2>
<p>This is <strong>not a failure</strong> — it is time-based delivery working as configured. A new revision (or the first deploy) is being held because the StageSet&rsquo;s <a href="/usage/update-windows/"><code>spec.updateWindows</code></a> do not currently permit a rollout: either a <code>Deny</code> window is active, or <code>Allow</code> windows are declared and none is active right now. With <code>spec.windowScope: All</code>, even drift correction is paused while a window is closed.</p>
<p><code>status.pendingUpdate</code> shows the held revisions and <code>nextWindowOpens</code> (when delivery resumes); the controller requeues at that boundary.</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 stageset &lt;name&gt; -n &lt;namespace&gt; -o <span class="nv">jsonpath</span><span class="o">=</span><span class="s1">&#39;{.status.pendingUpdate}&#39;</span>
</span></span><span class="line"><span class="cl">kubectl get stageset &lt;name&gt; -n &lt;namespace&gt; -o <span class="nv">jsonpath</span><span class="o">=</span><span class="s1">&#39;{.spec.updateWindows}&#39;</span>
</span></span></code></pre></div><p>Confirm the current time (in each window&rsquo;s <code>timeZone</code>) against the windows. An already-deployed StageSet stays <code>Ready=True</code> — the deployed version keeps running while the update waits.</p>
<h2 id="remediation">Remediation</h2>
<p>Usually none — the update applies automatically when the next window opens. If you need it sooner:</p>
<ul>
<li>
<p><strong>Force it through once</strong> (e.g. an emergency fix during a freeze):</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">kubectl annotate --overwrite stageset &lt;name&gt; -n &lt;namespace&gt; <span class="se">\
</span></span></span><span class="line"><span class="cl">  stages.metio.wtf/update-now<span class="o">=</span><span class="s2">&#34;</span><span class="k">$(</span>date +%s<span class="k">)</span><span class="s2">&#34;</span>
</span></span></code></pre></div><p>This applies the held rollout immediately, regardless of windows (one-shot per annotation value).</p>
</li>
<li>
<p><strong>Adjust the windows</strong> if the schedule is wrong — check <code>type</code> (Allow vs Deny), the cron <code>schedule</code>/<code>duration</code> or absolute <code>from</code>/<code>to</code>, and especially the <code>timeZone</code>.</p>
</li>
</ul>
]]></content><category scheme="https://stageset.projects.metio.wtf/tags/runbooks" term="runbooks" label="runbooks"/><category scheme="https://stageset.projects.metio.wtf/tags/update-windows" term="update-windows" label="update-windows"/><category scheme="https://stageset.projects.metio.wtf/tags/scheduling" term="scheduling" label="scheduling"/><category scheme="https://stageset.projects.metio.wtf/tags/troubleshooting" term="troubleshooting" label="troubleshooting"/></entry><entry><title type="html">Versioned migrations</title><link href="https://stageset.projects.metio.wtf/usage/versioned-migrations/?utm_source=atom_feed" rel="alternate" type="text/html"/><link href="https://stageset.projects.metio.wtf/runbooks/downgraderequiresmigration/?utm_source=atom_feed" rel="related" type="text/html" title="DowngradeRequiresMigration"/><link href="https://stageset.projects.metio.wtf/usage/actions/?utm_source=atom_feed" rel="related" type="text/html" title="Actions"/><link href="https://stageset.projects.metio.wtf/runbooks/invalidversion/?utm_source=atom_feed" rel="related" type="text/html" title="InvalidVersion"/><link href="https://stageset.projects.metio.wtf/usage/rollback/?utm_source=atom_feed" rel="related" type="text/html" title="Rollback"/><link href="https://stageset.projects.metio.wtf/runbooks/stagefailed/?utm_source=atom_feed" rel="related" type="text/html" title="StageFailed"/><id>https://stageset.projects.metio.wtf/usage/versioned-migrations/</id><published>0001-01-01T00:00:00+00:00</published><updated>2026-06-16T13:41:17+02:00</updated><content type="html"><![CDATA[<blockquote>Run migrations once, when the deployed version crosses a release boundary.</blockquote><p>Some changes only need to happen once, when you cross a release boundary — a
one-time data backfill on the way to 2.0, a schema conversion between 1.x and 2.x.
Versioned migrations run a ladder of <a href="/usage/actions/">actions</a> exactly when the
deployed version steps over the boundary, and never again.</p>
<p>Versioning is off until you set <code>spec.version</code>.</p>
<h2 id="declaring-the-version">Declaring the version</h2>
<p>The controller needs to know <em>what version is currently being deployed</em>. There are
three ways to declare it; pick by <strong>where the version lives</strong>.</p>
<table>
	<thead>
			<tr>
					<th>Source</th>
					<th>The version lives…</th>
					<th>Best for</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td><a href="#inline--versionvalue"><code>version.value</code></a></td>
					<td>on the <code>StageSet</code></td>
					<td>environment-pinned versions, quick starts</td>
			</tr>
			<tr>
					<td><a href="#from-a-rendered-object--versionfromobject"><code>version.fromObject</code></a></td>
					<td>inside the manifests</td>
					<td><strong>any source, including JaaS</strong> — the recommended default</td>
			</tr>
			<tr>
					<td><a href="#from-a-file-in-the-artifact--versionfromartifact"><code>version.fromArtifact</code></a></td>
					<td>a file in the artifact</td>
					<td>Git/OCI/Bucket sources that can ship a <code>VERSION</code> file</td>
			</tr>
	</tbody>
</table>
<p>Whichever you choose, the resolved value is trimmed and parsed as semver (a leading
<code>v</code> is accepted). A missing stage/object/file, an empty value, or an unparseable
one fails terminally with the <code>InvalidVersion</code> reason (see its
<a href="/runbooks/invalidversion/">runbook</a>) — a half-versioned system is worse than an
unversioned one.</p>
<h3 id="inline--versionvalue">Inline — <code>version.value</code></h3>
<p>The <code>StageSet</code> author pins the version directly. Use this when the version is a
property of the environment rather than of the content, or to get started quickly:</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">version</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">value</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;2.1.0&#34;</span><span class="w">      </span><span class="c"># bump this when you cut a release</span><span class="w">
</span></span></span></code></pre></div><p>The trade-off: the version is declared here, not carried by the content, so you
bump it by editing the <code>StageSet</code>.</p>
<h3 id="from-a-rendered-object--versionfromobject">From a rendered object — <code>version.fromObject</code></h3>
<p>The recommended way to let the version travel with the content.
<a href="https://kubernetes.io/docs/">Kubernetes</a> has a standard place for an
application&rsquo;s version: the <code>app.kubernetes.io/version</code> label. Well-formed manifests
set it, so the version is already inside the manifests — <code>fromObject</code> reads it back.
This works for every source kind, including a single-document renderer like
<a href="https://jaas.projects.metio.wtf/">JaaS</a> that has no room for a separate file.</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">version</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">fromObject</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">stage</span><span class="p">:</span><span class="w"> </span><span class="l">app           </span><span class="w"> </span><span class="c"># which stage&#39;s rendered manifests carry it</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">Deployment     </span><span class="w"> </span><span class="c"># the object to read</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">web</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="c"># fieldPath omitted → reads metadata.labels[&#39;app.kubernetes.io/version&#39;]</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">stages</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">app</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">name</span><span class="p">:</span><span class="w"> </span><span class="l">my-app</span><span class="w">
</span></span></span></code></pre></div><p>The controller builds the <code>app</code> stage&rsquo;s manifests (the same render it applies),
finds the <code>Deployment/web</code> object, and reads its <code>app.kubernetes.io/version</code> label.
Because the label is part of the manifests, the version changes in lockstep with
the content — no second file to keep in sync.</p>
<p><strong>Reading a different field.</strong> Set <code>fieldPath</code> to a kubectl-style JSONPath that
resolves to the bare version string. (It must be the version <em>only</em>; a JSONPath
can&rsquo;t split an <code>image: web:2.1.0</code> value, so prefer the label.) <code>apiVersion</code> is
optional and narrows the match when a <code>Kind</code>+<code>Name</code> pair would be ambiguous:</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">version</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">fromObject</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">stage</span><span class="p">:</span><span class="w"> </span><span class="l">app</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">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">ConfigMap</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">app-meta</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">fieldPath</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;{.data.version}&#34;</span><span class="w">   </span><span class="c"># must resolve to a bare semver, e.g. 2.1.0</span><span class="w">
</span></span></span></code></pre></div><p>This is the path the <a href="/tutorials/jsonnet-to-rollout/">Jsonnet-to-rollout tutorial</a>
uses: the snippet renders the version into the manifest&rsquo;s version label, and the
StageSet reads it straight back.</p>
<h3 id="from-a-file-in-the-artifact--versionfromartifact">From a file in the artifact — <code>version.fromArtifact</code></h3>
<p>The version travels with the content as a <strong>dedicated file</strong> containing a single
semver. This fits <strong>Git/OCI/Bucket</strong> sources, where you can ship an extra file
beside the manifests. (It does <em>not</em> fit JaaS <code>rendered</code> output, which is a single
<code>rendered.json</code>; use <code>fromObject</code> there.)</p>
<p><strong>Who writes it, and where:</strong> the artifact&rsquo;s producer. For a Git source, commit a
<code>VERSION</code> file in the repo; for an OCI/Bucket artifact, include it in the pushed
tree. The file lives at <code>path</code> inside the named stage&rsquo;s artifact, relative to the
artifact root:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl"># VERSION — committed alongside the manifests it versions
</span></span><span class="line"><span class="cl">2.1.0
</span></span></code></pre></div><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">version</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">fromArtifact</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">stage</span><span class="p">:</span><span class="w"> </span><span class="l">app         </span><span class="w"> </span><span class="c"># which stage&#39;s artifact carries the file</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">VERSION      </span><span class="w"> </span><span class="c"># the file&#39;s path inside that artifact (cleaned; no leading ./)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">stages</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">app</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">my-app</span><span class="w">
</span></span></span></code></pre></div><p>The controller fetches the <code>app</code> stage&rsquo;s artifact and reads the file at <code>path</code>.</p>
<h2 id="declaring-migrations">Declaring migrations</h2>
<p>Each migration names the boundary it crosses (<code>to</code>, optionally <code>from</code>), the stage
it anchors before, and the actions to run:</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">version</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">fromArtifact</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">stage</span><span class="p">:</span><span class="w"> </span><span class="l">app</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">VERSION</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">migrations</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">backfill-ledger-2-0</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="s2">&#34;1.*&#34;</span><span class="w">               </span><span class="c"># optional: only when coming from a 1.x</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 class="s2">&#34;2.0.0&#34;</span><span class="w">             </span><span class="c"># the boundary this migration crosses</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">stage</span><span class="p">:</span><span class="w"> </span><span class="l">app              </span><span class="w"> </span><span class="c"># runs before this stage&#39;s pre-actions</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">actions</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">backfill</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">job</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">name</span><span class="p">:</span><span class="w"> </span><span class="l">ledger-backfill-job</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">stages</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">app</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">name</span><span class="p">:</span><span class="w"> </span><span class="l">my-app</span><span class="w">
</span></span></span></code></pre></div><p>When the deployed version crosses from a <code>1.x</code> into <code>2.0.0</code>, the <code>backfill</code> job
runs once, anchored before the <code>app</code> stage. The controller tracks progress so a
retry doesn&rsquo;t re-run a completed migration:</p>
<ul>
<li><code>status.version</code> — the deployed version, written only after a fully successful
run.</li>
<li><code>status.pendingMigrations</code> — migrations the next run will execute.</li>
<li><code>status.executedMigrations</code> — the in-flight ledger for the current transition.</li>
</ul>
<p>Migrations emit <code>MigrationStarted</code> / <code>MigrationCompleted</code> events (and
<code>MigrationFailed</code> on error). A downgrade that would skip a required migration is
refused with the <code>DowngradeRequiresMigration</code> reason — see its
<a href="/runbooks/downgraderequiresmigration/">runbook</a>.</p>
]]></content><category scheme="https://stageset.projects.metio.wtf/tags/migrations" term="migrations" label="migrations"/><category scheme="https://stageset.projects.metio.wtf/tags/versioning" term="versioning" label="versioning"/><category scheme="https://stageset.projects.metio.wtf/tags/actions" term="actions" label="actions"/></entry><entry><title type="html">Webhook cert renewal failing</title><link href="https://stageset.projects.metio.wtf/runbooks/webhook-cert-renewal/?utm_source=atom_feed" rel="alternate" type="text/html"/><link href="https://stageset.projects.metio.wtf/runbooks/controller-pod-down/?utm_source=atom_feed" rel="related" type="text/html" title="Controller pod down"/><link href="https://stageset.projects.metio.wtf/runbooks/suspended/?utm_source=atom_feed" rel="related" type="text/html" title="Suspended"/><link href="https://stageset.projects.metio.wtf/runbooks/artifactnotfound/?utm_source=atom_feed" rel="related" type="text/html" title="ArtifactNotFound"/><link href="https://stageset.projects.metio.wtf/runbooks/dependencynotready/?utm_source=atom_feed" rel="related" type="text/html" title="DependencyNotReady"/><link href="https://stageset.projects.metio.wtf/runbooks/downgraderequiresmigration/?utm_source=atom_feed" rel="related" type="text/html" title="DowngradeRequiresMigration"/><id>https://stageset.projects.metio.wtf/runbooks/webhook-cert-renewal/</id><published>0001-01-01T00:00:00+00:00</published><updated>2026-06-16T13:41:17+02:00</updated><content type="html"><![CDATA[<blockquote>The self-signed admission webhook certificate is not being rotated.</blockquote><h2 id="symptom">Symptom</h2>
<p><code>stageset_webhook_cert_renewal_failures_total</code> is increasing; the
<code>StageSetWebhookCertRenewalFailing</code> alert fires (see
<a href="/installation/operations/">operations</a> for the alert set and its thresholds).
The current certificate keeps working until its natural expiry — that expiry is
the deadline, after which cluster-wide <code>StageSet</code> admission breaks.</p>
<h2 id="cause">Cause</h2>
<p>Only applies in <code>--webhook-cert-mode=self-signed</code>. The in-pod renewer regenerates
the serving cert every <code>validity/3</code> and patches the
<code>ValidatingWebhookConfiguration</code>&rsquo;s <code>caBundle</code>. It fails when:</p>
<ul>
<li>the controller lost <code>update</code> (or <code>get</code>) on the named
<code>ValidatingWebhookConfiguration</code> (<code>--webhook-validating-config-name</code>),</li>
<li>the VWC was renamed and the flag/<code>resourceNames</code> weren&rsquo;t updated,</li>
<li>the cert directory (<code>--webhook-cert-dir</code>) became read-only.</li>
</ul>
<p>In <code>cert-manager</code> mode this metric is irrelevant — <a href="https://cert-manager.io/">cert-manager</a> owns renewal.</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 -n stageset-system logs deploy/stageset-controller <span class="p">|</span> grep -i <span class="s1">&#39;cert\|renew\|caBundle&#39;</span>
</span></span><span class="line"><span class="cl">kubectl get validatingwebhookconfiguration &lt;name&gt; -o <span class="nv">jsonpath</span><span class="o">=</span><span class="s1">&#39;{.webhooks[*].clientConfig.caBundle}&#39;</span> <span class="p">|</span> head -c <span class="m">40</span>
</span></span></code></pre></div><h2 id="remediation">Remediation</h2>
<ul>
<li>Restore <code>get</code>/<code>update</code> on the named VWC in the controller&rsquo;s ClusterRole
(<code>resourceNames</code> must include it).</li>
<li>Fix the <code>--webhook-validating-config-name</code> / <code>--webhook-cert-dir</code> flags if they
drifted from the deployed VWC and mount.</li>
<li>As a longer-term option, switch to <code>--webhook-cert-mode=cert-manager</code> so renewal
is handled by cert-manager.</li>
</ul>
]]></content><category scheme="https://stageset.projects.metio.wtf/tags/runbooks" term="runbooks" label="runbooks"/><category scheme="https://stageset.projects.metio.wtf/tags/security" term="security" label="security"/><category scheme="https://stageset.projects.metio.wtf/tags/operations" term="operations" label="operations"/><category scheme="https://stageset.projects.metio.wtf/tags/troubleshooting" term="troubleshooting" label="troubleshooting"/></entry><entry><title type="html">Workqueue saturation</title><link href="https://stageset.projects.metio.wtf/runbooks/workqueue-saturation/?utm_source=atom_feed" rel="alternate" type="text/html"/><link href="https://stageset.projects.metio.wtf/runbooks/reconcile-latency/?utm_source=atom_feed" rel="related" type="text/html" title="Reconcile latency high"/><link href="https://stageset.projects.metio.wtf/runbooks/controller-pod-down/?utm_source=atom_feed" rel="related" type="text/html" title="Controller pod down"/><link href="https://stageset.projects.metio.wtf/installation/operations/?utm_source=atom_feed" rel="related" type="text/html" title="Operations"/><link href="https://stageset.projects.metio.wtf/runbooks/artifactnotfound/?utm_source=atom_feed" rel="related" type="text/html" title="ArtifactNotFound"/><link href="https://stageset.projects.metio.wtf/runbooks/dependencynotready/?utm_source=atom_feed" rel="related" type="text/html" title="DependencyNotReady"/><id>https://stageset.projects.metio.wtf/runbooks/workqueue-saturation/</id><published>0001-01-01T00:00:00+00:00</published><updated>2026-06-16T13:41:17+02:00</updated><content type="html"><![CDATA[<blockquote>The controller cannot drain its reconcile queue fast enough.</blockquote><h2 id="symptom">Symptom</h2>
<p><code>workqueue_depth{controller=&quot;stageset&quot;}</code> stays high; StageSets reconcile slowly or
lag behind their <code>spec.interval</code>. The <code>StageSetControllerWorkqueueDepthHigh</code> alert
fires (see <a href="/installation/operations/">operations</a> for the alert set and its
thresholds).</p>
<h2 id="cause">Cause</h2>
<p>The controller is enqueuing reconcile requests faster than it completes them.
Common causes:</p>
<ul>
<li><strong>apiserver slowness</strong> — applies, dry-runs, and status writes all block on the
apiserver (or the impersonated tenant&rsquo;s authorization).</li>
<li><strong>slow sources</strong> — a stage waiting on a large artifact fetch or a source that is
slow to become Ready holds a worker.</li>
<li><strong>a stuck stage</strong> — an action with a long timeout (a <code>wait</code>/<code>http</code>/<code>job</code> that
never completes) pins a worker for the whole timeout.</li>
<li><strong>too few workers for the StageSet count</strong> — many StageSets reconciling on short
intervals.</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"># which StageSets are churning?</span>
</span></span><span class="line"><span class="cl">kubectl get stagesets -A --sort-by<span class="o">=</span>.status.observedGeneration
</span></span><span class="line"><span class="cl"><span class="c1"># controller logs for slow operations / retries</span>
</span></span><span class="line"><span class="cl">kubectl -n stageset-system logs deploy/stageset-controller --tail<span class="o">=</span><span class="m">200</span>
</span></span></code></pre></div><p>Correlate with <code>controller_runtime_reconcile_time_seconds</code> (see
<a href="/runbooks/reconcile-latency/">reconcile latency</a>) and apiserver latency.</p>
<h2 id="remediation">Remediation</h2>
<ul>
<li>Lengthen <code>spec.interval</code> on high-churn StageSets that don&rsquo;t need fast
reconciliation.</li>
<li>Cap long-running actions with a tighter <code>timeout</code> so a stuck action releases its
worker.</li>
<li>Adding replicas does <strong>not</strong> help: leader election means only the lease holder
reconciles, so a second replica is failover, not added throughput
(<a href="/installation/production/#high-availability">production</a>). The controller has no
reconcile-concurrency flag — the levers are reducing load (longer intervals, fewer
StageSets, fewer objects per stage) and removing the slow operations below.</li>
<li>Investigate apiserver / tenant-authorization latency if reconciles are uniformly
slow.</li>
</ul>
]]></content><category scheme="https://stageset.projects.metio.wtf/tags/runbooks" term="runbooks" label="runbooks"/><category scheme="https://stageset.projects.metio.wtf/tags/metrics" term="metrics" label="metrics"/><category scheme="https://stageset.projects.metio.wtf/tags/alerts" term="alerts" label="alerts"/><category scheme="https://stageset.projects.metio.wtf/tags/troubleshooting" term="troubleshooting" label="troubleshooting"/></entry></feed>