Skip to content

CP-43292: make the webhook server optional - #847

Merged
evan-cz merged 2 commits into
developfrom
CP-43292/webhook-server-optional
Jun 17, 2026
Merged

evan-cz merged 2 commits into
developfrom
CP-43292/webhook-server-optional

Conversation

@evan-cz

@evan-cz evan-cz commented Jun 17, 2026

Copy link
Copy Markdown
Contributor

A customer cannot run the CloudZero Agent in production with the admission webhook server enabled, and needs the informer/KubeState-based path without it. The only existing toggle, insightsController.enabled, did not cleanly disable the webhook server: the config-loader Job stuck in ContainerCreating because it mounted a webhook ConfigMap that was never created when disabled, and many webhook resources rendered even when disabled, leaving dangling objects and a perpetually-firing critical "webhook down" alert.

Implementation Approach:

Introduce a single ternary toggle, components.webhookServer.enabled (true | false | "auto", default "auto" which currently maps to true), resolved by one helper, cloudzero-agent.webhookServer.enabled. Every webhook resource, the config-loader, and the validator consult that helper, so enablement is decided in exactly one place and disabling removes the entire webhook surface with no dangling objects. The legacy insightsController.enabled (default changed from true to null) takes precedence when explicitly set, preserving backward compatibility, and is marked deprecated in the schema. "auto" maps to enabled today behind a commented seam to later follow the KubeState configuration. Default behavior is unchanged.

Functional Requirements:

  1. Operators must be able to disable the webhook server with a single value, with the entire webhook surface removed.

    Added the resolver helper in helm/templates/_webhook_helpers.tpl and gated every webhook-component resource on it: the webhook Deployment, Service, ConfigMap, HPA, and PDB; the ValidatingWebhookConfiguration; the init-cert Job, ClusterRole, ClusterRoleBinding, and ServiceAccount; the TLS certificate, issuer, and secret; the backfill job; and the agent and alloy webhook scrape-config blocks (agent-cm.yaml, _alloy_helpers.tpl).

  2. Disabling must not break the agent (the original ContainerCreating/CrashLoop bug).

    In config-loader-job.yaml the webhook config volume and volumeMount are gated on the resolver, and the --config-webhook argument is passed as an empty string when disabled (NewSettings("") is benign), so the loader no longer mounts an absent ConfigMap. No Go change is required.

  3. The validator must not probe a webhook that is not running.

    validator-cm.yaml forces the post-start webhook_server_reachable check to "disabled" when the resolver is off, using a deepCopy of the checks map so .Values is never mutated in place.

  4. A disabled but monitored install must not page operators about the absent webhook.

    The four webhook alerts in prometheusrule.yaml (CloudZeroWebhookDown, NoEvents, ServerHighLatency, RemoteWriteFailures) are individually gated on the resolver, leaving the non-webhook alerts in the shared rule group intact.

  5. The new field must be schema-valid and the legacy field marked deprecated.

    Added components.webhookServer.enabled (null | boolean | "auto") and changed insightsController.enabled to (null | boolean) with deprecated: true in the source helm/values.schema.yaml, then regenerated helm/values.schema.json. Regenerated the helmless embedded defaults (app/functions/helmless/default-values.yaml) and the helm template snapshots.

Validation:

  • helm-unittest: a resolver truth-table (components.webhookServer.enabled crossed with the legacy insightsController.enabled) plus per-resource presence/absence assertions across the full webhook surface; the full suite passes (72 suites, 626 tests).
  • Schema: added pass/fail fixtures for components.webhookServer.enabled (true/false/auto/null pass, invalid-type fail); make helm-test-schema passes.
  • KUTTL e2e (new suites, run against a local kind cluster with a freshly built dev image): the disabled fresh-install suite passes — webhook Deployment, Service, and ValidatingWebhookConfiguration absent, config-loader Job reaches Complete, and the agent pod is Ready with zero container restarts. The enabled-to-disabled upgrade suite passes — the cluster-scoped ValidatingWebhookConfiguration and init-cert ClusterRole/ClusterRoleBinding are confirmed present after the enabled install and pruned after the upgrade.
  • Lint, format, and generate checks pass in CI; default-on behavior verified unchanged via a render diff.

