# KICK Operator (AI full context) Generated by hack/gen-docs.sh. # KICK Operator (AI quick context) KICK watches Secret and ConfigMap changes and evaluates whether a workload restart is required and currently allowed. ## APIs - KickRequest (`kick.corewire.io/v1alpha1`): durable restart request state machine. - KickPolicy (`kick.corewire.io/v1alpha1`): workload scope, GitOps gate, schedule, rate limit, dry-run. - NotificationPolicy (`kick.corewire.io/v1alpha1`): outbound webhook delivery for KickRequest outcomes. ## Runtime model - Source observation detects relevant dependency content changes. - KickRequest reconciliation re-resolves owner/gates and re-checks freshness before restart. - Restart action patches `kubectl.kubernetes.io/restartedAt` on Deployment, StatefulSet and DaemonSet. - Argo Rollouts are restarted via `spec.restartAt` instead, so the canary strategy is not re-run. ## Optional integrations - GitOps gating is opt-in: `spec.gitOps.provider` defaults to `None`. Providers: ArgoCD, Flux, Kargo, Auto. - `--enable-argocd` and `--enable-flux` default to true. - `--enable-kargo`, `--enable-argo-rollouts`, and `--enable-csi-integration` default to false. ## Deliberate non-goals - Dependencies declared by name are not supported; only proven env/volume/CSI references are used. - KICK has no admission webhook and never holds pods `Pending`. - OpenShift `DeploymentConfig` is not supported. ## Key references - docs/content/docs/installation.md - docs/content/docs/quickstart.md - docs/content/docs/comparison.md - docs/content/docs/guides/without-gitops.md - docs/content/docs/reference/kickrequest.md - docs/content/docs/reference/kickpolicy.md - docs/content/docs/reference/notificationpolicy.md - docs/content/docs/reference/configuration.md - docs/content/docs/reference/metrics.md - docs/content/docs/reference/events.md ## Security - Controller needs read access to Secrets/ConfigMaps to evaluate freshness. - KICK never logs Secret data or content digests. ## Source: docs/content/docs/_index.md --- title: Documentation weight: 1 description: KICK operator documentation. llmsDescription: | Documentation index for the KICK Kubernetes operator. Sections: getting started (install + quickstart), concepts (discovery, freshness, GitOps gates), theory (formal operator model with notation), guides (ArgoCD, running without GitOps, external-secrets, troubleshooting), reference (KickPolicy/KickRequest/NotificationPolicy, metrics, events, configuration), operations (RBAC, security, scalability, upgrades), development, and design decisions. GitOps gating is optional; spec.gitOps.provider defaults to None. --- KICK restarts a workload when a `Secret` or `ConfigMap` it consumes changes — but only when the running rollout is actually stale, and only when your GitOps tool permits the restart. GitOps gating is optional: KICK also runs on a cluster with no GitOps controller at all. ### What is KICK? Kubernetes never restarts a Pod when a `Secret` or `ConfigMap` it reads changes, so the running Pod silently drifts from its intended configuration. KICK is a small operator that closes this gap: it discovers each workload's dependencies, detects relevant changes, checks that the running rollout predates the change, optionally asks your GitOps tool for permission, and then issues exactly one restart via the standard `kubectl.kubernetes.io/restartedAt` annotation. It injects no state into your workloads. ### Where to start - **[Install KICK](installation/)** with Helm (or from source for local dev). - Run the **[Quickstart](quickstart/)** to see a Secret change restart a Deployment. - Read **[Concepts](concepts/)** to understand discovery, freshness, and GitOps gating. - Look up fields in the **[Reference](reference/)**. ## Sections | Section | What you'll find | |---------|------------------| | [Installation](installation/) | Install KICK with Helm or from source | | [Quickstart](quickstart/) | Watch KICK restart a Deployment on Kind | | [Concepts](concepts/) | Discovery, freshness, and GitOps gating, explained | | [Comparison](comparison/) | How KICK differs from Reloader and Wave | | [Guides](guides/) | Argo CD, Kargo, running without GitOps, External Secrets, troubleshooting | | [Reference](reference/) | KickPolicy / KickRequest / NotificationPolicy API, metrics, events, config | | [Operations](operations/) | RBAC, security, scalability, upgrades | | [Theory](theory/operator-model/) | The formal operator model in scientific notation | | [Development](development/) | Debugging, the timeline UI, workflow | | [For AI Agents](for-ai-agents/) | llms.txt, Markdown output, agent instructions | | [Decisions](decisions/) | Architecture decision records | ## Source: docs/content/docs/comparison.md --- title: Comparison weight: 35 description: How KICK compares to Stakater Reloader and Wave, including where those projects are the better choice. llmsDescription: | Comparison of KICK against Stakater Reloader and wave-k8s/wave. All three restart workloads when a Secret or ConfigMap changes. Reloader and Wave opt in per workload via annotations; KICK uses a KickPolicy with label selectors. Reloader writes a dummy env var or a last-reloaded-from annotation, Wave writes a config-hash annotation, KICK writes only kubectl.kubernetes.io/restartedAt (spec.restartAt for Argo Rollouts). KICK does not require GitOps: gitOps.provider defaults to None and gating is opt-in. Only KICK gates restarts on GitOps state (Argo CD AppProject sync windows and Application Synced, Flux Kustomization and HelmRelease Ready, Kargo Stage promotions) and on cron windows. KICK supports Secrets Store CSI SecretProviderClass rotation, Argo Rollouts, delivery webhooks via NotificationPolicy, and a dryRun mode. KICK deliberately does not support dependencies declared by name, holding pods Pending, or OpenShift DeploymentConfig. Reloader remains more mature and more widely deployed. --- KICK is not the first operator to restart workloads on configuration change. [Stakater Reloader](https://github.com/stakater/Reloader) and [Wave](https://github.com/wave-k8s/wave) solve the same core problem and are both older and more widely deployed. This page explains what is actually different, and when you should pick one of them instead. ## At a glance | | Reloader | Wave | KICK | |---|---|---|---| | Opt-in | Workload annotation | Workload annotation | `KickPolicy` selectors | | Writes | Env var, or annotation | `config-hash` | `restartedAt` | | Trigger | Event | Hash differs | Change newer than rollout | | Kinds | + `DeploymentConfig`, Argo Rollout | Deployment, StatefulSet, DaemonSet | + Argo Rollout (opt-in) | | Secrets Store CSI | Yes | No | Yes (opt-in) | | Deps declared by name | Yes | Yes | Deliberately not supported | | Timing | Debounce | Rate limit | Cron windows, rate limit | | GitOps gate | None | None | Optional: Argo CD, Flux, Kargo | | Restart-time webhooks | Yes | No | Yes (`NotificationPolicy`) | | Preview without acting | No | No | Yes (`dryRun`) | ## What is different about KICK **It can gate on GitOps state.** Neither Reloader nor Wave reads Argo CD or Flux state. Reloader's documentation is candid that mutating the pod template makes Argo CD report `OutOfSync`, and its answer is to mutate less. That solves drift, not timing: a restart still fires during a freeze, because Argo CD sync windows only ever constrained Argo CD's own syncs. KICK reads `spec.syncWindows` from the owning `AppProject` and blocks until the window opens. See [GitOps gates](../concepts/gitops-gates/). Gating is **opt-in**. `spec.gitOps.provider` defaults to `None`, so KICK runs on a cluster with no GitOps controller at all. See [Running without GitOps](../guides/without-gitops/). **No annotations on your workloads.** Reloader and Wave both need an annotation on each workload, which means you have to own the rendering. A `KickPolicy` selects workloads by label, so KICK also covers third-party Helm charts and vendored manifests you do not template. **Freshness is a comparison, not a hash.** KICK compares the last relevant change against the running rollout's start time, so a restart somebody else already performed satisfies the condition, and restarting the operator does not re-trigger anything. See [Freshness](../concepts/freshness/). **Adoption does not restart what is already fresh.** The first time KICK sees a Secret or ConfigMap it anchors the baseline to the last write the API server recorded for it, unmodified. A source untouched since its creation is older than the running rollout, so installing KICK over a healthy cluster performs no restarts, and it needs no mutating webhook to achieve that. A source that *was* written after the workload last rolled out is genuinely stale, and KICK restarts it once. Drift the API server did not record is not detected retroactively — the next real change picks it up. **Every restart is a durable object.** A `KickRequest` records why a restart was wanted, what blocked it, and what finally happened. `spec.dryRun` on a `KickPolicy` runs the entire pipeline and stops immediately before the patch, leaving a `DryRun` `KickRequest` you can inspect. ## When to use Reloader or Wave instead KICK is narrower and much newer. Prefer the alternatives when: - **Your application reads a Secret through the API** rather than through env, a volume, or a Secrets Store CSI mount. Reloader and Wave can both be told about such a dependency by name. KICK deliberately does not support this; see below. - **You need OpenShift `DeploymentConfig`.** KICK has no counterpart and none is planned. - **You want pods held `Pending` while a required Secret is missing.** Wave's mutating webhook does this. KICK never blocks scheduling. - **You want the most proven option.** Reloader has years of production use behind it across far more clusters than KICK. ## Deliberate non-goals These are not gaps waiting to be filled. They are decisions. **Dependencies declared by name.** Reloader's `secret.reloader.stakater.com/reload` and Wave's `wave.pusher.com/extra-configmaps` let you name a Secret the workload does not actually reference. KICK only restarts on dependencies it can *prove* the workload consumes, by reading the pod spec. A hand-maintained list drifts silently from reality, and a stale entry produces restarts nobody can explain. An application that reads a Secret through the API is better served by a client that re-reads it, or by an explicit `KickRequest` created by whatever triggered the change. **Holding pods `Pending`.** Blocking scheduling requires a mutating admission webhook in the pod path. A failure there is a cluster-wide outage of pod creation, which is a much worse failure mode than the one it prevents. KICK has no admission webhook at all and can never take the cluster down this way. ## One thing that is not different KICK also modifies the pod template — that is unavoidable, since changing the template is what starts a rollout. The difference is what gets written: the standard `kubectl.kubernetes.io/restartedAt` annotation that `kubectl rollout restart` uses, rather than tool-specific hashes, environment variables, or KICK-owned fields. Argo Rollouts are the exception: KICK sets `spec.restartAt`, which is the Rollout controller's own restart mechanism, so a configuration change does not run the canary or blue-green strategy. Checked against the Reloader and Wave documentation in August 2026. Both projects move quickly; verify current behaviour before relying on this table. ## Source: docs/content/docs/concepts/_index.md --- title: Concepts weight: 30 description: How KICK discovers dependencies, decides a workload is stale, and gates the restart behind GitOps. --- Kubernetes does **not** restart your workloads when a `Secret` or `ConfigMap` they consume changes. KICK closes that gap. It watches a workload's dependencies, detects when one changed **after** the last rollout, optionally asks your GitOps tool for permission, and then issues exactly one controlled restart. ![How KICK turns a dependency change into a gated restart](/images/how-kick-works.drawio.svg "Observation → coalesce → gate → freshness → restart") ## The pipeline Every restart decision follows the same four steps. Each concept page below covers one of them in depth. | Step | What happens | Concept | |------|--------------|---------| | 1. Detect the gap | A `Secret`/`ConfigMap` changes, but the running Pod keeps its old config. | [The hidden restart requirement](hidden-restart-requirement/) | | 2. Discover deps | Find the `Secret`s and `ConfigMap`s the workload consumes via env and volumes. `imagePullSecrets` are excluded. | [Dependency discovery](dependency-discovery/) | | 3. Check freshness | Compare the latest dependency change against the current rollout start. Newer dependency = stale = restart needed. | [Freshness](freshness/) | | 4. Gate the restart | If a provider or windows are configured, restart only when the owner is in sync and the window is open. | [GitOps gates](gitops-gates/) | By default (no `gitOps` provider) step 4 is a no-op and KICK restarts as soon as a workload is stale. {{< cards >}} {{< card link="hidden-restart-requirement/" title="Hidden restart requirement" subtitle="Why changing a Secret or ConfigMap silently drifts your Pods." >}} {{< card link="dependency-discovery/" title="Dependency discovery" subtitle="How KICK finds the Secrets and ConfigMaps a workload consumes." >}} {{< card link="freshness/" title="Freshness" subtitle="How KICK decides a running rollout is stale and needs a restart." >}} {{< card link="gitops-gates/" title="GitOps gates" subtitle="How native windows and a GitOps provider gate the restart." >}} {{< /cards >}} --- For the same model in formal notation, see the [operator model](../theory/operator-model/). The diagrams are editable draw.io SVGs (`docs/static/images/*.drawio.svg`) — open them in [draw.io](https://app.diagrams.net) to edit, then save in place. ## Source: docs/content/docs/concepts/dependency-discovery.md --- title: Dependency discovery weight: 20 description: How KICK finds the Secrets and ConfigMaps a workload consumes. --- KICK supports `Deployment`, `StatefulSet`, and `DaemonSet` workloads, and optionally `argoproj.io/v1alpha1` `Rollout` when the controller runs with `--enable-argo-rollouts`. It discovers their dependencies automatically — you never maintain a dependency list by hand. For each workload it walks the Pod template and collects every `Secret` and `ConfigMap` reached through: - container and init-container `envFrom` references; - container and init-container `valueFrom.secretKeyRef` / `valueFrom.configMapKeyRef`; - `volumes[].secret.secretName` and `volumes[].configMap.name`; - `volumes[].projected.sources[].secret.name` / `...configMap.name`; - `volumes[].csi` with driver `secrets-store.csi.k8s.io`, which yields a `SecretProviderClass` dependency (see below). `imagePullSecrets` are **excluded by design** and never trigger a restart — they authenticate image pulls, not application config. Each discovered source is identified by `apiVersion` + `kind` + `namespace` + `name`, so the same underlying object referenced twice is one dependency. An Argo `Rollout` that uses `spec.workloadRef` instead of an inline `spec.template` has no pod spec of its own, so KICK discovers no dependencies for it. Select the referenced workload instead. ## Only proven references KICK restarts only on dependencies it can prove the workload consumes by reading the pod spec. There is deliberately no way to declare an extra dependency by name: a hand-maintained list drifts from reality, and a stale entry causes restarts nobody can explain. If an application reads a Secret through the API, create a `KickRequest` from whatever triggers that change instead. ## Secrets Store CSI With `--enable-csi-integration`, KICK watches `SecretProviderClassPodStatus` and derives a content fingerprint from the mounted object versions reported by every pod of the class. Because a rotation reaches pods one at a time, KICK only acts once every pod reports the same versions; while they disagree it re-checks shortly instead of restarting mid-rotation. The first observation is anchored to an epoch baseline, so enabling the integration never restarts anything by itself. ## Scoping with selectors Discovery is always automatic — KICK never needs a hand-maintained dependency list. Two optional selectors on `spec.discovery` narrow what a policy acts on: - `workloadSelector` picks the workloads the policy manages. - `dependencySelector` picks which consumed Secret/ConfigMap changes trigger a restart (by the Secret/ConfigMap's own labels). A workload restarts only when it consumes a changed dependency that is in both scopes. An empty or omitted selector matches everything on its axis, so a policy with no selectors watches every workload and every dependency. When set, `dependencySelector` also scopes freshness: out-of-scope dependencies never mark a workload stale. ## Source: docs/content/docs/concepts/freshness.md --- title: Freshness weight: 30 description: How KICK decides a running rollout is stale and needs a restart. --- A changed dependency is not enough on its own — the running rollout might already predate the change *or* already include it. **Freshness** is the check that decides. It compares two timestamps: - **Λ** — the latest *relevant* dependency change (see [dependency discovery](../dependency-discovery/) for what counts). - **σ** — the current rollout's start time (the `restartedAt` stamp if present, otherwise the active revision's creation time), computed per workload kind. The decision is a strict comparison: | Condition | Verdict | |-----------|---------| | dependency newer than rollout (Λ > σ) | **stale** → restart required | | dependency older than or equal to rollout (Λ ≤ σ) | **fresh** → no restart | KICK always **re-reads live state** immediately before restarting, so a rollout that started in the meantime is never restarted twice. ## Initial baseline Freshness needs an authoritative change timestamp, and the first observation of a dependency never witnessed the change that produced the content it finds. KICK therefore dates that content from what the API server recorded: the source's creation time, or the last write any field manager made to it, whichever is later. - A source untouched since creation carries exactly what the workload picked up when it rolled out, so it is fresh and adopting a workload never restarts it. - A source written after its creation is dated to that write, exactly as the API server recorded it. Kubernetes' own timestamps are second-granular, so KICK keeps its change times at sub-second precision: a change made in the same second as a rollout is still recognised as newer. Drift that the API server did not record - a write by a client that does not use server-side field management - still cannot be inferred. From the baseline onward, every relevant change advances Λ and is compared normally. Evaluation is driven by observed changes, including the first observation of a source. A workload created after all of its dependencies already exist is fresh by construction — its Pods started from the current content of every source — so no `KickRequest` is created for it. The next relevant change to one of its sources triggers evaluation as usual. ## Source: docs/content/docs/concepts/gitops-gates.md --- title: GitOps gates weight: 40 description: How KICK gates a restart behind native schedule windows and a GitOps provider. --- A **gate** decides whether a restart that is *needed* (the workload is [stale](../freshness/)) may actually run *right now*. The gate never invents work: it only ever delays or permits a restart that freshness already justified. ![If the GitOps owner is unknown or the sync window is closed, KICK stays blocked and re-checks later; only a clear owner with an open window lets the restart run.](/images/gitops-gate.drawio.svg) Gating is **opt-in**. With no `gitOps` block — the default, `provider: None` — KICK self-gates and restarts as soon as a dependency is stale. You add a gate when a restart must wait for a maintenance window, or until your GitOps tool has finished reconciling the workload. ## Two stages The gate is evaluated in order. A restart runs only if **both** stages allow it. ``` stale workload ──▶ [ 1. native windows ] ──▶ [ 2. GitOps provider ] ──▶ restart │ │ OutsideSchedule sync window · owner · sync state ▼ ▼ wait & re-check wait & re-check ``` ### Stage 1 — native schedule windows `spec.schedule.windows[]` are KICK-native cron windows evaluated without any provider. Each window is either `Allow` or `Deny`: - The current time is **open** when it falls inside at least one `Allow` window and inside no `Deny` window. - **Deny always wins.** A `Deny` window overlapping the current time blocks the restart even if an `Allow` window also matches. - With no windows configured, this stage is always open. When the clock is outside the allowed schedule the gate blocks with reason `OutsideSchedule` and re-checks when the next window boundary arrives. Each window takes a 5-field `cron` expression, a `duration`, and an optional IANA `timeZone` (UTC by default). This stage never consults a GitOps tool, so a maintenance window works on its own. ### Stage 2 — GitOps provider `spec.gitOps.provider` selects who owns the restart decision: | Provider | Behaviour | |----------|-----------| | `None` *(default)* | KICK self-gates; this stage always allows. | | `ArgoCD` | Defer to Argo CD ownership and sync state. | | `Auto` | Detect the managing provider automatically. | | `Flux` | Reserved for Flux ownership (roadmap). | With a real provider, KICK must resolve **exactly one** owner for the workload and confirm that owner is reconciled before it restarts. **Owner resolution** (Argo CD): 1. **Primary** — the workload's Argo CD tracking annotation. 2. **Fallback** — an indexed lookup of `Application`s that manage the workload. 3. **Zero or ambiguous** — no owner (`OwnerUnknown`) or more than one (`MultipleOwners`) blocks the automatic restart. KICK never guesses. **Sync windows** — the Argo CD provider also evaluates `spec.syncWindows` on the `AppProject` that owns the `Application`, using the application name and its destination. A closed window blocks with `OutsideSchedule`, the same reason the native stage uses. So Argo CD sync windows are honoured without restating them in the `KickPolicy`. **Sync check** — when `requireReconciled` is `true` (the default), the owning `Application` must report `Synced`. An out-of-sync or still-reconciling owner blocks with `OwnerOutOfSync` / `OwnerReconciling`, so KICK never restarts against config the GitOps tool is mid-flight on. ## Blocking reasons The gate surfaces why a restart is waiting on `KickRequest.status.gate.reason`: | Reason | Stage | Meaning | |--------|-------|---------| | `Allowed` | — | Both stages passed; the restart may run. | | `OutsideSchedule` | Windows | Current time is outside a native window or an Argo CD sync window. | | `OwnerUnknown` | Provider | No GitOps owner could be resolved. | | `AmbiguousOwner` | Provider | More than one owner matched. | | `OwnerOutOfSync` | Provider | The owning application is not `Synced`. | | `OwnerReconciling` | Provider | The owning application is still applying. | | `ProjectUnknown` | Provider | The application's `AppProject` could not be read. | | `ProviderUnavailable` | Provider | The provider could not be queried. | | `ConfigurationError` | Provider | A sync window could not be parsed. | ## Waiting behaviour A blocked restart is not dropped. KICK persists the waiting phase and reason on the `KickRequest` and re-evaluates the gate when either: - a timer fires (to catch schedule-window boundaries), or - a relevant provider object changes (for example the `Application` becomes `Synced`). The moment the gate flips to `Allowed`, KICK re-reads live state and — if the workload is still stale — issues the restart. ## Example Restart only inside a nightly window **and** only once Argo CD has synced the owner: ```yaml apiVersion: kick.corewire.io/v1alpha1 kind: KickPolicy metadata: name: gated spec: discovery: workloadSelector: {} schedule: windows: - type: Allow cron: "0 2 * * *" # 02:00 daily duration: 1h gitOps: provider: ArgoCD requireReconciled: true ``` Native `schedule.windows` gate *when* KICK restarts; they are independent of the provider. Argo CD `AppProject` sync windows still constrain when Argo itself syncs, and the Argo provider honors them as part of its own gate. ## See also - [Freshness](../freshness/) — what makes a restart *needed* before the gate runs. - [KickPolicy reference](../../reference/kickpolicy/) — every `gitOps` field. - [Operator model §6](../../theory/operator-model/#6-policy-scope-and-the-gate) — the gate in formal notation. ## Source: docs/content/docs/concepts/hidden-restart-requirement.md --- title: Hidden restart requirement weight: 10 description: Why changing a Secret or ConfigMap silently drifts your running Pods. --- Kubernetes does **not** restart a workload when the content of a `Secret` or `ConfigMap` it consumes changes. The change lands in the API server, but the running Pod keeps serving its old configuration. ![Updating a Secret or ConfigMap does nothing in Kubernetes, so the Pod keeps its old config and drifts from the intended state.](/images/the-problem.drawio.svg) There are two reasons: - **Environment variables** are resolved once, at Pod start. A later change to the source is never re-read. - **Mounted files** are updated on the node eventually, but whether the process reloads them is entirely workload-specific — most never do. The result is silent **configuration drift**: the intended state (the updated Secret/ConfigMap) and the running state (the Pod's in-memory config) diverge, with no error and no signal. KICK closes this gap. It tracks each workload's dependencies and triggers a controlled rollout when one changes — immediately by default, or gated on [GitOps state and schedule windows](../gitops-gates/) when configured. ## Source: docs/content/docs/decisions/0001-observation-storage-spike.md # ADR 0001: Change Observation Storage Spike ## Status Accepted for implementation guidance. ## Context Task 03 requires a decision for durable Secret/ConfigMap change observations while open questions remain about reliable Kubernetes change timestamps and baseline semantics. Constraints: - survive controller restart; - distinguish relevant content changes from metadata-only updates; - avoid workload annotations; - prevent Secret data leakage from stored state; - define garbage collection behavior. ## Options Considered ### Option A: Infer from Kubernetes metadata only Candidate fields: resourceVersion, generation, managedFields time. Pros: - no extra storage model. Cons: - does not reliably separate metadata-only updates from relevant content changes; - managedFields and generation behavior is not stable enough across all writers; - downtime gaps are ambiguous without durable content signature state. Decision: rejected. ### Option B: Store full object snapshots Pros: - exact diff after restart. Cons: - high storage cost; - unacceptable Secret exposure risk. Decision: rejected. ### Option C: Store durable observation records with canonical relevant fingerprints Pros: - restart-safe; - metadata-only filtering is deterministic; - no Secret content stored; - independent of workload annotations. Cons: - requires a durable store and GC policy. Decision: selected. ## Decision Use an observation service behind interfaces with durable records containing: - source identity: apiVersion/kind/namespace/name; - last seen resourceVersion; - last relevant resourceVersion; - last relevant change time (controller observed time); - canonical relevant fingerprint digest. Digest input includes only relevant fields: - Secret: data, type, immutable; - ConfigMap: data, binaryData, immutable. Metadata-only changes do not modify last relevant fields. ## Initial Baseline Semantics Default policy is conservative: - first observation establishes baseline; - first observation alone does not enqueue restart. This is isolated behind BaselinePolicy so optional source creation can be treated as RelevantChange by higher-level controllers when reference-index context proves it is a dependency appearance event. ## Durable Storage Model Proposal Production target: dedicated observation CRD (namespaced by source namespace). Suggested object key: - name: - to avoid long names; - namespace: source namespace. Suggested fields: - spec.identity - status.lastSeenResourceVersion - status.lastRelevantResourceVersion - status.lastRelevantChangeTime - status.relevantFingerprint No Secret data or plaintext content is persisted. ## Garbage Collection GC behavior: - periodic sweep lists observation records by namespace; - if source object does not exist and reverse dependency indexes show no current consumers, delete observation record; - retain records while source is still referenced, even if source is temporarily absent. ## Downtime Behavior After restart, observer reads durable records and compares new objects against stored fingerprint and RV state. Result: - metadata-only updates remain metadata-only; - relevant changes remain detectable even after controller downtime. ## Prototype Evidence Prototype package: internal/observation - interface-based observation service; - deterministic relevant fingerprint generation; - restart recovery test by re-instantiating observer with persisted store. This spike intentionally does not implement full source controllers. ## Source: docs/content/docs/decisions/0002-current-replicaset-selection.md # ADR 0002: Current ReplicaSet Selection for Deployment Freshness ## Status Accepted for implementation guidance. ## Context Task 06 requires a robust way to identify the Deployment ReplicaSet corresponding to the current Deployment Pod template. The algorithm must not rely on "newest ReplicaSet wins" and must represent ambiguous states explicitly. ## Decision Use a two-step algorithm: 1. Filter ReplicaSets to those controlled by the Deployment UID. 2. Select ReplicaSets whose PodTemplateSpec is equivalent to the Deployment template after normalizing hash-only labels (`pod-template-hash`). Selection outcomes: - exactly one match: current ReplicaSet selected; - zero matches: explicit `NoMatchingReplicaSet`; - more than one match: explicit `AmbiguousReplicaSetMatch`. ## Why this approach - Owner UID scoping prevents cross-workload contamination. - Template equivalence handles rollback and history cleanup correctly. - It remains correct when the newest ReplicaSet is not current. ## Rejected approach Newest ReplicaSet by creation timestamp alone: - fails during rollback; - fails during overlapping rollout states; - can choose wrong ReplicaSet when history is retained. ## Rollout progress interpretation `InProgress` is true when Deployment status indicates rollout activity (generation not observed, updatedReplicas lagging desired, stale old replicas, or insufficient availability). `Paused` produces a non-in-progress blocked reason. ## Zero replicas `spec.replicas: 0` is supported. Current ReplicaSet can still be selected via template matching; completion remains deterministic when no rollout progress is active. ## Prototype evidence - Unit tests cover normal rollout, active rollout, rollback, pause, zero replicas, and history cleanup. - Envtest validates behavior against a real API server cache/list path. ## Source: docs/content/docs/decisions/0003-argocd-compatibility-research.md # ADR 0003: Argo CD Compatibility Research Baseline ## Status Accepted as implementation baseline for Task 09. ## Sources consulted - Argo CD resource tracking docs (stable): annotation tracking-id format, installation-id, non self-referencing behavior. - Argo CD sync windows docs (stable): selector semantics, deny precedence, timezone, OR/AND matching, manualSync, syncOverrun. - Argo CD ApplicationSet integration docs (stable): default namespace assumptions around Argo CD control plane resources. ## Supported version matrix | Argo CD minor | Status | Notes | |---|---|---| | 2.10.x | target | adapter fixtures and gate semantics expected to apply | | 2.11.x | target | adapter fixtures and gate semantics expected to apply | | 2.12.x | target | adapter fixtures and gate semantics expected to apply | | <2.10 | unsupported | no compatibility guarantee in KICK v1 | | >2.12 | best effort | must pass fixture contract before being declared supported | ## Control-plane namespace discovery decision (open question 1) Decision: - do not rely on a single Application status field for controller namespace discovery; - require explicit config `argocd.namespace` for v1; - allow future optional auto-discovery only after per-version verification. Rationale: - stable docs do not provide a universal guaranteed field for all modes; - explicit config keeps owner/project lookups deterministic. ## Tracking-id parser decision (open question 5) Decision: - primary ownership path uses `argocd.argoproj.io/tracking-id` in annotation or annotation+label mode; - parser requires self-reference validation against runtime workload identity; - non self-referencing tracking-id is rejected for ownership resolution. Canonical fixture format: `:/:/` Example: `my-app:apps/Deployment:default/my-deployment` Installation ID: - if `argocd.argoproj.io/installation-id` is configured, it must be included in ownership checks to avoid cross-instance collisions. ## Ownership fallback data source (open question 4) Decision: - v1 fallback source is Application managed-resource membership from Application status/resource summary data where available; - if a cluster/operator mode omits reliable membership data, KICK returns `AmbiguousOwner` or `OwnerUnknown` instead of guessing. Rationale: - correctness over convenience; - no full-cluster arbitrary inference by destination namespace or repo URL. ## Sync-window semantics decision (open question 6) Decision: - deny overrides allow; - no matching windows => allow; - if any allow windows match, sync allowed only during active allow windows; - selector matching supports applications, namespaces, clusters with wildcard; - default selector composition is OR, with explicit support for AND mode when `useAndOperator` is set; - timezone honored from window config; - `manualSync` does not grant KICK bypass in v1 automation flow. ## Rollout annotation self-heal findings (open question 7) Decision: - restart annotation ownership is treated as Argo-managed desired-state interaction. - if Argo sync removes or rewrites restart timestamp, KICK reevaluates freshness and only reissues when still required. Evidence status: - documented expected outcomes in reusable fixture file; - e2e verification remains required before final support declaration. ## Unsupported modes (explicit) - label-only tracking mode without tracking-id parser support. - ambiguous multi-instance ownership without installation-id disambiguation. - controller namespace auto-discovery without explicit config. - ownership inference by destination namespace/repo URL alone. ## Reusable fixture sets - `internal/gitops/argocd/fixtures/tracking_id_cases.yaml` - `internal/gitops/argocd/fixtures/ownership_fallback_cases.yaml` - `internal/gitops/argocd/fixtures/sync_window_cases.yaml` - `internal/gitops/argocd/fixtures/rollout_annotation_selfheal_cases.yaml` ## Source: docs/content/docs/decisions/_index.md --- title: Decisions weight: 90 --- Architecture decision records for the KICK operator. ## Source: docs/content/docs/development/_index.md --- title: Development weight: 80 --- Debugging KICK, the end-to-end suites, the timeline UI, and the token-efficient development workflow. ## Source: docs/content/docs/development/documentation-standards.md --- title: Documentation Standards weight: 35 description: "Hard documentation rules: editable images, freshness, concision, proof links, and feature-to-example coverage." --- Hard rules for KICK documentation. ## Rules 1. Images - Every image referenced from docs or README must be editable draw.io SVG named `*.drawio.svg`. - Reference format: [how-kick-works.drawio.svg](/images/how-kick-works.drawio.svg). 2. Freshness - Docs must change with behavior changes in the same PR. - No known stale statements. 3. Concision - Keep pages short. - Remove filler and duplicates. 4. Proofs for claims - Every non-trivial claim must link to proof. - Proof can be spec, code, tests, traceability mapping, or generated artifact. 5. Feature coverage - Every feature must be documented. - Every feature must have at least one example, usually an e2e scenario. ## Proof sources - Feature registry: [traceability/features.yaml](https://github.com/corewire/kick/blob/main/traceability/features.yaml) - Scenario registry: [traceability/e2e-scenarios.yaml](https://github.com/corewire/kick/blob/main/traceability/e2e-scenarios.yaml) - Scenario examples: [test/e2e/scenarios/](https://github.com/corewire/kick/tree/main/test/e2e/scenarios) - API surface: [api/v1alpha1/](https://github.com/corewire/kick/tree/main/api/v1alpha1) - Specs: [ai-docs/kick-operator-specs/kick-specs/](https://github.com/corewire/kick/tree/main/ai-docs/kick-operator-specs/kick-specs) For agent workflows, use the dedicated skill at `.agents/skills/documentation-hard-rules/SKILL.md` and the repository rules in `AGENTS.md`. ## Source: docs/content/docs/development/e2e-testing.md --- title: End-to-end tests weight: 40 --- KICK's end-to-end tests run Chainsaw scenarios against a kind cluster. Every scenario directory under `test/e2e/scenarios` maps to one stable scenario ID and carries a `trace.yaml` linking it to the features it proves. ## Suites | Target | Covers | Installs | |---|---|---| | `make test-e2e-core` | restart, policy and observation behaviour | KICK | | `make test-e2e-argocd` | Argo CD ownership, sync windows and sync state | Gitea, Argo CD | | `make test-e2e-recovery` | crash and restart recovery | KICK | | `make test-e2e-rollouts` | Argo Rollouts restarts | Argo Rollouts | | `make test-e2e-csi` | Secrets Store CSI rotation | CSI driver and provider | | `make test-e2e-kargo` | Kargo promotion gating | cert-manager, Kargo | Each target installs its own prerequisites and then redeploys the manager from the `config/e2e` overlay. The manager probes the optional integration CRDs once at startup, so it is always restarted after a CRD is installed — otherwise the integration would stay silently inactive. ## Working on a single scenario ```bash make e2e-rollouts-setup # prerequisites for the suite make test-e2e-scenario E2E=060 # one scenario, integration timeout budget make test-e2e-render # render every scenario without a cluster ``` ## GitOps fixtures Scenarios that need a real GitOps source push manifests into the in-cluster Gitea, one repository per scenario, so parallel scenarios never share state: ```bash test/e2e/setup/gitea/seed.sh e2e-037 manifests ./manifests test/e2e/setup/gitea/commit-file.sh e2e-037 manifests/app.yaml ./updated/app.yaml ``` Argo CD sync windows also block Argo CD's own sync, so a scenario that closes a window must deliver the dependency change directly instead of through git. ## Source: docs/content/docs/development/kamera-debugging.md # Kamera Debugging for KICK KICK includes optional kamera tooling for structural control-plane analysis and exploration artifact inspection. Upstream: https://github.com/tgoodwin/kamera ## Install From repo root: ```bash make kamera bin/kamera --help ``` The Make target installs: ```bash go install github.com/tgoodwin/kamera/cmd/kamera@main ``` ## KICK example you can run now This repository now includes a concrete KICK graph example with real KICK resource kinds: - docs/development/examples/kamera/kick-dependency-graph.json It models: - SourceObservationReconciler - KickRequestReconciler - core/v1 Secret + ConfigMap - apps/v1 Deployment - kick.corewire.io/v1alpha1 KickRequest ### 1) Detect hotspots in the KICK graph ```bash bin/kamera inspect hotspots docs/development/examples/kamera/kick-dependency-graph.json ``` Expected hotspot types in output: - multi_writer on KickRequest status - feedback_cycle on KickRequestReconciler <-> KickRequest - reducer_controller on KickRequestReconciler - missing_trigger on resources KickRequestReconciler reads but does not directly watch This gives a fast structural risk scan before runtime debugging. ### 2) Render dependency graph PDF ```bash bin/kamera inspect dependency-graph docs/development/examples/kamera/kick-dependency-graph.json ``` On Linux this uses xdg-open and reports a temp PDF path such as: ```text opened dependency graph from docs/development/examples/kamera/kick-dependency-graph.json pdf saved at /tmp/dependency-graph-.pdf ``` Use this graph during design reviews for controller/resource coupling. ## Exploration dumps (when you have one) If you have an exploration dump file or directory from a kamera harness: ```bash bin/kamera inspect exploration bin/kamera inspect exploration --interactive=false bin/kamera analyze report bin/kamera analyze diff ``` For headless CI triage, prefer: ```bash bin/kamera inspect exploration --interactive=false ``` This prints DAG + node details directly to stdout. ## Determinize helper Kamera also provides deterministic time rewrite tooling: ```bash bin/kamera determinize [paths...] ``` Use this on isolated experiments, not as a blanket rewrite of the repository. ## KICK workflow recommendation 1. Reproduce issue with existing checks (`make test`, `make test-e2e-scenario E2E=...`). 2. Run hotspot scan on a graph snapshot of the affected controllers/resources. 3. Inspect exploration dumps (if available) to isolate ordering-sensitive paths. 4. Convert findings into deterministic unit/envtest/e2e assertions in KICK. ## Notes - kamera generate currently depends on upstream translation code paths that are evolving; prefer inspect/analyze workflows for now. - kamera tagged module versions currently lag cmd/kamera availability, so this repository pins installation to main. ## Source: docs/content/docs/development/timeline-ui.md # Timeline UI and Tracing KICK now exposes a timeline API and browser UI for workload restart investigations. > **⚠ Experimental — do not expose.** The timeline UI and API are an unauthenticated, read-only debug aid intended for local development (localhost / `kubectl port-forward`) only. They expose namespace, workload, policy, and event metadata with no authn/authz. Never expose `--timeline-bind-address` through an Ingress, LoadBalancer, or any untrusted network. ## Endpoints - Root: `/` (redirects to `/timeline/ui`) - UI: `/timeline/ui` - Overview API: `/timeline/overview` (all managed workloads and their events across every namespace) - API: `/timeline?namespace=&kind=&name=` - Discovery API: `/timeline/discovery?namespace=[&policy=][&kind=][&name=]` - DAG API: `/timeline/dag?namespace=` ## Cross-namespace overview The UI opens on a compact, all-namespace overview that answers "what happened, when" at a glance: - one swimlane per managed workload, grouped by namespace, with a color-coded state band over time; - color-coded event markers (dependency change, restart, request, waiting/blocked, failure, k8s event) with hover details; - a chronological event log alongside the lanes; - a dedicated ruler row with a time picker (from/to), quick presets, and drag-to-zoom (draw a box on the ruler); - a text filter to narrow lanes and the log by namespace or workload. ## Policy-driven discovery and filtering The UI now auto-loads workloads discovered from `KickPolicy` selectors in the selected namespace. - it lists discovered `Deployment`, `StatefulSet`, and `DaemonSet` workloads; - it supports filtering by policy, workload kind, and workload name substring; - selecting a discovered workload auto-fills the timeline target and loads that workload timeline. - it renders a namespace DAG: `KickPolicy -> workload -> Secret/ConfigMap`. ## What the timeline shows - relevant Secret/ConfigMap change timestamps from observation records; - KickRequest creation and current phase snapshots; - controller/emitted Kubernetes events for the workload and related KickRequests; - workload `kubectl.kubernetes.io/restartedAt` updates. ## Enable timeline server The manager serves timeline endpoints by default: ```text --timeline-bind-address=:8090 ``` Set empty value to disable: ```text --timeline-bind-address= ``` ## OTEL export (Tempo/Jaeger) KICK emits a small, high-signal set of spans built to answer one question: *when did the source change, and when did the workload restart?* Each restart cycle is a **single trace** with two spans sharing one trace ID: - `dependency.changed` — a relevant Secret/ConfigMap change with consumers, carrying a `source.changed` event at the observed-change time. - `restart.executed` — the actual workload patch, carrying a `workload.restarted` event at the restart time. Correlation is durable: the observer stamps the change's W3C `traceparent` onto the KickRequest (annotation `kick.corewire.io/traceparent`), and the executor resumes that trace when it restarts the workload — even across GitOps gate waits and controller restarts. Per-reconcile bookkeeping is deliberately **not** traced. Configure OTLP export: ```text --otel-otlp-endpoint= --otel-otlp-insecure=true ``` Examples: - Tempo via OTLP collector service in-cluster. - Jaeger collector OTLP gRPC endpoint. When endpoint is unset, tracing remains disabled and has no exporter overhead. ## Tracing demo (Tilt) `tilt up` deploys a self-contained tracing backend for local development: - Jaeger all-in-one (`hack/tracing/jaeger.yaml`) in the `kick-tracing` namespace, with in-memory storage and the OTLP receiver enabled. - The `config/dev` overlay points the manager at it via `--otel-otlp-endpoint=jaeger.kick-tracing.svc.cluster.local:4317`. Open the Jaeger UI at [http://localhost:16686](http://localhost:16686) (port-forwarded by Tilt), select the `kick-controller` service, and trigger a restart to see source-observation, KickRequest reconciliation, and restart-execution spans. > The demo backend has no persistence or auth. It is for local development only. ## Source: docs/content/docs/development/token-efficient-workflow.md # Token-efficient development workflow ## Goal Enable a lower-cost coding agent to implement KICK in small, verifiable increments without repeatedly reading the entire specification or inventing missing behavior. ## Core rule One agent invocation MUST address exactly one task file from `tasks/` and the smallest set of linked specification files. Do not ask an agent to "implement KICK" or to read every document. The task file is the entry point and declares the required context. ## Context loading order For any implementation task, read only: 1. `AGENTS.md` — global constraints. 2. The selected `tasks/NN-*.md` file. 3. Only the specification files listed under that task's `Dependencies` section. 4. The relevant feature entries from `traceability/features.yaml`. 5. Existing code in the package being changed and its direct tests. Read other files only when the selected task or compiler output proves they are needed. ## Prompt template Use this prompt for a coding agent: ```text Implement task in the KICK repository. Read in this order: 1. AGENTS.md 2. 3. only the files listed in the task's Dependencies section 4. the feature IDs referenced by the task in traceability/features.yaml 5. existing code and tests in the packages you modify Rules: - Do not implement later tasks. - Do not resolve open questions by assumption. - Keep the patch minimal. - Add or update unit tests for every behavior changed. - Add/update e2e scenario metadata when required by the feature matrix. - Run the narrowest relevant checks first, then the task's full acceptance checks. - Report changed files, tests run, unresolved blockers, and feature IDs covered. ``` ## Work-unit sizing A task should normally change: - one production package; - its unit tests; - at most one API or test fixture area; - one traceability entry when necessary. Split a task further when it requires more than one independent reconciliation concern or more than approximately 400 lines of new production code. ## Stable interfaces first Implement pure, provider-neutral components before controllers: 1. dependency extraction; 2. dependency identity and indexes; 3. content-change classification; 4. rollout inspection; 5. freshness evaluation; 6. GitOps provider contract; 7. Argo CD adapter; 8. KickRequest reconciliation; 9. execution and rollout observation. Pure functions and narrow interfaces reduce the context required for later agents and make most behavior testable without Envtest. ## Test-first sequence For each behavior: 1. identify the feature ID; 2. write or update the smallest unit test; 3. implement the pure behavior; 4. add the Envtest/controller test when API behavior is involved; 5. add or update the isolated Chainsaw scenario; 6. update traceability metadata; 7. run the coverage checker. No feature is complete when its required test level is missing. ## Commands by feedback cost Run checks in this order: ```text 1. go test ./path/to/changed/package 2. go test ./path/to/changed/package -run TestSpecificCase 3. go test ./... 4. make generate 5. make verify-generated 6. make test-envtest 7. make e2e-scenario SCENARIO=KICK-E2E-NNN 8. make e2e-pr ``` Do not begin with the complete e2e suite unless the task explicitly changes shared cluster setup. ## Agent handoff record Every completed task MUST leave a short handoff in the pull request or task result: ```text Task: Feature IDs: Changed packages: Tests added/changed: Commands executed: Known limitations: Next task unlocked: ``` This allows the next agent to start from repository state rather than rereading prior conversations. ## Decision records When an implementation choice is not already specified and affects public API, persistence, ownership resolution, or compatibility: - stop implementation; - add the question to `specs/17-open-questions.md` or an ADR; - implement only an interface or fake needed to continue independent work. Do not let a coding agent silently encode a product decision. ## Generated files Agents MUST NOT manually edit generated CRDs, deepcopy files, or generated API documentation. Modify sources and run the documented generation target. Generated-diff checks prevent an agent from spending tokens reviewing derived output as if it were handwritten code. ## Efficient review strategy Review by feature ID rather than by file count: 1. inspect the feature contract; 2. inspect the unit test proving local behavior; 3. inspect the e2e scenario proving cluster behavior; 4. inspect the minimal implementation path; 5. verify the traceability report. ## Parallel work Tasks may run in parallel only when they do not modify the same API types, controller, generated manifests, or traceability entries. Good parallel candidates: - dependency extractor and Argo CD research spike; - rollout inspector spike and docs tooling; - observability after metric names are frozen, alongside Helm/RBAC. Avoid parallel edits to: - `api/`; - manager setup; - `KickRequestController`; - generated CRDs; - shared e2e cluster bootstrap. ## Completion definition A task is complete only when: - acceptance criteria pass; - tests required by the feature matrix exist and pass; - generated files are current; - no unresolved assumption was hidden in code; - the handoff record is complete. ## Source: docs/content/docs/for-ai-agents.md --- title: For AI Agents weight: 85 description: Machine-readable endpoints for consuming the KICK documentation with an LLM or agent. llmsDescription: | Machine-readable documentation endpoints for KICK. llms.txt at the site root lists every page with a one-line summary. llms-full.txt contains the whole documentation set in one file. Every page is also served as clean Markdown: leaf pages at {page}.md, sections at {section}/index.md. AGENTS.md in the repo root instructs IDE coding agents. Regenerate with make generate. --- KICK's documentation is published in machine-readable form so an agent can load the whole project context in one or two requests. ## Endpoints | URL | Content | Use case | |-----|---------|----------| | [`/kick/llms.txt`](/kick/llms.txt) | Compact project summary: APIs, runtime model, key references | Cheap orientation | | [`/kick/llms-full.txt`](/kick/llms-full.txt) | Every documentation page concatenated into one file | One GET = full project context | | `{page}.md` | Clean Markdown for a single page | Fetch one topic | ## Markdown output Every page is served as Markdown next to its HTML. Leaf pages append `.md` to the page path; sections append `index.md`: ``` /kick/docs/installation/ → HTML /kick/docs/installation.md → Markdown /kick/docs/reference/ → HTML /kick/docs/reference/index.md → Markdown ``` ## Context menu Every documentation page has a context menu in the top-right with **Open in ChatGPT** and **Open in Claude**. Both pre-load the page's Markdown URL, so the model reads the page directly instead of relying on training data. ## IDE coding agents `AGENTS.md` in the repository root is auto-discovered by IDE coding agents. It carries the rules that matter when changing code: the required task workflow, framework constraints, design constraints, traceability requirements, and the local `make` targets. For documentation work, use the dedicated skill at `.agents/skills/documentation-hard-rules/SKILL.md` and follow the [Documentation Standards](development/documentation-standards/). ## Regenerating ```bash make generate ``` `hack/gen-docs.sh` rewrites `llms.txt`, rebuilds `llms-full.txt` from every Markdown file under `docs/content/docs/`, and publishes a copy to `docs/static/` so the site serves it. `make docs-gen-check` fails the build when the generated files drift from the sources. ## Source: docs/content/docs/guides/_index.md --- title: Guides weight: 40 --- Task-focused guides for integrating KICK with GitOps tooling and diagnosing issues, including Argo CD, Argo Rollouts, and Kargo. ## Source: docs/content/docs/guides/argo-rollouts.md --- title: Argo Rollouts weight: 42 description: Configure KICK for Argo Rollouts with scenario-backed restart examples. --- KICK can restart Argo Rollouts workloads when the Rollouts integration is enabled. ![Argo Rollouts restart path](/images/argo-rollouts-restart.drawio.svg) ## Enable the integration Set one of the supported toggles: - Helm value: `integrations.argoRollouts.enabled: true` - Manager flag: `--enable-argo-rollouts=true` Proof: [Configuration reference](../reference/configuration/). ## Minimal policy ```yaml apiVersion: kick.corewire.io/v1alpha1 kind: KickPolicy metadata: name: default namespace: kick-e2e-060 spec: discovery: workloadSelector: {} ``` Proof scenario: [KICK-E2E-060](https://github.com/corewire/kick/tree/main/test/e2e/scenarios/KICK-E2E-060-canary-restart-does-not-rerun-steps). Raw test manifest (implementation detail): [resources.yaml](https://github.com/corewire/kick/blob/main/test/e2e/scenarios/KICK-E2E-060-canary-restart-does-not-rerun-steps/resources.yaml). ## Restart behavior that matters KICK restarts Rollouts through `spec.restartAt`, not by patching pod-template annotations. That avoids re-running canary/blue-green steps from step zero. Proof scenario: - [KICK-E2E-060](https://github.com/corewire/kick/tree/main/test/e2e/scenarios/KICK-E2E-060-canary-restart-does-not-rerun-steps) ## WorkloadRef behavior For a Rollout with `workloadRef`, KICK targets the referenced Deployment when that Deployment owns the dependency. Proof scenario: - [KICK-E2E-063](https://github.com/corewire/kick/tree/main/test/e2e/scenarios/KICK-E2E-063-workloadref-rollout-restarts-referenced-deployment) ## More proven examples - Canary restart keeps step index: [KICK-E2E-060](https://github.com/corewire/kick/tree/main/test/e2e/scenarios/KICK-E2E-060-canary-restart-does-not-rerun-steps) - Blue/green active service remains stable: [KICK-E2E-061](https://github.com/corewire/kick/tree/main/test/e2e/scenarios/KICK-E2E-061-blue-green-restart-keeps-active-service) - Rollout completion gates KickRequest: [KICK-E2E-062](https://github.com/corewire/kick/tree/main/test/e2e/scenarios/KICK-E2E-062-rollout-completion-gates-request) ## Feature mapping - Argo Rollout workload restarts: [KICK-FEAT-024](https://github.com/corewire/kick/blob/main/traceability/features.yaml) ## Source: docs/content/docs/guides/argocd.md --- title: Argo CD weight: 41 description: Configure Argo CD gating with concrete, scenario-backed examples. --- KICK can gate restarts on Argo CD ownership, sync state, and AppProject sync windows. ![Argo CD gate flow](/images/gitops-gate.drawio.svg) ## Minimal policy ```yaml apiVersion: kick.corewire.io/v1alpha1 kind: KickPolicy metadata: name: default namespace: team-a spec: discovery: workloadSelector: {} gitOps: provider: ArgoCD ``` Proof: provider contract and fields in [KickPolicy reference](../reference/kickpolicy/). ## Ownership resolution order 1. Tracking annotation on workload: `argocd.argoproj.io/tracking-id`. 2. Fallback owner search when annotation is missing or invalid. 3. Block if owner is missing or ambiguous. Proof scenarios: - Annotation owner in Argo CD namespace: [KICK-E2E-024](https://github.com/corewire/kick/tree/main/test/e2e/scenarios/KICK-E2E-024-annotation-owner-in-argo-cd-namespace) - Annotation owner in other namespace: [KICK-E2E-025](https://github.com/corewire/kick/tree/main/test/e2e/scenarios/KICK-E2E-025-annotation-owner-in-other-namespace) - Invalid annotation fallback: [KICK-E2E-026](https://github.com/corewire/kick/tree/main/test/e2e/scenarios/KICK-E2E-026-invalid-annotation-fallback) - No owner blocks: [KICK-E2E-028](https://github.com/corewire/kick/tree/main/test/e2e/scenarios/KICK-E2E-028-no-owner-blocks) - Multiple owners block: [KICK-E2E-029](https://github.com/corewire/kick/tree/main/test/e2e/scenarios/KICK-E2E-029-multiple-owners-block) ## Gate checks - `Application` sync state: waits while OutOfSync or actively syncing. - `AppProject` sync windows: restart only when window allows. Proof scenarios: - Open window allows restart: [KICK-E2E-032](https://github.com/corewire/kick/tree/main/test/e2e/scenarios/KICK-E2E-032-open-window-and-synced-allows-kick) - Closed/deny windows wait: [KICK-E2E-033](https://github.com/corewire/kick/tree/main/test/e2e/scenarios/KICK-E2E-033-closed-allow-window-waits), [KICK-E2E-034](https://github.com/corewire/kick/tree/main/test/e2e/scenarios/KICK-E2E-034-deny-window-waits) - OutOfSync/syncing waits: [KICK-E2E-037](https://github.com/corewire/kick/tree/main/test/e2e/scenarios/KICK-E2E-037-outofsync-waits), [KICK-E2E-038](https://github.com/corewire/kick/tree/main/test/e2e/scenarios/KICK-E2E-038-active-sync-waits) ## Feature mapping - Provider-neutral gate: [KICK-FEAT-008](https://github.com/corewire/kick/blob/main/traceability/features.yaml) - Argo owner + fallback + ambiguity: [KICK-FEAT-009](https://github.com/corewire/kick/blob/main/traceability/features.yaml), [KICK-FEAT-010](https://github.com/corewire/kick/blob/main/traceability/features.yaml) - AppProject windows + sync wait: [KICK-FEAT-012](https://github.com/corewire/kick/blob/main/traceability/features.yaml), [KICK-FEAT-013](https://github.com/corewire/kick/blob/main/traceability/features.yaml) ## Source: docs/content/docs/guides/external-secrets.md # External Secrets KICK reacts to resulting Kubernetes Secret and ConfigMap changes, not to external secret provider events directly. Recommended pattern: 1. external system updates secret source; 2. sync controller writes Kubernetes Secret/ConfigMap; 3. KICK observes content change and evaluates freshness. Notes: - metadata-only updates are ignored; - unchanged data values are ignored; - Secret read RBAC is required in target namespaces. ## Source: docs/content/docs/guides/kargo.md --- title: Kargo weight: 43 description: Configure KICK Kargo gating with Stage-annotation and promotion-state examples. --- KICK can gate restarts on Kargo Stage promotion state when `provider: Kargo` is set. ![Kargo promotion gate](/images/kargo-promotion-gate.drawio.svg) ## Enable the integration Set one of the supported toggles: - Helm value: `integrations.kargo.enabled: true` - Manager flag: `--enable-kargo=true` Proof: [Configuration reference](../reference/configuration/). ## Minimal policy ```yaml apiVersion: kick.corewire.io/v1alpha1 kind: KickPolicy metadata: name: default namespace: kick-e2e-068 spec: discovery: workloadSelector: {} gitOps: provider: Kargo ``` Proof example: [KICK-E2E-068 resources](https://github.com/corewire/kick/blob/main/test/e2e/scenarios/KICK-E2E-068-kargo-promotion-blocks-restart/resources.yaml). ## Required Argo CD Application annotation KICK resolves the authorized Stage from this annotation on the Argo CD Application: ```yaml metadata: annotations: kargo.akuity.io/authorized-stage: kick-e2e-068:prod ``` Proof example: [KICK-E2E-068 Application](https://github.com/corewire/kick/blob/main/test/e2e/scenarios/KICK-E2E-068-kargo-promotion-blocks-restart/resources.yaml). ## Gate behavior - If promotion is active for the resolved Stage, KICK blocks restart with gate reason. - After promotion completes, KICK proceeds to Argo CD gate checks. Proof scenarios: - Promotion blocks restart: [KICK-E2E-068](https://github.com/corewire/kick/tree/main/test/e2e/scenarios/KICK-E2E-068-kargo-promotion-blocks-restart) - Restart after promotion: [KICK-E2E-069](https://github.com/corewire/kick/tree/main/test/e2e/scenarios/KICK-E2E-069-kargo-restart-after-promotion) ## Safety cases - Missing annotation blocks: [KICK-E2E-070](https://github.com/corewire/kick/tree/main/test/e2e/scenarios/KICK-E2E-070-kargo-unannotated-application-blocks) - Ambiguous stage list blocks: [KICK-E2E-071](https://github.com/corewire/kick/tree/main/test/e2e/scenarios/KICK-E2E-071-kargo-ambiguous-stage-blocks) ## Feature mapping - Kargo stage promotion gate: [KICK-FEAT-025](https://github.com/corewire/kick/blob/main/traceability/features.yaml) ## Source: docs/content/docs/guides/troubleshooting.md # Troubleshooting ## KickRequest stays in waiting phase - `WaitingForOwner`: owner missing or ambiguous. - `WaitingForGate`: provider schedule/window blocks restart. - `WaitingForApplicationSync`: Argo CD app is not synced. - `WaitingForRollout`: existing rollout still active. Check: ```bash kubectl --kubeconfig .kubeconfig-kind-kick-dev --context kind-kick-dev -n describe kickrequest ``` ## Request fails Typical causes: - rollout timeout; - patch rejection; - provider query failure. Inspect controller logs and request conditions. ## No request created after source update Confirm the source is a supported dependency type and consumed by a managed Deployment. ## Source: docs/content/docs/guides/without-gitops.md --- title: Running without GitOps weight: 45 description: KICK does not require Argo CD, Flux or Kargo. GitOps gating is opt-in. llmsDescription: | KickPolicy spec.gitOps.provider defaults to None, so KICK restarts workloads on a plain Kubernetes cluster with no GitOps controller installed. Auto detection blocks when no provider recognises the workload, so Auto must not be used on a cluster without Argo CD, Flux or Kargo. Kargo is never auto-detected and must be selected explicitly. --- GitOps gating is the feature KICK is best known for, but it is **not** a requirement. `spec.gitOps.provider` defaults to `None`, and a policy with no `gitOps` block at all restarts workloads on any Kubernetes cluster. ## Minimal policy ```yaml apiVersion: kick.corewire.io/v1alpha1 kind: KickPolicy metadata: name: default namespace: team-a spec: discovery: workloadSelector: matchLabels: kick.corewire.io/enabled: "true" ``` No `gitOps` block means no ownership resolution and no gate evaluation. A detected change goes straight to the freshness check and then to the restart. Cron windows, rate limiting and `dryRun` all work unchanged. ## Choosing a provider | `spec.gitOps.provider` | Behaviour | |---|---| | `None` (default) | No ownership resolution, no gate. Restarts proceed. | | `Auto` | Ask every registered provider to identify the owner. | | `ArgoCD` | Resolve the owning `Application` and its `AppProject` sync windows. | | `Flux` | Resolve the owning `Kustomization` or `HelmRelease` and require `Ready`. | | `Kargo` | Resolve the authorised Kargo `Stage`, then delegate to Argo CD. | ## Do not use `Auto` without a GitOps controller `Auto` asks each provider to detect ownership. On a cluster with neither Argo CD nor Flux installed, no provider is confident, the gate resolves to `ProviderUnavailable`, and the `KickRequest` waits indefinitely. This is intentional — silently restarting a workload whose ownership could not be established would violate KICK's core safety rule — but it means `Auto` is the wrong choice here. Use `None`. ## Kargo is never auto-detected Kargo does not write to workloads; Argo CD does. A Kargo-managed workload therefore looks exactly like an Argo CD-managed one, and detection cannot tell them apart. Set `provider: Kargo` explicitly to also gate on in-flight Stage promotions. ## What you still get - Change detection for env, `envFrom`, volume and Secrets Store CSI references - Freshness comparison against the running rollout, so redundant restarts are skipped - Cron restart windows and per-policy rate limiting - Durable `KickRequest` objects, events, metrics and the timeline API - `dryRun` previews and `NotificationPolicy` webhooks ## Source: docs/content/docs/installation.md --- title: Installation weight: 10 aliases: - /docs/getting-started/ - /docs/getting-started/installation/ description: Install the KICK operator with Helm, or from source for local development. llmsDescription: | Installation guide for the KICK operator. Prerequisites: Kubernetes 1.28+, Helm 3.12+. Install via the Helm chart (oci://ghcr.io/corewire/charts/kick) into namespace kick-system. Key values: integrations.argocd.enabled, image.repository/tag. Helm installs CRDs on first install but does not upgrade them — re-apply the CRDs with kubectl on upgrade. A from-source path (kind + make) is provided for local development. --- KICK ships as a single controller plus two cluster-scoped CRDs (`KickPolicy`, `KickRequest`). The recommended install is the Helm chart. ## Prerequisites - Kubernetes 1.28+ - Helm 3.12+ - (optional) Argo CD, if you want to gate restarts behind GitOps ## Helm install ```bash helm install kick oci://ghcr.io/corewire/charts/kick \ --namespace kick-system \ --create-namespace ``` That installs the CRDs, RBAC, and the controller `Deployment`. ### Common values Override with `--set key=value` or a `-f values.yaml` file: | Value | Default | Description | |-------|---------|-------------| | `image.repository` | `ghcr.io/corewire/kick` | Controller image. | | `image.tag` | chart `appVersion` | Controller image tag. | | `integrations.argocd.enabled` | `true` | Grant RBAC to read Argo CD `Application`/`AppProject` for GitOps gating. | | `integrations.argocd.applicationNamespaces` | `[]` | Namespaces KICK may read Argo CD objects from. Empty means Argo CD namespace only. | | `integrations.flux.enabled` | `true` | Grant RBAC to read Flux `Kustomizations`/`HelmReleases` for GitOps gating. | | `integrations.kargo.enabled` | `false` | Grant RBAC to read Kargo `Stages`/`Promotions` for GitOps gating. | | `leaderElection.enabled` | `true` | Enable leader election for HA. | | `replicaCount` | `1` | Controller replicas. | If you do not use Argo CD, disable its RBAC: ```bash helm install kick oci://ghcr.io/corewire/charts/kick \ --namespace kick-system --create-namespace \ --set integrations.argocd.enabled=false ``` ### Upgrading CRDs Helm installs the CRDs on first install but **does not** upgrade them on `helm upgrade`. When a release changes the CRDs, re-apply them explicitly: ```bash kubectl apply -f https://raw.githubusercontent.com/corewire/kick/main/config/crd/bases/kick.corewire.io_kickpolicies.yaml kubectl apply -f https://raw.githubusercontent.com/corewire/kick/main/config/crd/bases/kick.corewire.io_kickrequests.yaml ``` ## From source (local development) For a throwaway Kind cluster with the controller built from your working tree: ```bash make kind-create # create the kind-kick-dev cluster make kind-load # build and load the controller image make install # install CRDs + controller ``` Local defaults: context `kind-kick-dev`, kubeconfig `.kubeconfig-kind-kick-dev`. You can also install the chart directly from the repo checkout: ```bash helm install kick charts/kick --namespace kick-system --create-namespace ``` ## Verify ```bash kubectl -n kick-system get pods kubectl get crd | grep kick.corewire.io ``` The controller Pod should be `Running` and both CRDs (`kickpolicies`, `kickrequests`) present. ## Next steps - [Quickstart](../quickstart/) — see KICK restart a Deployment when a Secret changes. - [Concepts](../concepts/) — how discovery, freshness, and GitOps gating work. ## Source: docs/content/docs/operations/_index.md --- title: Operations weight: 60 --- Running KICK in production: RBAC, security posture, scalability, and upgrades. ## Source: docs/content/docs/operations/rbac.md # RBAC Controller ClusterRole includes: - `get/list/watch` on `secrets` and `configmaps`; - `get/list/watch/patch` on `deployments` and `replicasets`; - full CRUD on `kickrequests` plus status updates; - `get/list/watch` on `kickpolicies`; - lease permissions for leader election. Source of truth: `config/rbac/role.yaml`. ## Source: docs/content/docs/operations/scalability.md # Scalability Initial implementation targets correctness over horizontal scale optimization. Current characteristics: - one KickRequest per target Deployment; - request coalescing prevents duplicate restarts for the same target; - gate and freshness checks are recomputed from live state before action. Operational guidance: - monitor `kick_controller_errors_total` and queue behavior; - scale controller resources before increasing managed namespace count; - validate provider API rate behavior in your environment. ## Source: docs/content/docs/operations/security.md # Security KICK requires read access to Secrets and ConfigMaps in managed namespaces to evaluate dependency freshness. Implications: - treat controller ServiceAccount as sensitive; - restrict namespace scope where possible; - avoid exposing logs broadly. KICK safety constraints: - no privileged containers; - no CRI socket access; - no Secret value logging. ## Source: docs/content/docs/operations/upgrades.md # Upgrades API maturity: `v1alpha1`. Policy: - breaking changes may occur before beta; - when production users persist objects, migrations or conversion guidance must be documented. Upgrade flow: 1. review release notes; 2. apply updated CRDs; 3. upgrade chart image/tag; 4. watch controller readiness and KickRequest reconciliation. ## Source: docs/content/docs/quickstart.md --- title: Quickstart weight: 20 aliases: - /docs/getting-started/quickstart/ description: Watch KICK restart a Deployment when its Secret changes — no GitOps tool required. --- This validates the end-to-end KICK flow on a Kind cluster: 1. install KICK; 2. apply a Deployment that reads a Secret, plus a `KickPolicy`; 3. change the Secret; 4. watch KICK restart the Deployment. > The local dev commands use context `kind-kick-dev` and kubeconfig > `.kubeconfig-kind-kick-dev`. Add `--context` / `--kubeconfig` to match your setup. ## 1) Install KICK ```bash make kind-create make kind-load make install ``` Already have a cluster? Install the chart instead — see [Installation](../installation/). ## 2) Apply a workload and a policy ```bash kubectl -n shop apply -f - <<'EOF' apiVersion: v1 kind: Namespace metadata: { name: shop } --- apiVersion: v1 kind: Secret metadata: { name: web-secret, namespace: shop } type: Opaque stringData: { API_TOKEN: alpha } --- apiVersion: apps/v1 kind: Deployment metadata: { name: web, namespace: shop, labels: { app: web } } spec: replicas: 1 selector: { matchLabels: { app: web } } template: metadata: { labels: { app: web } } spec: containers: - name: app image: nginx envFrom: - secretRef: { name: web-secret } --- apiVersion: kick.corewire.io/v1alpha1 kind: KickPolicy metadata: { name: web, namespace: shop } spec: discovery: workloadSelector: matchLabels: { app: web } EOF ``` ## 3) Change the Secret ```bash kubectl -n shop patch secret web-secret --type merge \ -p '{"stringData":{"API_TOKEN":"bravo"}}' ``` ## 4) Observe the request and rollout ```bash kubectl -n shop get kickrequests -w kubectl -n shop rollout status deploy/web --timeout=5m ``` Success signal: - a `KickRequest` appears and reaches `Succeeded` or `NoLongerRequired`; - the Deployment starts a fresh rollout. ## Optional: gate on Argo CD To make restarts respect Argo CD ownership and sync windows, install Argo CD and set `spec.gitOps.provider: Auto` on the policy: ```bash kubectl create namespace argocd kubectl apply -n argocd -f https://raw.githubusercontent.com/argoproj/argo-cd/stable/manifests/install.yaml kubectl -n argocd rollout status deploy/argocd-server --timeout=5m ``` See the [Argo CD guide](../guides/argocd/) for a full Argo CD-tracked workload and policy. ## Source: docs/content/docs/reference/_index.md --- title: Reference weight: 50 --- API fields, metrics, events, and configuration for the KICK operator. ## Source: docs/content/docs/reference/configuration.md # Configuration Reference Primary configuration surface: Helm chart values in `charts/kick/values.yaml`. | Key | Description | Default | | --- | --- | --- | | `namespace.name` | Controller namespace | `kick-system` | | `requestRetention` | Completed request retention duration | `24h` | | `rolloutTimeout` | Restart rollout timeout | `15m` | | `leaderElection.enabled` | Enable leader election | `true` | | `podDisruptionBudget.enabled` | Protect single replica availability | `true` | | `resources` | Controller CPU/memory requests/limits | set in values | | `integrations.argocd.enabled` | Enable Argo CD adapter logic | `true` | | `integrations.argocd.namespace` | Argo CD control-plane namespace | `argocd` | | `integrations.argocd.applicationNamespaces` | Application namespaces to search | `[]` | | `integrations.flux.enabled` | Enable Flux adapter logic | `true` | | `integrations.argoRollouts.enabled` | Treat `argoproj.io` Rollouts as restartable workloads, and grant RBAC for them | `false` | | `integrations.secretsStoreCSI.enabled` | Observe `SecretProviderClassPodStatus` for Secrets Store CSI rotation, and grant RBAC for it | `false` | | `integrations.kargo.enabled` | Grant RBAC for Kargo `Stages` and `Promotions` so `provider: Kargo` can be used | `false` | Example: ```yaml integrations: argocd: enabled: true applicationNamespaces: [] flux: enabled: true argoRollouts: enabled: true secretsStoreCSI: enabled: true kargo: enabled: false ``` ## Manager flags | Flag | Description | Default | | --- | --- | --- | | `--rollout-timeout` | Maximum time KICK waits for rollout convergence before marking a request timed out. | `15m` | | `--enable-argocd` | Gate restarts on Argo CD Application state. Ignored when the CRD is absent. | `true` | | `--enable-flux` | Gate restarts on Flux Kustomization and HelmRelease state. Ignored when CRDs are absent. | `true` | | `--enable-kargo` | Block restarts while a Kargo Promotion is active. Ignored when the CRD is absent. | `false` | | `--enable-argo-rollouts` | Watch and restart `argoproj.io/v1alpha1` Rollouts. Ignored when the CRD is absent. | `false` | | `--enable-csi-integration` | Watch `SecretProviderClassPodStatus`. Ignored when the CRD is absent. | `false` | Every integration additionally requires its CRD to exist in the cluster; the manager probes the REST mapper at startup and skips the integration rather than failing. The Helm chart sets these flags from matching `integrations.*.enabled` values. ### Detection happens once at start-up Integrations — Argo CD, Flux, Kargo, Argo Rollouts and Secrets Store CSI — are detected exactly once while the manager starts. If their CRDs are installed later, the integration stays inactive until the KICK manager pod is restarted. The manager logs one line per skipped integration at start-up, naming the integration and the group/version/kind it did not find, so `kubectl logs` on the manager pod tells you what is inactive. The probe is deliberately not dynamic: registering a watch or a field index for a kind that does not exist aborts the manager, so the check has to happen before the manager runs. Controller runtime defaults and remaining flags are defined by chart templates and manager args in Kubernetes manifests. ## Source: docs/content/docs/reference/events.md # Events Reference KICK emits typed Kubernetes events with stable reason strings. Reasons: - `WaitingForSchedule` - `WaitingForGitOpsSync` - `WaitingForRollout` - `KickStarted` - `KickSucceeded` - `KickNoLongerRequired` - `KickFailed` - `KickDryRun` - `OwnerUnknown` - `OwnerAmbiguous` Use `kubectl describe kickrequest ` to inspect event history for a request. `KickDryRun` is emitted instead of `KickStarted`/`KickSucceeded` when the matching KickPolicy sets `spec.dryRun: true`. The restart is never performed. ## Source: docs/content/docs/reference/kickpolicy.md # KickPolicy API Reference Group/version: `kick.corewire.io/v1alpha1` Kind: `KickPolicy` ## spec.discovery Two optional label selectors scope the policy. An empty or omitted selector matches everything on its axis. - `workloadSelector` — which workloads the policy manages (the actors that may be restarted). Omit to match all supported workloads in the namespace. - `dependencySelector` — which consumed `Secret`/`ConfigMap` changes count as a trigger. Omit to treat every discovered dependency as a trigger. A workload restarts when it **consumes a changed dependency**, the workload is in `workloadSelector` scope, and the changed dependency is in `dependencySelector` scope. `dependencySelector` also scopes freshness: out-of-scope dependencies are ignored entirely. ## spec.schedule `spec.schedule` is the KICK-native time gate: pure scheduling, evaluated without any GitOps provider. Omit it to allow restarts at any time. - `windows[]` KICK-native restart windows - `type` enum: `Allow`, `Deny` (required) - `cron` 5-field cron expression marking each window start (required) - `duration` how long the window stays open from each start, e.g. `1h` (required) - `timeZone` IANA zone used to evaluate the cron expression (default UTC) ## spec.gitOps `spec.gitOps` is optional. When omitted, `provider` defaults to `None` and KICK restarts without consulting a GitOps tool (gated only by any native windows). - `provider` enum: `None`, `Auto`, `ArgoCD`, `Flux`, `Kargo` (default `None`) - `requireReconciled` default: `true` (applies only to a real provider) `Kargo` is never auto-detected and must be selected explicitly: Kargo does not write to workloads, Argo CD does, so a Kargo-managed workload is indistinguishable from a plain Argo CD one. With `Kargo`, KICK resolves the authorised `Stage` from the owning Application's `kargo.akuity.io/authorized-stage` annotation, blocks while a Promotion for that Stage is in flight, and then delegates to the Argo CD gate. More than one authorised stage is treated as ambiguous ownership and blocks. See [Running without GitOps](../guides/without-gitops/) for the `None` case. ## spec.restart - `minInterval` default: `30s` ## spec - `suspend` pauses the policy without deleting it (default `false`) - `dryRun` evaluates every gate and the freshness check but never patches a workload (default `false`). The `KickRequest` ends in the terminal `DryRun` phase with the decision recorded in its conditions, so you can see exactly what would have happened. ## status - `observedGeneration` - `matchedWorkloads` - `blockedWorkloads` - `conditions` ## Source: docs/content/docs/reference/kickrequest.md # KickRequest API Reference Group/version: `kick.corewire.io/v1alpha1` Kind: `KickRequest` ## spec - `targetRef.apiVersion` default: `apps/v1`. Use `argoproj.io/v1alpha1` for an Argo Rollout. - `targetRef.kind` enum: `Deployment`, `StatefulSet`, `DaemonSet`, `Rollout` - `targetRef.name` required `Rollout` is only accepted with `apiVersion: argoproj.io/v1alpha1`, and requires the controller to run with `--enable-argo-rollouts` and the CRD to be present. ## status - `phase` enum: - `Pending` - `WaitingForGate` - `WaitingForOwner` - `WaitingForApplicationSync` - `WaitingForRollout` - `Executing` - `Succeeded` - `NoLongerRequired` - `Failed` - `DryRun` — terminal. The policy had `spec.dryRun: true`, so everything was evaluated but no workload was patched. - `owner`: resolved GitOps owner details - `gate`: last gate decision (`reason`, `message`, `requeueAt`) - `latestObservedDependencyChange` - `currentRollout` (`replicaSet`, `startedAt`) - `conditions` ## Mutated fields KICK mutates: - `status.*` on KickRequest; - `metadata.annotations["kubectl.kubernetes.io/restartedAt"]` on the target Deployment, StatefulSet or DaemonSet PodTemplate; - `spec.restartAt` on a target Argo `Rollout`. The pod template is deliberately left untouched so the canary or blue-green strategy is not re-run for a configuration change. KICK never writes dependency hashes, environment variables, or KICK-owned state annotations into a workload. ## Source: docs/content/docs/reference/metrics.md # Metrics Reference Prometheus metrics are registered in controller-runtime metrics registry. ## `kick_requests_total` Counter by labels: - `provider` — GitOps provider name, or `unknown` when none applies - `result` — `succeeded`, `no_longer_required`, `failed`, `dry_run` Meaning: completed KickRequest outcomes. `dry_run` is recorded when a policy sets `spec.dryRun: true`; the request reaches a terminal decision without a restart being performed. ## `kick_restarts_total` Counter by labels: - `provider` — GitOps provider name, or `unknown` when none applies - `result` — `started`, `succeeded`, `failed` Meaning: restart execution attempts and outcomes. ## `kick_controller_errors_total` Counter by labels: - `controller` - `reason` — `Unknown` when no reason is supplied Meaning: controller reconciliation errors. ## `kick_notification_deliveries_total` Counter by labels: - `namespace` — namespace of the NotificationPolicy - `policy` — NotificationPolicy name - `outcome` — `success` or `failure` Meaning: NotificationPolicy webhook delivery attempts. A failed delivery never fails a restart. ## `kick_notification_dropped_total` Counter, no labels. Meaning: notification events discarded because the delivery queue was full. A non-zero value means the webhook endpoint is not keeping up. ## Source: docs/content/docs/reference/notificationpolicy.md # NotificationPolicy API Reference Group/version: `kick.corewire.io/v1alpha1` Kind: `NotificationPolicy` (namespaced) A `NotificationPolicy` delivers an HTTP webhook when a `KickRequest` in the same namespace reaches a selected phase. Delivery is best-effort: a failed webhook never fails a restart. ## spec - `suspend` pauses delivery without deleting the policy (default `false`) - `phases[]` which `KickRequest` phases to deliver. Defaults to the terminal phases `Succeeded`, `Failed`, `NoLongerRequired`, `DryRun`. - `workloadSelector` optional label selector on the `KickRequest` labels. Omit to match every request in the namespace. ## spec.webhook - `url` required, must match `^https?://` - `method` enum `POST`, `PUT` (default `POST`) - `timeoutSeconds` default `10`, min `1`, max `120` - `headers[]` static headers - `name` required - `value` literal value - `valueFrom.name` / `valueFrom.key` read the value from a `Secret` in the same namespace - `auth.bearerToken.name` / `.key` — `Authorization: Bearer ` - `auth.basic.username` / `auth.basic.password` — each a Secret key reference - `tls.caBundle.name` / `.key` — PEM bundle used to verify the server - `tls.clientCertificate.name` / `.key` — a `kubernetes.io/tls` Secret used for mutual TLS All credentials are Secret references. Literal credentials cannot be set inline. ## Payload The request body is JSON with a fixed field set: ```json { "namespace": "team-a", "requestName": "api-0f3c", "phase": "Succeeded", "reason": "RestartCompleted", "message": "rollout completed", "targetKind": "Deployment", "targetName": "api", "gitOpsProvider": "argocd", "occurredAt": "2026-03-01T12:00:00Z" } ``` The payload never contains `Secret` or `ConfigMap` data, key names, or content digests. ## Delivery semantics - Events are queued in memory. The queue is bounded; when it is full the oldest event is dropped and `kick_notification_dropped_total` is incremented. - Delivery is retried up to three times with exponential backoff. `4xx` responses other than `429` are not retried. - Delivery runs only on the elected leader, so a highly-available deployment does not duplicate webhooks. - TLS is negotiated with a minimum version of TLS 1.2. ## status - `observedGeneration` - `lastDeliveryTime` - `lastError` - `delivered`, `failed` counters - `conditions` ## Metrics - `kick_notification_deliveries_total{namespace,policy,outcome}` - `kick_notification_dropped_total` ## Source: docs/content/docs/theory/_index.md --- title: Theory weight: 70 --- A formal, self-contained model of the KICK operator: state, observation, freshness, gating, the reconcile transition system, and the safety and liveness properties it maintains. ## Source: docs/content/docs/theory/operator-model.md --- title: The KICK operator, formally linkTitle: Operator model weight: 10 math: true description: A self-contained formal model of the KICK operator — state, observation, freshness, gating, the reconcile transition system, and its safety and liveness properties. llmsDescription: | Formal model of the KICK operator in scientific notation. Defines the cluster state, dependency extraction, source fingerprints and the observation store, the sub-second timestamp precision the freshness comparison requires, the freshness/staleness predicate, the gate function (native windows + GitOps provider), the KickRequest transition system, coalescing, and the restart action. States and proves the operator invariants: no spurious baseline restart, eventual restart on relevant change, kind-agnostic freshness, at-most-one active request per target, gate safety, non-injection of workload state, and source-driven evaluation. --- This page gives a precise, self-contained model of what the KICK controller does. It is written for readers who want to reason about correctness rather than read Go. Every definition corresponds to a concrete piece of the controller, and the invariants at the end are the properties the implementation is designed to preserve. {{< callout type="info" >}} Notation is standard set theory and first-order logic. We write \(2^{X}\) for the power set of \(X\), \(f : X \rightharpoonup Y\) for a partial function, and \(\lnot,\ \land,\ \lor,\ \Rightarrow\) for the usual connectives. {{< /callout >}} ## 1. Objects and state Let a point in time be \(t \in \mathbb{R}_{\ge 0}\). The observable cluster state at time \(t\) is a tuple $$ \mathcal{C}(t) = \big(\, W,\; S,\; \mathsf{data},\; \mathsf{tmpl},\; \mathsf{rs} \,\big), $$ whose components are: - \(W\) — the set of **workloads**. Each \(w \in W\) has a kind \(\kappa(w) \in \{\textsf{Deployment},\ \textsf{StatefulSet},\ \textsf{DaemonSet}\}\) and a namespace \(\mathsf{ns}(w)\). - \(S\) — the set of **sources**, i.e. objects of kind \(\textsf{Secret}\) or \(\textsf{ConfigMap}\). Each \(s \in S\) has a creation time \(\gamma(s)\), a resource version \(\rho(s) \in \mathbb{N}\), and a **last-write time** \(\lambda(s) = \max\big(\gamma(s),\ \max_{m \in \mathsf{mgr}(s)} \tau(m)\big)\), where \(\mathsf{mgr}(s)\) are the server-side field-management entries of \(s\) and \(\tau(m)\) is the time the API server recorded for the last write of manager \(m\). By construction \(\lambda(s) \ge \gamma(s)\), and \(\lambda(s) = \gamma(s)\) for a source never written since its creation. - \(\mathsf{data}(s)\) — the key/value payload of a source (`data` + `binaryData`/`stringData`), together with its `type` and immutability flag. - \(\mathsf{tmpl}(w)\) — the Pod template of a workload, in particular its annotation map \(\mathsf{tmpl}(w).\mathsf{ann}\). - \(\mathsf{rs}(w)\) — for a Deployment, the set of its ReplicaSets; the *current* one is \(\mathsf{rs}^{\star}(w)\). The controller never mutates \(\mathsf{data}\); its only write to cluster state is a single annotation on \(\mathsf{tmpl}(w)\), defined in §8. ## 2. Dependency extraction A workload consumes a source when its Pod template references it through an environment variable, an `envFrom`, a volume, or a projected volume source, in any container or init container. Image-pull secrets are **not** consumed data and are excluded. This is captured by a pure function $$ \mathsf{deps} : W \longrightarrow 2^{S}, \qquad \mathsf{deps}(w) = \big\{\, s \in S \ \mid\ s \text{ is referenced by } \mathsf{tmpl}(w) \text{ as data} \,\big\}. $$ \(\mathsf{deps}\) is deterministic and depends only on \(\mathsf{tmpl}(w)\); it is insensitive to reference multiplicity, so a source referenced twice contributes once. Its inverse image gives the **consumers** of a source, $$ \mathsf{cons}(s) = \{\, w \in W \ \mid\ s \in \mathsf{deps}(w) \,\}, $$ restricted to \(\mathsf{ns}(w) = \mathsf{ns}(s)\). ## 3. Fingerprints and relevant change To distinguish a change that matters from one that does not, each source is reduced to a content **fingerprint** by a collision-resistant hash \(H = \mathrm{SHA\text{-}256}\): $$ \phi(s) \;=\; H\!\Big(\,\textsf{type}(s)\ \Vert\ \textsf{imm}(s)\ \Vert \mathop{\Big\Vert}\limits_{k \in \mathrm{sort}(\mathrm{keys})} \big(k \Vert \mathsf{data}(s)[k]\big)\Big) \ \in\ \{0,1\}^{256}. $$ Keys are sorted so the fingerprint is canonical, and only the payload, type, and immutability enter it. Object metadata — labels, annotations, `resourceVersion`, managed fields — is deliberately excluded. Hence $$ \phi(s) = \phi(s') \iff \mathsf{data}(s) = \mathsf{data}(s') \ \land\ \textsf{type}(s)=\textsf{type}(s') \ \land\ \textsf{imm}(s)=\textsf{imm}(s'). $$ A **relevant change** to a source is exactly a change of \(\phi\); a change of \(\rho\) with fixed \(\phi\) is *metadata-only*. ## 4. The observation store The controller maintains a durable partial map from a source identity to a record, $$ \Omega : \mathrm{Id}(S) \rightharpoonup \mathcal{R}, \qquad \mathcal{R} = \big(\,\rho_{\text{seen}},\ \rho_{\text{rel}},\ \theta,\ \varphi\,\big), $$ where \(\theta\) is the time of the last relevant change and \(\varphi\) the last relevant fingerprint. When the controller observes a source \(s\) at wall-clock time \(t\), the store transition \(\mathsf{obs}\) classifies the event and updates the record: $$ \mathsf{obs}(\Omega, s, t) = \begin{cases} \textsf{Baseline} & s \notin \operatorname{dom}\Omega, \\[2pt] \textsf{NoChange} & \varphi = \phi(s)\ \land\ \rho_{\text{seen}} = \rho(s), \\[2pt] \textsf{MetaOnly} & \varphi = \phi(s)\ \land\ \rho_{\text{seen}} \neq \rho(s), \\[2pt] \textsf{Relevant} & \varphi \neq \phi(s). \end{cases} $$ The recorded change time is the crux of baseline correctness: $$ \theta' = \begin{cases} \beta(s) & \text{on }\textsf{Baseline}\quad(\text{the source's last recorded write}),\\ t & \text{on }\textsf{Relevant}\quad(\text{observed “now”}),\\ \theta & \text{on }\textsf{NoChange},\ \textsf{MetaOnly}, \end{cases} \qquad \beta(s) = \lambda(s). $$ {{< callout type="info" >}} **Why baseline uses \(\beta(s)\) and not \(t\) or \(\gamma(s)\).** A first observation never witnessed the change that produced the content it sees, so it must date that content from evidence. Using the wall-clock observation instant \(t\) would make freshness depend on a race between "KICK first saw the Secret" and "the ReplicaSet was created", producing spurious restarts whenever a workload is adopted. Using \(\gamma(s)\) is unsound in the other direction: if \(s\) was written after its creation but before KICK first observed it — KICK was installed, restarting, or its cache had not synced — then \(\gamma(s) \le \sigma(w)\) even though the content is newer than the rollout, and since every later observation matches that baseline the change is dismissed as fresh and never reconsidered. \(\lambda(s)\) is the tightest evidence the API server records for "when did this content come to be": it is the latest write the server itself attributes to the object. The baseline therefore takes \(\lambda(s)\) unmodified. The residual ambiguity — the API server stores that instant with second granularity — is handled by carrying change times at sub-second precision (§5), not by widening the baseline: an artificially advanced baseline dates content later than it provably is and causes spurious restarts when KICK adopts an existing cluster. {{< /callout >}} Only \(\textsf{Baseline}\) and \(\textsf{Relevant}\) enqueue work for the consumers \(\mathsf{cons}(s)\); \(\textsf{NoChange}\) and \(\textsf{MetaOnly}\) are inert. ## 5. Timestamp precision Kubernetes records object timestamps — `creationTimestamp`, `managedFields` entry times, Deployment condition times — with **whole-second** granularity. Every quantity that enters \(\sigma(w)\) is therefore second-granular, while a change time \(\theta\) observed by the controller is not. Change times must be carried at sub-second precision at every hop: the durable observation record stores \(\theta\) as RFC 3339 with nanosecond precision, and the KickRequest status field `latestObservedDependencyChange` is a `metav1.MicroTime` rather than a `metav1.Time`. The failure mode is exact. Truncation to whole seconds is the map \(t \mapsto \lfloor t \rfloor\). Let a relevant change occur at \(\theta = \sigma(w) + \varepsilon\) with \(0 < \varepsilon < 1\mathrm{s}\), so the change is genuinely newer than the rollout it must supersede. Truncation gives \(\lfloor \theta \rfloor = \sigma(w)\), and because the staleness test \(\Lambda(w) > \sigma(w)\) of §6 is strict, $$ \Lambda(w) > \sigma(w) \quad\text{but}\quad \lfloor \Lambda(w) \rfloor \not> \sigma(w), $$ so the workload is wrongly declared fresh and the change is lost. Any change falling in the same second as the rollout it supersedes is affected. Hence no component on the path from the observation store to the freshness comparison — record serialisation, status write, status read, coalescing — may truncate. ## 6. Rollout state and the freshness relation For a workload \(w\) the rollout inspector returns a **start time** and a **completeness** flag, $$ \mathsf{R}(w) = \big(\sigma(w),\ \mathrm{complete}(w)\big). $$ The start time is the latest moment at which the currently-running Pod template is provably in place. Writing \(\mathrm{cond}(w)\) for the workload's status conditions and \(\upsilon(c)\) for the `lastUpdateTime` of a condition \(c\), or its `lastTransitionTime` when the former is unset: $$ \sigma(w) = \begin{cases} \mathsf{tmpl}(w).\mathsf{ann}[\textsf{restartedAt}] & \text{if that annotation is set},\\[4pt] \max\Big(\gamma\!\big(\mathsf{rs}^{\star}(w)\big),\ \max\limits_{c\,\in\,\mathrm{cond}(w)} \upsilon(c)\Big) & \kappa(w)=\textsf{Deployment},\\[6pt] \max\Big(\gamma(w),\ \max\limits_{c\,\in\,\mathrm{cond}(w)} \upsilon(c)\Big) & \kappa(w)=\textsf{DaemonSet},\\[6pt] \gamma(w) & \kappa(w)=\textsf{StatefulSet}, \end{cases} $$ with \(\max_{c \in \varnothing} \upsilon(c) = -\infty\). For a Deployment the current ReplicaSet's creation time is advanced to the latest condition update, which is effectively the moment the rollout became available and complete. This is deliberate: \(\sigma(w)\) must be the *latest* instant that is provably true, because a source change may only be dismissed when the running Pods provably already carry it. A DaemonSet falls back to its latest condition transition when one is present; upstream rarely populates DaemonSet conditions, so in practice the creation time is used. For StatefulSets and DaemonSets no comparable completion timestamp exists, so \(\sigma(w)\) there is only a *lower* bound on when the Pods began running. A source created after the workload object but before its Pods started is consequently counted as newer and produces exactly one adoption restart. Manifest ordering makes this rare — Helm and Argo CD apply Secrets and ConfigMaps before the workloads that consume them — and one extra restart is the safe side of the ambiguity. Completeness is **kind-aware** — this is what lets non-Deployment workloads be evaluated at all: $$ \mathrm{complete}(w) = \begin{cases} \big(\mathsf{rs}^{\star}(w)\neq\varnothing\big)\ \land\ \lnot\text{InProgress}(w)\ \land\ \lnot\text{Failed}(w) & \kappa(w)=\textsf{Deployment},\\[4pt] \mathrm{observedGen}(w) = \mathrm{gen}(w)\ \land\ \text{replicas up to date} & \kappa(w)\in\{\textsf{StatefulSet},\textsf{DaemonSet}\}. \end{cases} $$ Given the dependency scope \(D(w) \subseteq \mathsf{deps}(w)\) selected by the policy (§7), the **latest relevant change** seen for \(w\) is $$ \Lambda(w) \;=\; \max_{\,d \,\in\, D(w)\,\cap\,\operatorname{dom}\Omega} \Omega(d).\theta, \qquad \Lambda(w) = -\infty \ \text{ if the set is empty.} $$ The workload is **stale** exactly when its rollout is complete yet older than the latest relevant dependency change: $$ \boxed{\ \mathrm{stale}(w) \;\iff\; \mathrm{complete}(w)\ \land\ \Lambda(w) > \sigma(w)\ } $$ If \(\lnot\,\mathrm{complete}(w)\) the workload is *in progress* and no freshness decision is taken (the request waits). The comparison \( \Lambda(w) > \sigma(w)\) is strict, so equal timestamps count as fresh. ## 7. Policy scope and the gate A `KickPolicy` selects which workloads it manages and which of their dependency changes may trigger a restart, via two label selectors: $$ D(w) = \{\, s \in \mathsf{deps}(w) \ \mid\ \mathsf{labels}(s) \models \text{dependencySelector} \,\}, \qquad w \text{ managed} \iff \mathsf{labels}(w) \models \text{workloadSelector}. $$ The **gate** decides whether a permitted-in-principle restart may run *now*: $$ \mathsf{G} : W \times \mathbb{R}_{\ge 0} \to \{\textsf{Allowed}\} \cup \{\textsf{Blocked}(r) : r \in \mathcal{Q}\}, $$ with blocking reasons \(\mathcal{Q} = \{\textsf{OutsideSchedule},\ \textsf{OwnerUnknown},\ \textsf{MultipleOwners},\ \textsf{OutOfSync},\ \textsf{SyncInProgress},\dots\}\). It is evaluated in two stages. First, KICK-native schedule windows, if any, are applied: a set of allow/deny cron windows \(\{(\text{kind}_i,\text{cron}_i,\text{dur}_i)\}\) induces the predicate $$ \mathrm{open}(t) \;=\; \Big(\exists i:\ \text{kind}_i=\textsf{Allow}\ \land\ t \in \mathrm{window}_i\Big)\ \land\ \Big(\lnot\exists j:\ \text{kind}_j=\textsf{Deny}\ \land\ t \in \mathrm{window}_j\Big), $$ and \(\lnot\mathrm{open}(t)\) yields \(\textsf{Blocked}(\textsf{OutsideSchedule})\). Second, the GitOps provider \(P \in \{\textsf{None},\textsf{Auto},\textsf{ArgoCD},\textsf{Flux}\}\) is consulted. With \(P=\textsf{None}\) (the default) KICK self-gates and the stage returns \(\textsf{Allowed}\). Otherwise the owner-resolution relation \(\mathsf{own}(w)\) must yield exactly one owner whose application is reconciled/synced: $$ \mathsf{G}(w,t) = \textsf{Allowed} \iff \mathrm{open}(t)\ \land\ \Big(P=\textsf{None}\ \lor\ \big(|\mathsf{own}(w)|=1\ \land\ \mathrm{synced}(\mathsf{own}(w))\big)\Big). $$ ## 8. The restart action Restarting is the single side effect KICK performs on a workload. It stamps the Pod template with the standard annotation, which forces the workload controller to roll a new revision: $$ \mathsf{A}(w)\ :\quad \mathsf{tmpl}(w).\mathsf{ann}[\textsf{restartedAt}] \;\leftarrow\; \mathrm{now}. $$ By the definition of \(\sigma\) in §6, immediately after \(\mathsf{A}(w)\) we have \(\sigma(w) = \mathrm{now} \ge \Lambda(w)\), so the workload is no longer stale. KICK writes **no** other state: no content hashes, no environment variables, no owner annotations. The action is the only writer of \(\textsf{restartedAt}\) in this model. ## 9. The KickRequest transition system Discovery and coalescing (§10) produce at most one `KickRequest` per target workload. A request is a state machine over phases $$ \Phi = \{\textsf{Pending},\ \textsf{WaitingForGate},\ \textsf{WaitingForOwner},\ \textsf{WaitingForApplicationSync},\ \textsf{WaitingForRollout},\ \textsf{Executing},\ \textsf{Succeeded},\ \textsf{NoLongerRequired},\ \textsf{Failed}\}, $$ with terminal set \(\mathsf{Term} = \{\textsf{Succeeded},\ \textsf{NoLongerRequired},\ \textsf{Failed}\}\). Each request carries a rollout marker \(\mu \in \{\bot\} \cup \mathbb{R}_{\ge 0}\) (the start time of the rollout it is currently driving). One reconcile step \(\delta\) applied to a non-terminal request evaluates, in order, the gate, the freshness relation, and the executor: $$ \delta(w) = \begin{cases} \textsf{WaitingForGate}/\textsf{WaitingForOwner}/\ldots & \text{if } \mathsf{G}(w,t)=\textsf{Blocked}(r),\\[2pt] \textsf{WaitingForRollout} & \text{if } \mathsf{G}=\textsf{Allowed}\ \land\ \lnot\,\mathrm{complete}(w),\\[2pt] \textsf{NoLongerRequired} & \text{if } \mathsf{G}=\textsf{Allowed}\ \land\ \mathrm{complete}(w)\ \land\ \lnot\,\mathrm{stale}(w),\\[2pt] \textsf{Executing} \xrightarrow{\ \mathsf{A}(w)\ } \textsf{Succeeded} & \text{if } \mathsf{G}=\textsf{Allowed}\ \land\ \mathrm{stale}(w). \end{cases} $$ The executor issues the physical restart only on the transition into \(\textsf{Executing}\) when the request has **no** in-flight rollout (\(\mu=\bot\)); while \(\mu\neq\bot\) it merely watches the rollout to completion. A terminal request is inert under \(\delta\) except for retention: it is deleted after a TTL. ## 10. Coalescing and reopening For a target workload \(w\), the coalescer maintains a single request keyed by \((\mathsf{ns}(w),\ \mathrm{name}(w),\ \kappa(w))\). On an incoming relevant change with time \(\theta\): $$ \mathsf{ensure}(w,\theta):\quad \begin{cases} \text{create request in } \textsf{Pending} & \text{if none exists},\\[2pt] \big(\text{phase} \leftarrow \textsf{Pending},\ \ \mu \leftarrow \bot\big) & \text{if the request is in } \mathsf{Term}\ \ (\textbf{reopen}),\\[2pt] \text{advance } \textsf{latestObservedDependencyChange} \leftarrow \max(\cdot,\theta) & \text{always.} \end{cases} $$ Resetting \(\mu \leftarrow \bot\) on reopen is essential: it is what makes the next \(\delta\) step run the executor's *start-rollout* path (issuing a fresh \(\mathsf{A}(w)\)) instead of adopting the already-completed previous rollout. ## 11. Invariants We collect the properties the operator maintains. Let a *run* be an infinite fair sequence of reconcile steps under a scheduler that eventually delivers every enqueued event. **(I1) No spurious restart at baseline.** If a source \(s\) was last written no later than its consumer's rollout, \(\lambda(s) \le \sigma(w)\), and no relevant change occurs, then \(\Lambda(w) \le \sigma(w)\) and hence \(\lnot\,\mathrm{stale}(w)\); by §9 the request settles in \(\textsf{NoLongerRequired}\) and \(\mathsf{A}(w)\) never fires. *(Ensured by \(\beta(s)=\lambda(s)\), §4.)* Conversely, if \(s\) was written after the rollout started, \(\lambda(s) > \sigma(w)\), the workload is genuinely stale and is restarted exactly once: KICK adopting it late does not make it fresh. **(I2) Eventual restart on relevant change.** If at time \(t^\star\) a relevant change gives \(\Lambda(w) = t^\star > \sigma(w)\), and from some time on \(\mathsf{G}(w,\cdot)=\textsf{Allowed}\) and the workload's rollout is complete, then in every run \(\mathsf{A}(w)\) eventually fires and afterwards \(\sigma(w) \ge t^\star\). *(Ensured by reopen resetting \(\mu\leftarrow\bot\), §10.)* **(I3) Kind-agnostic freshness.** \(\mathrm{stale}(w)\) is well-defined and satisfiable for every \(\kappa(w) \in \{\textsf{Deployment},\textsf{StatefulSet},\textsf{DaemonSet}\}\), because completeness is defined per kind and does not require a ReplicaSet. *(Ensured by the kind-aware \(\mathrm{complete}\), §6.)* **(I4) At most one active request per target.** At every reconcile boundary, \(\big|\{\,r : \mathrm{target}(r)=w \ \land\ \mathrm{phase}(r)\notin\mathsf{Term}\,\}\big| \le 1\). Duplicate and repeated references to the same source therefore cause at most one concurrent restart. *(Ensured by the keyed coalescer, §10.)* **(I5) Gate safety.** \(\mathsf{A}(w)\) fires only from the \(\textsf{Executing}\) transition, which is reachable only when \(\mathsf{G}(w,t)=\textsf{Allowed}\). Equivalently, $$ \mathsf{A}(w)\ \text{fires at } t \ \Longrightarrow\ \mathsf{G}(w,t)=\textsf{Allowed}. $$ **(I6) Idempotence / no rollout amplification.** After \(\mathsf{A}(w)\) at time \(t'\) with \(t' \ge \Lambda(w)\), we have \(\sigma(w)=t' \ge \Lambda(w)\), so \(\lnot\,\mathrm{stale}(w)\) and no further restart is issued until a new relevant change advances \(\Lambda\). A single change thus yields exactly one new rollout. **(I7) Non-injection.** The only workload write is \(\mathsf{A}\), setting the standard \(\textsf{restartedAt}\) annotation. KICK injects no dependency hashes, no environment, and no owner state into managed workloads, and never treats `imagePullSecrets` as dependencies (§2). Secret *values* never appear in status, events, or logs. **(I8) Source-driven evaluation.** A workload is evaluated only when KICK observes a \(\textsf{Baseline}\) or \(\textsf{Relevant}\) event for one of its in-scope sources; there is no workload-driven evaluation. Consequently a workload created after every source in \(D(w)\) has already been observed carries no `KickRequest` at all. This is sound: the Pods of such a workload started from the current content of every source, so \(\Lambda(w) \le \sigma(w)\) and \(\lnot\,\mathrm{stale}(w)\) would hold if the request existed. The absence of a request is the correct no-op, not a missed restart. ## 12. Reading the pipeline as one relation Composing the pieces, the end-to-end effect of a relevant change to a source \(s\) on a managed, in-scope consumer \(w \in \mathsf{cons}(s)\) is $$ \underbrace{\phi\ \text{changes}}_{\text{observation}} \ \Rightarrow\ \underbrace{\Lambda(w) > \sigma(w)}_{\text{freshness}} \ \land\ \underbrace{\mathsf{G}(w,t)=\textsf{Allowed}}_{\text{gate}} \ \Rightarrow\ \underbrace{\mathsf{A}(w)}_{\text{restart}}. $$ Everything else the controller does — coalescing, phase transitions, retention — exists to make this implication hold *exactly once* per change, *only* when permitted, and *uniformly* across workload kinds. ## See also - [Freshness](../../concepts/freshness/) — the same idea in prose. - [GitOps gates](../../concepts/gitops-gates/) — provider and window behaviour. - [KickPolicy reference](../../reference/kickpolicy/) and [KickRequest reference](../../reference/kickrequest/).