Skip to content

Name the NetworkPolicy that denies Radar its Prometheus candidate - #1737

Merged
nadaverell merged 7 commits into
mainfrom
feat/prometheus-netpol-attribution
Sep 13, 2026
Merged

nadaverell merged 7 commits into
mainfrom
feat/prometheus-netpol-attribution

Conversation

@nadaverell

@nadaverell nadaverell commented Sep 13, 2026 •

Copy link
Copy Markdown
Contributor

Why

In-cluster, a Prometheus candidate whose direct probe fails ends as a bare no Prometheus service reachable in cluster (Radar deliberately doesn't fall back to port-forwarding there). That message looks identical whether the pod is down or a NetworkPolicy walls off the monitoring namespace — and the second is the common one on locked-down clusters, where the fix is a one-line ingress rule the user can't see they need.

What

When a probe fails at the network level, discovery evaluates the core NetworkPolicy objects it can see against Radar's own pod and the candidate's published endpoints, and names the policies only when they deny the connection outright:

Prometheus candidate monitoring/prometheus-server:9090 was unreachable, and NetworkPolicy monitoring/default-deny isolates ingress to its pods with no rule admitting radar/radar-7d9f…; add an ingress rule for Radar's namespace (radar)

An egress denial on Radar's own side is reported the same way with an egress remedy. The error unwraps to the existing sentinels, so every errors.Is caller and the Availability classification are unchanged; it surfaces through prom.Status.error (from #1736) into the charts and Settings.

Positive evidence only. pkg/netpol is pure and three-valued (Denied / Allowed / Unknown); only Denied is acted on. It requires that the source is egress-isolated with no admitting rule, or that every backend is ingress-isolated with no rule admitting the source — policyTypes defaulting, additive policies, namespaceSelector on real Namespace labels, podSelector, both, ipBlock with except matched per address family, ports incl. named ports and endPort, and the self-traffic exception. Everything the static model can't decide is Unknown and reported as nothing: hostNetwork pods, a Namespace object the caller couldn't read, egress ipBlock rules (cluster-IP DNAT is CNI-defined), malformed selectors.

The wiring in internal/prometheus adds its own preconditions before trusting the cache: MY_POD_NAME/MY_POD_NAMESPACE from the downward API; informers synced and KindCoversNamespace for Pods/Services/NetworkPolicies in every namespace involved; backends resolved from EndpointSlices (real membership + resolved pod port, TCP only, terminating-but-serving included) with each endpoint tied to its cached Pod by UID and routed address; and the probe's rejection reason recorded per candidate so only a true transport failure — no HTTP response at all — is attributed. An endpoint that answered 500, refused credentials, or stalled mid-body was reached.

pkg/netpol is meant to be the single home for core NetworkPolicy semantics. The policy-type defaulting, port matching and policy-to-pod selection (EffectivePolicyTypes, RuleMatchesPort, Selects) move out of internal/trace/netpol.go and trace now calls them, so the trace and attribution evaluators cannot disagree on which policies apply to a pod or which ports a rule covers. Their verdict layers stay separate on purpose: trace answers for an unspecified client and can only assert what holds for every source, while attribution knows the exact source pod. Each backend is judged on its whole path — the source's egress and the backend's ingress — so a Service is never called reachable because egress admits one backend and ingress a different one.

Limits (documented in docs/in-cluster.md)

  • Core NetworkPolicy only — Cilium/Calico CRD policies are not evaluated.
  • A denial is the policy's declared intent; a CNI that doesn't enforce NetworkPolicy (kindnet) still passes the traffic. The message is worded as what the policy says.
  • At boot the pre-warm may run before the NetworkPolicy informer syncs; attribution then stays silent and the next discovery (the first metrics view) attributes with a warm cache.

Testing

  • pkg/netpol: 35 table-driven cases — every rule shape above, the uncertainty cases (hostNetwork, missing Namespace, ipBlock miss, egress ipBlock, malformed selector), self-traffic, dual-stack address families, unset protocol, and a Service whose egress admits one backend while ingress admits another (no complete path → denied on both directions).

  • internal/prometheus: attribution wiring against a standalone cache + fake clientset — ingress and egress attribution; admitting policy, refused/answered probe, missing pod identity, namespace-scoped policy cache, not-yet-ready deferred informer, unresolvable / stale-UID / mis-addressed endpoints, UDP twin port, serving-terminating endpoint, cross-namespace backend → silence; egress remedy names the resolved backend; probes that hang to the pass deadline count as transport failures.

  • pkg/prom: probe reasons (HTTP error vs transport vs oversized body). internal/trace: its suite is unchanged and green with policy selection routed through pkg/netpol.Selects.

  • Live, in-cluster on kind + Cilium (an enforcing CNI), Radar built from this branch and installed with the chart's default values. A Prometheus in monitoring behind a default-deny-ingress policy: the probe hangs to the 3 s deadline and discovery — the boot pre-warm itself, deferred informers having synced in ~100 ms — reports

    Prometheus candidate monitoring/prometheus-server:9090 was unreachable, and NetworkPolicy monitoring/default-deny-ingress isolates ingress to its pods with no rule admitting radar/radar-…; add an ingress rule for Radar's namespace (radar)

    Verified in the UI through the in-cluster instance: the Pod drawer's Metrics section and Settings → Overview → Metrics both show that message; adding an allow-from-radar rule on 9090 flips the drawer to charts with no click (connected directly at the Service address); changing that rule to port 8080 denies again and names both policies. With local Radar against the same cluster, the Service's Reachability view still shows trace's own would-deny finding naming the policy, and its in-cluster probe fails to get through — the two evaluators agree. The egress direction is unit-tested only: a core egress policy that still lets Radar reach the API server needs an ipBlock rule, which is deliberately Unknown.

Follow-ups (not this PR)

  • internal/server/network_policy.go (the /api/network-policies/evaluate endpoint behind the Traffic flow list) still has its own evaluator, and it diverges from pkg/netpol: it treats a policy without policyTypes as isolating nothing, ignores rule ports, matches namespaceSelector without the Namespace's labels, and never matches ipBlock. It should be rebased on pkg/netpol behind an adapter that keeps its per-policy effect/reason response and adds the port the UI doesn't yet send.
  • Trace's verdict layer (evaluateNetpol / evaluatePodPort) could be expressed on pkg/netpol once it grows a per-backend, per-direction API that accepts an unknown source; today only the primitives are shared.
  • NetworkPolicyRenderer / NetworkPolicyDiagram in k8s-ui have presentation gaps unrelated to this PR: a peer with both selectors shows only podSelector, matchExpressions render as "all", policyTypes defaulting isn't applied, and the rule list omits endPort.
  • The chart in skyhook-dev/deployment is vendored from an older Radar and does not inject MY_POD_NAME / MY_POD_NAMESPACE; this repo's chart does. Re-vendor there for attribution to run on staging.

Note

Medium Risk
Changes Prometheus discovery failure paths and adds API/cache-dependent NetworkPolicy evaluation; mis-attribution is mitigated by conservative silence, but operators may act on policy messages that reflect declared intent rather than live CNI behavior.

Overview
When in-cluster Prometheus discovery finds candidates but none answer a direct Service probe, Radar no longer stops at the generic no Prometheus service reachable in cluster whenever it can prove a core NetworkPolicy blocks the path. Probes now record per-candidate rejection reasons; only true transport failures (no HTTP response, including probes still hanging when the pass ends) trigger static policy evaluation from Radar’s pod to the candidate’s EndpointSlice backends.

A new shared pkg/netpol package holds the evaluator and port/policy-type helpers (trace’s netpol code now imports it). Attributed failures return a detailed message naming the isolating policy and a suggested ingress or egress fix, while still unwrapping to the existing unreachable sentinel so errors.Is and UI classification stay the same. Attribution stays silent when evidence is incomplete (missing MY_POD_NAME/MY_POD_NAMESPACE, partial cache, answered-but-refused probes, etc.).

pkg/prom adds ProbeReasonHTTPError and distinguishes HTTP responses from dial/timeouts so network-path reasoning is not confused with auth or 5xx answers. docs/in-cluster.md documents the new message and evaluation limits (core NP only, declared intent vs CNI enforcement).

Reviewed by Cursor Bugbot for commit d28678d. Bugbot is set up for automated code reviews on this repo. Configure here.

@nadaverell
nadaverell requested a review from hisco as a code owner September 13, 2026 11:02
@qodo-code-review

Copy link
Copy Markdown

PR Summary by Qodo

Attribute Prometheus failures to blocking NetworkPolicies

✨ Enhancement 🧪 Tests 📝 Documentation 🕐 40+ Minutes

Grey Divider

AI Description

• Names blocking ingress or egress NetworkPolicies when Prometheus transport probes fail.
• Adds conservative three-state policy evaluation using authoritative cache data and resolved
 EndpointSlice backends.
• Preserves existing error classification and documents and tests attribution behavior.
Diagram

graph TD
  A["Candidate Probe"] --> B{"Transport failure?"}
  B -- "No" --> C["Generic rejection"]
  B -- "Yes" --> D["Cache validation"] --> E["Backend resolution"] --> F["Policy evaluator"] --> G{"Denied?"}
  G -- "Yes" --> H["Attributed error"]
  G -- "No" --> C
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. CNI-specific diagnostics
  • ➕ Could reflect actual packet enforcement rather than declared policy intent.
  • ➕ Could include Cilium or Calico custom policy resources.
  • ➖ Requires vendor-specific integrations and permissions.
  • ➖ Produces inconsistent behavior across clusters and CNI versions.
2. Service-selector approximation
  • ➕ Avoids EndpointSlice API calls and UID/address validation.
  • ➕ Requires less attribution wiring.
  • ➖ Can evaluate pods that do not receive Service traffic.
  • ➖ Cannot reliably resolve per-endpoint named target ports or manually managed backends.

Recommendation: Keep the PR's conservative, CNI-independent evaluator and EndpointSlice-based backend resolution. Returning only positive Denied evidence minimizes false attribution, while the alternatives either sacrifice portability or evaluate an inaccurate backend set.

Files changed (13) +1545 / -118

Enhancement (5) +826 / -7
client.goExpose probe rejection reasons to discovery +11/-1

Expose probe rejection reasons to discovery

• Adds a reason-returning probe wrapper while preserving the existing boolean API. HTTP response failures now receive a distinct diagnostic message from transport failures.

internal/prometheus/client.go

discovery.goAttribute failed candidate probes before returning the generic error +43/-6

Attribute failed candidate probes before returning the generic error

• Records rejection reasons for concurrent candidate probes and invokes NetworkPolicy attribution when every direct probe fails. Attributed failures are logged and returned without changing cancellation handling or deterministic candidate ordering.

internal/prometheus/discovery.go

netpol_attribution.goResolve candidate backends and report blocking policies +280/-0

Resolve candidate backends and report blocking policies

• Adds conservative attribution using Radar's pod identity, authoritative informer coverage, EndpointSlices, cached pods, and the shared policy evaluator. The resulting error names ingress or egress policies, suggests remediation, and unwraps to the existing unreachable sentinel.

internal/prometheus/netpol_attribution.go

evaluate.goAdd conservative three-state NetworkPolicy evaluator +373/-0

Add conservative three-state NetworkPolicy evaluator

• Introduces pure ingress and egress evaluation across additive policies and multiple backends. It supports namespace and pod selectors, ingress IP blocks, self-traffic, dual-stack addresses, and explicit Unknown outcomes for undecidable inputs.

pkg/netpol/evaluate.go

ports.goCentralize NetworkPolicy type and port matching +119/-0

Centralize NetworkPolicy type and port matching

• Adds reusable helpers for policyTypes defaulting, protocol normalization, numeric ranges, and named container-port resolution. Package documentation clarifies that evaluation models declared core NetworkPolicy intent rather than observed CNI behavior.

pkg/netpol/ports.go

Bug fix (2) +30 / -5
client.goSeparate answered HTTP failures from transport failures +14/-3

Separate answered HTTP failures from transport failures

• Adds an HTTP-error probe reason for non-authentication HTTP statuses and response-processing failures. Only failures that receive no HTTP response remain classified as transport errors.

pkg/prom/client.go

transport.goMark post-response body failures explicitly +16/-2

Mark post-response body failures explicitly

• Wraps body-read and response-size failures in ResponseError carrying the received status code. Callers can therefore distinguish an answered endpoint from a connection-level failure.

pkg/prom/transport.go

Refactor (1) +8 / -102
netpol.goReuse shared NetworkPolicy matching primitives +8/-102

Reuse shared NetworkPolicy matching primitives

• Replaces local policy-type, protocol, named-port, and rule-port implementations with helpers from pkg/netpol. Existing trace-specific verdict aggregation remains unchanged.

internal/trace/netpol.go

Tests (4) +679 / -3
netpol_attribution_test.goTest safe NetworkPolicy attribution boundaries +308/-0

Test safe NetworkPolicy attribution boundaries

• Covers ingress and egress attribution, admitting policies, incomplete cache visibility, non-transport failures, stale endpoints, protocol mismatches, terminating backends, and cross-namespace endpoints.

internal/prometheus/netpol_attribution_test.go

netpol_test.goUpdate trace tests for shared policy helpers +5/-3

Update trace tests for shared policy helpers

• Moves effective policy-type assertions to the exported pkg/netpol implementation used by trace evaluation.

internal/trace/netpol_test.go

evaluate_test.goExercise NetworkPolicy verdict semantics +337/-0

Exercise NetworkPolicy verdict semantics

• Tests allowed, denied, and unknown outcomes across selectors, ports, IP blocks, policy defaulting, additive policies, heterogeneous backends, host networking, malformed inputs, and self-traffic.

pkg/netpol/evaluate_test.go

client_test.goTest precise Prometheus probe failure classification +29/-0

Test precise Prometheus probe failure classification

• Verifies that non-2xx and oversized responses count as answered HTTP failures while connection refusal remains a transport failure.

pkg/prom/client_test.go

Documentation (1) +2 / -1
in-cluster.mdDocument attributed Prometheus NetworkPolicy failures +2/-1

Document attributed Prometheus NetworkPolicy failures

• Explains the new policy-specific discovery error, suggested ingress or egress remediation, and conditions where Radar intentionally remains silent. It also documents CNI enforcement and custom-policy limitations.

docs/in-cluster.md

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes using default effort and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit bb1af2a. Configure here.

Comment thread internal/prometheus/netpol_attribution.go
@qodo-code-review

qodo-code-review Bot commented Sep 13, 2026 •

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (0) 📘 Rule violations (0) 🔗 Cross-repo conflicts (1) 📜 Skill insights (0)

Grey Divider


Remediation recommended

1. Timed-out probes lose policy attribution ✓ Resolved 🐞 Bug ☼ Reliability
Description
probeCandidatesWithReasons snapshots reasons as soon as its context expires, before in-flight
workers necessarily return from probeWithReason and store their transport rejection. When a
NetworkPolicy silently drops the probe until the direct-pass deadline, attribution receives an empty
reason for that candidate and skips it instead of naming the denying policy.
Code

internal/prometheus/discovery.go[R447-449]

		case <-woke:
		case <-ctx.Done():
-			return earliestSuccess()
+			return earliestSuccess(), snapshot()
Relevance

●●● Strong

Recent precedent accepts cancellation and concurrent-discovery race fixes; this preserves transport
reasons after timeout.

PR-#1153

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
Workers store their reason only after the probe returns, whereas the cancellation branch returns a
snapshot immediately. Attribution deliberately accepts only the transport-error value, so an
unrecorded timed-out probe cannot be attributed.

internal/prometheus/discovery.go[381-411]
internal/prometheus/discovery.go[423-450]
internal/prometheus/discovery.go[149-151]
internal/prometheus/netpol_attribution.go[60-65]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
The concurrent probe coordinator returns a reason snapshot immediately when its context expires. Workers record their probe reason only after the probe returns, so a probe that reaches the deadline can be absent from the snapshot and is excluded from NetworkPolicy attribution.

### Fix Focus Areas
- internal/prometheus/discovery.go[381-411]
- internal/prometheus/discovery.go[447-449]
- internal/prometheus/netpol_attribution.go[60-65]

### Recommended Fix
On direct-pass expiry, retain or collect terminal outcomes from launched probes before returning attribution reasons, without waiting indefinitely for uncancellable resolver work. Record context-deadline probe failures as transport failures when the probe had actually been started, while retaining empty reasons only for probes that were never launched.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


2. Egress instructions can permit wrong traffic ✓ Resolved 🐞 Bug ≡ Correctness
Description
describeNetworkPolicyBlock builds the egress remedy from cand.Namespace and cand.Port, while
candidateBackends evaluates each EndpointSlice backend using its resolved target namespace and pod
port. When a Service translates targetPort or a manually managed EndpointSlice targets pods in
another namespace, the message directs operators to permit the Service destination instead of the
backend destination that was evaluated.
Code

internal/prometheus/netpol_attribution.go[R274-275]

+		return fmt.Sprintf("Prometheus candidate %s was unreachable, and %s isolates egress from %s with no rule admitting it; add an egress rule to the %s namespace on port %d",
+			target, noun, selfRef, cand.Namespace, cand.Port)
Relevance

●●● Strong

Deterministic user-facing correctness bug: remedy must use the resolved backend namespace and port
evaluated.

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
Backend resolution explicitly obtains each endpoint’s namespace from its EndpointSlice target
reference, supports pods outside the Service namespace, resolves the EndpointSlice port, and passes
both values to policy evaluation. Egress policy matching therefore uses the actual backend namespace
and pod port, but the generated remedy substitutes the candidate Service namespace and port, proving
that its suggested rule can diverge from the connection evaluated.

internal/prometheus/netpol_attribution.go[192-209]
pkg/netpol/evaluate.go[123-142]
internal/prometheus/netpol_attribution.go[265-279]
internal/prometheus/netpol_attribution.go[74-97]
internal/prometheus/netpol_attribution.go[265-278]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The egress diagnostic recommends the candidate Service namespace and port, but NetworkPolicy evaluation uses each EndpointSlice-resolved backend namespace and pod port. This makes the suggested egress rule incorrect when a Service translates its target port or a manually managed EndpointSlice targets a pod in another namespace, so applying the recommendation can leave the connection blocked.

## Fix Focus Areas
- internal/prometheus/netpol_attribution.go[70-105]
- internal/prometheus/netpol_attribution.go[192-209]
- internal/prometheus/netpol_attribution.go[265-279]
- internal/prometheus/netpol_attribution_test.go[269-293]

## Recommended Fix
Pass the resolved backend destinations, or their distinct namespace-and-port tuples, into `describeNetworkPolicyBlock`. For egress denials, render every actual backend namespace and pod port that must be admitted instead of `cand.Namespace` and `cand.Port`; if the backends have different destination tuples, either list each required tuple or omit the port-specific remedy rather than suggesting the Service port. Add a cross-namespace EndpointSlice test covering an egress denial.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


3. Pod IP rules can blame the wrong policy ✓ Resolved 🐞 Bug ≡ Correctness
Description
peerAdmits evaluates ingress ipBlock entries against the source Pod's cached IP addresses and
turns a non-match into a definitive denial. When source NAT or other rewriting occurs before policy
enforcement, the network policy may admit the observed address while discovery still attributes the
failed probe to that policy.
Code

pkg/netpol/evaluate.go[R245-249]

+	if peer.IPBlock != nil {
+		if dir == DirectionEgress {
+			return triUnknown
+		}
+		return ipBlockAdmits(peer.IPBlock, podIPs(target.Pod))
Relevance

●● Moderate

Valid CNI-dependent attribution concern, but the PR explicitly models declared policy intent rather
than wire-level enforcement.

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The ingress branch passes podIPs(target.Pod) into ipBlockAdmits, which returns triNo when none
of those addresses matches, and the ingress fold converts that result into Denied. Kubernetes
documents that source or destination rewriting may occur before or after NetworkPolicy processing
depending on the network plugin, cloud provider, and Service implementation, so Pod status addresses
do not establish which source address the policy actually evaluates.

pkg/netpol/evaluate.go[241-250]
pkg/netpol/evaluate.go[294-331]
pkg/netpol/evaluate.go[145-176]
pkg/netpol/evaluate.go[245-250]
🌐 Kubernetes states that source or destination rewriting may occur before or after NetworkPolicy processing, with behavior varying by network plugin, cloud provider, and Service implementation.

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Ingress `ipBlock` evaluation assumes NetworkPolicy enforcement sees the source Pod's recorded IP, although Kubernetes leaves the ordering of source-address rewriting and policy processing implementation-dependent. This can manufacture a definitive denial and violate the attribution feature's positive-evidence requirement on CNIs or Service paths that apply source NAT.

## Fix Focus Areas
- pkg/netpol/evaluate.go[241-250]
- pkg/netpol/evaluate.go[294-331]
- pkg/netpol/evaluate_test.go[131-149]

## Recommended Fix
Return an unknown result for ingress `ipBlock` peers when the source address observed at policy enforcement cannot be established independently of CNI rewriting, including pod-to-pod Service traffic, as egress `ipBlock` rules are already treated as unknown. Propagate that unknown result so attribution remains silent rather than naming a policy, and update tests to verify that an ingress rule whose only peer is an `ipBlock` cannot produce a definitive denial from Pod status addresses alone.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


View medium (1)
4. Staging misses policy attribution 🔗 Cross-repo conflict ≡ Correctness
Description
attributeNetworkPolicyBlock returns without evaluating policies when MY_POD_NAME or
MY_POD_NAMESPACE is unset. The deployment repo's staging Radar container does not inject these
downward-API variables, so transport failures there still reach the generic unreachable path instead
of the new attribution.
Code

internal/prometheus/netpol_attribution.go[R43-45]

+	selfName, selfNs := os.Getenv("MY_POD_NAME"), os.Getenv("MY_POD_NAMESPACE")
+	if selfName == "" || selfNs == "" || c.k8sClient == nil {
+		return nil
Relevance

●● Moderate

Concrete staging configuration gap, but cross-repository deployment changes are scope-sensitive and
lack close precedent.

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The PR gates the feature on both pod-identity environment variables, while the deployment
repository's vendored staging chart lists the Radar container environment without either variable.

internal/prometheus/netpol_attribution.go[42-45]
External repo: skyhook-dev/deployment, argocd/addons/radar-staging/radar-staging/skh-nonprod/charts/radar/templates/deployment.yaml [71-88]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
NetworkPolicy attribution requires `MY_POD_NAME` and `MY_POD_NAMESPACE`, but the staging chart consumed by the deployment repository does not inject them into the Radar container.

## Fix Focus Areas
- deploy/helm/radar/templates/deployment.yaml[70-100]
- internal/prometheus/netpol_attribution.go[42-45]

## Recommended Fix
Add `MY_POD_NAME` and `MY_POD_NAMESPACE` to the Radar container environment using downward-API `fieldRef` values for `metadata.name` and `metadata.namespace`. Regenerate or synchronize the vendored staging chart in `skyhook-dev/deployment` so its rendered deployment carries the same variables.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Context sources
✅ Compliance rules (platform): 44 rules
✅ Web pages:
  +6 more
✅ Cross-repo context — repo relationships
  Explored: repo: skyhook-dev/radar-docs (sha: 82731c0c)
  Explored: repo: skyhook-dev/radar-e2e (sha: e4918cc2)
  Explored: repo: skyhook-dev/deployment (sha: af3d2421)
Review mode: 🧠 Deep: This is a large, behavior-changing Kubernetes networking feature spanning multiple independent code paths, with complex policy semantics, cache authority, endpoint resolution, probe classification, and substantial new logic where redundant review could catch subtle defects.

Grey Divider

Tip of the day
💡 Did you know, you can start a comment with 'qodo' or '@qodo' to chat about any finding

More tips ↗ | Customize Qodo ↗ | Qodo docs ↗

Grey Divider

Qodo Logo

Comment thread internal/prometheus/netpol_attribution.go Outdated
Comment thread internal/prometheus/discovery.go
Comment thread pkg/netpol/evaluate.go Outdated
Comment on lines +43 to +45
selfName, selfNs := os.Getenv("MY_POD_NAME"), os.Getenv("MY_POD_NAMESPACE")
if selfName == "" || selfNs == "" || c.k8sClient == nil {
return nil

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remediation recommended

4. Staging misses policy attribution 🔗 Cross-repo conflict ≡ Correctness

attributeNetworkPolicyBlock returns without evaluating policies when MY_POD_NAME or
MY_POD_NAMESPACE is unset. The deployment repo's staging Radar container does not inject these
downward-API variables, so transport failures there still reach the generic unreachable path instead
of the new attribution.
Agent Prompt
## Issue description
NetworkPolicy attribution requires `MY_POD_NAME` and `MY_POD_NAMESPACE`, but the staging chart consumed by the deployment repository does not inject them into the Radar container.

## Fix Focus Areas
- deploy/helm/radar/templates/deployment.yaml[70-100]
- internal/prometheus/netpol_attribution.go[42-45]

## Recommended Fix
Add `MY_POD_NAME` and `MY_POD_NAMESPACE` to the Radar container environment using downward-API `fieldRef` values for `metadata.name` and `metadata.namespace`. Regenerate or synchronize the vendored staging chart in `skyhook-dev/deployment` so its rendered deployment carries the same variables.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

nadaverell added a commit that referenced this pull request Sep 13, 2026
…e headers without a URL (#1736)

## Why

Users saw a **Discover Prometheus** button even though discovery is
automatic. Two gaps:

1. Discovery already ran in the background at boot, but `prom.Status`
had no in-flight state, so any metrics view opened during the 5–10 s run
(4 s direct-probe budget + port-forward off-cluster) rendered the manual
CTA. Clicking it only joined the run already underway.
2. After a context switch nothing started discovery at all —
`Reinitialize` builds a fresh client and stops there.

Separately: `--prometheus-header` values were sent to *every*
auto-discovered candidate during probing. A mislabelled or hostile
Service that looks like Prometheus would receive the operator's bearer
token.

## What

**Discovery status.** `prom.Status` gains `discovering`, owned by the
detached discovery run itself — waiters come and go on their own
deadlines, the flag follows the run (gate wait included). The last run's
outcome, including a timeout, is reported as `error` when idle, so
charts and Settings say *why* there is no connection instead of showing
a bare button. Both are generation-keyed: an older run can never
describe a newer configuration, and a cached-endpoint probe that
finishes after a configuration change neither returns the superseded
endpoint nor tears down what the new generation connected.

**Pre-warm everywhere a cluster comes up.** `Prewarm()` runs at boot and
from the context-switch and namespace-rescope callbacks. A cluster with
no Prometheus no longer records an errorlog warning on every start; the
status carries the reason.

**Headers require a URL.** With headers configured and no URL, discovery
refuses to run — in the Prometheus client and the Caretta traffic source
— the status names the rule, and boot logs a warning (not fatal). `PUT
/api/integrations/prometheus` rejects saving that pair, checked against
what will actually be in effect: the submitted headers, else the running
client's (flags, env and file already folded in), plus env-sourced
headers on disk that re-resolve at the next start. The Helm chart's
`values.schema.json` enforces the same dependency so `helm
install`/`lint` fails on `traffic.prometheusHeaders*` without
`traffic.prometheusUrl`.

**Atomic live configuration.** A URL + header change applies in one step
with the cached connection dropped alongside it (`prometheus.Configure`,
`traffic.SetMetricsConfig`), and only the forward that served the
dropped endpoint is stopped. The traffic subsystem is rebuilt on such a
change — serialized with context operations — because a live source
copies URL and headers at construction; without the rebuild, rotated or
cleared credentials would keep being used until the next context switch.

**Frontend.** Status polls every 2 s while a run is in flight or before
the server's run has reported (a status read before the boot/switch
pre-warm starts is transient — every run ends in `connected` or
`error`); both chart CTA sites render "Discovering Prometheus…" instead
of the button; `useAutoPromConnect` marks discovering optimistically
before its POST and undoes it if the request itself fails, so an
unreachable server can't leave the spinner up. Settings → Overview →
Metrics shows Discovering / Connected / the failure reason.
`@skyhook-io/k8s-ui` `PrometheusChartsView` gains an optional
`discovering` prop (additive).

## Testing

- **Go** (`internal/prometheus`, `internal/server`, `internal/traffic`,
`pkg/prom`): status before / during / after a run; discovering follows
the flight when every waiter cancels; failure outcome surfaces when
idle; timeout and generation rules; headers gate never touches the
cluster (fake client asserts no list); new credentials never reach the
previous endpoint through `Configure`; a stale cached probe cannot tear
down a newer connection; apply-handler 400 matrix (submitted / live /
env-on-disk headers). **Vitest**: poll cadence. `helm lint` with and
without headers/URL.
- **Live, local Radar against an EKS cluster** with
`monitoring/prometheus-operated`: opening a Pod drawer inside the boot
window shows "Discovering Prometheus…" and flips to populated charts ~1
s later with no click; a header context switch to a cluster without
Prometheus shows *Not connected — no Prometheus service found in
cluster* in Settings, and switching back pre-warms and connects (~6 s)
while the open drawer waits on the same run. Started with
`--prometheus-header` and no URL: warning logged, no enumeration or
probe, the drawer and Settings carry the rule, `PUT` with an empty URL
returns 400, and clearing the headers in the same `PUT` discovers and
connects.

## Notes

- Local and in-cluster both pre-warm; in-cluster this is a `list
services` plus in-cluster GETs, no port-forward.
- A context switch performed server-side (API, CAPI connect) reaches an
already-open tab on the regular 60 s status cadence; the header switcher
invalidates queries itself. Unchanged by this PR.
- Stacked follow-up: #1737 names the NetworkPolicy behind an in-cluster
unreachable candidate.

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Medium Risk**
> Changes metrics discovery and how Prometheus auth headers are applied;
misconfigurations that relied on headers without a URL will stop
discovering until fixed, but the change reduces credential leakage to
unintended endpoints.
> 
> **Overview**
> Improves **Prometheus metrics UX** and **credential safety** when
Radar auto-discovers metrics backends.
> 
> **Discovery progress** — `prom.Status` now exposes `discovering` (tied
to the in-flight discovery run, not individual HTTP waiters) and
surfaces the last failure (including timeouts) as `error` when idle.
Boot and **context/namespace switches** call `Prewarm()` so discovery
starts automatically; the UI polls every 2s while unsettled and shows
“Discovering Prometheus…” instead of a manual CTA.
> 
> **Headers require an explicit URL** — Shared rule via
`pkg/prom.HeadersRequireURL` / `ErrHeadersRequireURL`: with auth headers
but no `prometheusUrl`, discovery is skipped (Prometheus client and
Caretta traffic), Settings/`PUT /api/integrations/prometheus` reject
saving that combo, and Helm `values.schema.json` enforces
`traffic.prometheusUrl` when headers are set. Docs call out that
credentials are never sent to auto-discovered endpoints.
> 
> **Safer live config** — URL and headers apply atomically (`Configure`,
`SetMetricsConfig`) with the cached connection dropped in the same step;
live Prometheus integration changes **restart the traffic subsystem** so
rotated headers cannot keep hitting an old endpoint.
> 
> <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit
52216a7. Bugbot is set up for automated
code reviews on this repo. Configure
[here](https://www.cursor.com/dashboard/bugbot).</sup>
<!-- /CURSOR_SUMMARY -->
Base automatically changed from feat/prometheus-discovery-status to main September 13, 2026 12:28
In-cluster, a Prometheus candidate that does not answer its direct probe
used to end as a bare "no Prometheus service reachable in cluster", which
looks identical whether the pod is down or a NetworkPolicy walls off the
monitoring namespace. Discovery now records why each probe was rejected,
and for a network-level failure evaluates the core NetworkPolicy objects it
can see against Radar's own pod and the candidate's published endpoints.

The verdict is positive evidence only. pkg/netpol is pure and three-valued:
Denied requires that the source is egress-isolated with no admitting rule,
or that every ready backend is ingress-isolated with no rule admitting the
source — namespaceSelector on real Namespace labels, podSelector, both,
ipBlock with except, ports incl. named ports and endPort. Anything the
static model cannot decide is Unknown and never reported: hostNetwork pods,
a Namespace the caller could not read, egress ipBlock rules, malformed
selectors. The wiring in internal/prometheus adds its own preconditions
before trusting the cache — synced and covering both namespaces, Radar's
pod identity from the downward API, backends from EndpointSlices with every
endpoint resolved to a cached Pod — and otherwise says nothing.

The policy-type and port-matching primitives move out of internal/trace into
pkg/netpol so the two evaluators share one definition of the semantics.
A probe that got an HTTP error reached its endpoint, so it is no longer a
transport failure. Backends are tied to their cached pod by UID and address,
judged under their own namespace's policies, matched on the TCP port the
probe used, and include terminating-but-serving endpoints that kube-proxy
may still route to. The evaluator matches ipBlock rules against the pod's
addresses of the block's own family and never lets a pod deny itself. A
run cancelled while attribution reads the API ends as cancelled rather than
being recorded as a cluster without Prometheus.
… address

A body-read failure or an oversized response arrives after the endpoint
answered, so the transport now marks those as post-response errors and the
probe reports them as HTTP errors rather than transport failures. Endpoint
validation compares the pod against the first published address — the one
consumers route to — instead of any address in the list.
A deferred kind's lister is nil for a moment after its informer syncs;
attribution now requires the kind to be ready as well as synced, and
nil-checks every lister it uses, so a cache that is still warming leaves
the generic message in place instead of panicking.
…lock match as evidence

A policy that drops packets makes the probe hang until the pass deadline;
those probes now count as transport failures rather than being left
unexplained by the snapshot. An ingress ipBlock miss no longer denies — the
address a policy sees may have been rewritten by the network plugin — while
a match still admits. The egress remedy names the resolved backends'
namespaces and pod ports, which a targetPort or a manually managed
EndpointSlice can make differ from the Service's.
@nadaverell
nadaverell force-pushed the feat/prometheus-netpol-attribution branch from ca2fb81 to d28678d Compare September 13, 2026 12:31
… trace

A Service was reported reachable when egress admitted one backend and
ingress admitted a different one; each backend is now judged on both legs
of its own path, and a denial no single direction accounts for names both
policy sets. Policy-to-pod selection is exported from pkg/netpol and trace
uses it in place of its own copies, so the two evaluators agree on which
policies apply to a pod. RuleMatchesPort normalizes the caller's protocol
the way it already normalized the rule's.
@nadaverell
nadaverell merged commit d2ebeb8 into main Sep 13, 2026
9 checks passed
@nadaverell
nadaverell deleted the feat/prometheus-netpol-attribution branch September 13, 2026 15:23
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.

1 participant