@evan-cz
evan-cz requested a review from a team as a code owner June 17, 2026 14:26
@evan-cz
evan-cz force-pushed the CP-43292/webhook-server-optional branch from f7c9ee7 to 6cf4945 Compare June 17, 2026 14:29
@evan-cz
evan-cz marked this pull request as draft June 17, 2026 16:06
@evan-cz
evan-cz force-pushed the CP-43292/webhook-server-optional branch 2 times, most recently from bc85a93 to 1840acc Compare June 17, 2026 17:33
@evan-cz
evan-cz marked this pull request as ready for review June 17, 2026 17:38
@evan-cz
evan-cz enabled auto-merge June 17, 2026 17:38
@evan-cz
evan-cz force-pushed the CP-43292/webhook-server-optional branch 2 times, most recently from 0f2530a to b4b1010 Compare June 17, 2026 18:38
@evan-cz
evan-cz changed the base branch from develop to CP-43356 June 17, 2026 19:16
auto-merge was automatically disabled June 17, 2026 19:16

Head branch was modified

@evan-cz
evan-cz force-pushed the CP-43292/webhook-server-optional branch from b4b1010 to 465cb0b Compare June 17, 2026 19:16
…nds:

Command-based KUTTL steps across the pre-existing tests/kuttl suites silently
no-op for two independent reasons, so their e2e assertions have never actually
run (the suites report PASS without executing kubectl/helm/curl):

1. `command:` + `args:` — kuttl's Command has no `args` field and `command` is a
   single string, so only the bare first word ran.
2. `spec:` wrapper — kuttl's TestStep has a top-level `commands` field and no
   `spec`, so nesting `commands:` under `spec:` made the whole step a no-op.
   (Confirmed empirically with a marker probe against the pinned kuttl v0.25.0.)

This converts the 11 affected step files in alloy-clustered-test, collector-test,
collector-comprehensive-test, webhook-test, and webhook-comprehensive-test to
valid kuttl, preserving each step's intent:

- Drop `spec:`; put `commands:` at the top level. Use `script:` / single-string
  `command:` instead of `command:`+`args:`.
- Reference shared agent components by label (app.kubernetes.io/name=
  aggregator|webhook-server|ksm) and discover the install namespace, instead of
  the stale hardcoded `cz-agent-*` names that don't exist in the current chart.
  Exclude the alloy co-install (instance!=cz-alloy-ct).
- Background `kubectl port-forward` (foreground would hang the step) and land the
  curl body to a file before `head` so a failed fetch fails the step.
- Walk up to the repo root before the alloy helm calls (kuttl runs scripts with
  CWD = the step dir); pre-poll selector-based `kubectl wait`s.
- Set explicit per-command `timeout:` on long-running commands (sleeps, helm
  --wait, and every `kubectl wait`): these suites' kuttl-test.yaml put `timeout`
  under `spec:`, which kuttl ignores, so the 30s per-command default otherwise
  applied and clamped them.
- Consolidate webhook-comprehensive/03 into one `script:` so $WEBHOOK_METRICS
  survives across the curl/grep/count chain (kuttl runs each entry in its own
  shell).

Once the suites actually run, they need a working agent image, which exposed a
pre-existing gap: the test-matrix-k8s CI path installs an unpullable image
(kind-overrides pins an intentionally-invalid tag and the freshly-built untested
image was never wired in), so agent pods never reached Ready. It was false-green
only because the suites were hollow. Fix CI to make the built image usable:

- Add a `kind-load` Makefile target.
- In both test-matrix-k8s jobs, log in to ghcr, pull the freshly-built untested
  image for the matrix arch, retag it as the tag the chart installs, kind-load it
  into the node, and run the kind phases explicitly (up -> load -> install/wait
  -> helm-test-kuttl -> teardown) so the image is present before install.

All five suites verified to genuinely execute and PASS against a local kind
cluster using this exact sequence, with a negative control confirming the
now-live assertions go red on a real regression. The CP-43292 webhookserver-*
suites are handled on that branch.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
A customer cannot run the CloudZero Agent in production with the admission
webhook server enabled, and needs the informer/KubeState-based path without it.
The only existing toggle, insightsController.enabled, did not cleanly disable the
webhook server: the config-loader Job stuck in ContainerCreating because it
mounted a webhook ConfigMap that was never created when disabled, and many
webhook resources rendered even when disabled, leaving dangling objects and a
perpetually-firing critical "webhook down" alert.

Implementation Approach:

Introduce a single ternary toggle, components.webhookServer.enabled
(true | false | "auto", default "auto" which currently maps to true), resolved by
one helper, cloudzero-agent.webhookServer.enabled. Every webhook resource, the
config-loader, and the validator consult that helper, so enablement is decided in
exactly one place and disabling removes the entire webhook surface with no
dangling objects. The legacy insightsController.enabled (default changed from true
to null) takes precedence when explicitly set, preserving backward compatibility,
and is marked deprecated in the schema. "auto" maps to enabled today behind a
commented seam to later follow the KubeState configuration. Default behavior is
unchanged.

Functional Requirements:

1. Operators must be able to disable the webhook server with a single value, with
   the entire webhook surface removed.

   Added the resolver helper in helm/templates/_webhook_helpers.tpl and gated every
   webhook-component resource on it: the webhook Deployment, Service, ConfigMap,
   HPA, and PDB; the ValidatingWebhookConfiguration; the init-cert Job, ClusterRole,
   ClusterRoleBinding, and ServiceAccount; the TLS certificate, issuer, and secret;
   the backfill job; and the agent and alloy webhook scrape-config blocks
   (agent-cm.yaml, _alloy_helpers.tpl).

2. Disabling must not break the agent (the original ContainerCreating/CrashLoop bug).

   In config-loader-job.yaml the webhook config volume and volumeMount are gated on
   the resolver, and the --config-webhook argument is passed as an empty string when
   disabled (NewSettings("") is benign), so the loader no longer mounts an absent
   ConfigMap. No Go change is required.

3. The validator must not probe a webhook that is not running.

   validator-cm.yaml forces the post-start webhook_server_reachable check to
   "disabled" when the resolver is off, using a deepCopy of the checks map so
   .Values is never mutated in place.

4. A disabled but monitored install must not page operators about the absent webhook.

   The four webhook alerts in prometheusrule.yaml (CloudZeroWebhookDown, NoEvents,
   ServerHighLatency, RemoteWriteFailures) are individually gated on the resolver,
   leaving the non-webhook alerts in the shared rule group intact.

5. The new field must be schema-valid and the legacy field marked deprecated.

   Added components.webhookServer.enabled (null | boolean | "auto") and changed
   insightsController.enabled to (null | boolean) with deprecated: true in the source
   helm/values.schema.yaml, then regenerated helm/values.schema.json. Regenerated the
   helmless embedded defaults (app/functions/helmless/default-values.yaml) and the
   helm template snapshots.

Validation:

- helm-unittest: a resolver truth-table (components.webhookServer.enabled crossed
  with the legacy insightsController.enabled) plus per-resource presence/absence
  assertions across the full webhook surface; the full suite passes (72 suites,
  626 tests).
- Schema: added pass/fail fixtures for components.webhookServer.enabled
  (true/false/auto/null pass, invalid-type fail); make helm-test-schema passes.
- KUTTL e2e (new suites): these run under `make kind-test`, which installs the
  full default release first and then runs every suite against the same Kind node.
  Each new suite installs its own differently-configured release alongside that
  default one, which surfaced two CI-only failures, both addressed by
  tests/kuttl/webhookserver-min-footprint.yaml without changing the behavior under
  test. (a) Two full-size stacks exceeded the Kind node's allocatable CPU, so the
  new pods stayed Pending; the overlay shrinks each test release to one replica
  with tiny CPU/memory requests. (b) The validator's api_key_valid pre-start check
  retries HTTP calls against the deliberately-invalid test key, adding minutes of
  latency to the server's init container; the overlay disables that single check
  (orthogonal to webhook gating) so init completes promptly. The per-step kuttl
  timeouts are 600s and helm --wait is 8m to absorb a busy CI node. Both suites
  were run to completion co-resident with the full default release: the disabled
  fresh-install suite passes -- webhook Deployment, Service, and
  ValidatingWebhookConfiguration absent, config-loader Job reaches Complete, agent
  pod Ready with zero container restarts; the enabled-to-disabled upgrade suite
  passes -- the ValidatingWebhookConfiguration and init-cert
  ClusterRole/ClusterRoleBinding are present after the enabled install and pruned
  after the upgrade.
- Lint, format, and generate checks pass in CI; default-on behavior verified
  unchanged via a render diff.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@evan-cz
evan-cz force-pushed the CP-43292/webhook-server-optional branch from 465cb0b to f6b69d2 Compare June 17, 2026 19:35
Base automatically changed from CP-43356 to develop June 17, 2026 21:57
@evan-cz
evan-cz added this pull request to the merge queue Jun 17, 2026
Merged via the queue into develop with commit 6c060f1 Jun 17, 2026
41 checks passed
@evan-cz
evan-cz deleted the CP-43292/webhook-server-optional branch June 17, 2026 22:25
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants