fix(operator): distinguish SecretMissing from KeyMissing in TLS Secret resolution - #59
Conversation
…t resolution
`secret::read_secret_bytes` collapsed two distinct cases into `Ok(None)`:
the referenced Secret not existing at all, and the Secret existing but
lacking the requested key. `endpoint_tls::read_secret_bytes_for_tls`
(shared by InferenceProvider's metrics/health-check TLS resolution and
the reconcile-time verify_tls_accessible check) could therefore only
ever report KeyMissing when a key was present with an empty value,
never when it was absent entirely -- contradicting its own documented
behavior and misleading operators diagnosing a live misconfiguration
("Secret doesn't exist" vs. "Secret exists, wrong/missing key name").
Introduce SecretKeyLookup, a three-way Found/SecretMissing/KeyMissing
result, and have read_secret_bytes return it directly instead of
collapsing to Option<Vec<u8>>. This also removes a near-duplicate of
the same distinction that verify_tls_accessible's private
TlsSecretCheck/read_tls_secret_for_verify had already implemented
independently against its own kube::Api call -- it now delegates to
the same fixed read_secret_bytes.
grid_site.rs's gateway-probe path doesn't need the distinction, so it
uses SecretKeyLookup::into_bytes() to keep its existing
Option<Vec<u8>>-based control flow unchanged.
Adds direct unit tests for read_secret_bytes (secret.rs) and for
resolve_tls_config/verify_tls_accessible (endpoint_tls.rs) against a
mocked kube::Client, covering: key found, secret absent, secret with no
data section, key absent from an existing secret's data (the bug), and
key present but empty.
Fixes praxis-proxy#58
Signed-off-by: Jordi Gil <jgil@redhat.com>
…issing Adds the integration and E2E tiers that were missing for the SecretMissing-vs-KeyMissing status.reason distinction fixed in this PR: - Integration: resolve_phase_and_sites (the function reconcile() calls) is now exercised end-to-end against a mocked kube::Client, proving the distinction survives through the actual (phase, status.reason) pair written to the CR, not just the lower-level resolve_tls_config/ read_secret_bytes helpers. - E2E: a new InferenceProvider fixture with a CA Secret that exists but is missing the expected key runs against a live kind cluster and polls status.reason for "HealthCheckTlsKeyMissing" (verified locally: PASS). This gap pre-existed for all four TlsFailureReason variants, not just this fix; the E2E addition here only covers the specific regression this PR guards against, since it needs no live probe endpoint (TLS resolution fails before any health probe is attempted). Signed-off-by: Jordi Gil <jgil@redhat.com>
Follow-up to the previous commit: the cleanup_includes_all_owned_providers documentation test enforces that every InferenceProvider fixture name is registered for idempotent cleanup between runs. Missed adding the new TEST_PROVIDER_TLS_KEY_MISSING constant when it was introduced. Signed-off-by: Jordi Gil <jgil@redhat.com>
|
Pushed a follow-up to close the pyramid-invariant gap flagged in review discussion: unit tests alone weren't enough to claim behavioral assurance for the Added:
Full test suite (981 tests), clippy, fmt, and machete all clean. Side finding (not in scope here): while validating the E2E fixture I hit a pre-existing, deterministic scoring-order failure in |
praxis-bot
left a comment
There was a problem hiding this comment.
PR Review
Summary: Fixes grid#58 by introducing SecretKeyLookup to distinguish Secret-not-found from key-not-found in TLS Secret resolution, consolidating the duplicated TlsSecretCheck into the shared read_secret_bytes path, and adding thorough regression tests at unit, integration, and e2e tiers.
Overall assessment: Well-executed bug fix. The three-way enum is the right abstraction, the into_bytes() escape hatch keeps the grid_site path clean, and the deduplication of TlsSecretCheck vs SecretKeyLookup is a meaningful consolidation. Test coverage is excellent -- the bug case is exercised at every layer (direct unit, mocked kube client, mocked reconcile, and live e2e). Two medium-severity convention items below.
| Severity | Count |
|---|---|
| Critical | 0 |
| Large | 0 |
| Medium | 2 |
`[`secret::read_secret_bytes`]` referenced an out-of-scope `secret` path at the doc-comment's location, failing the rustdoc CI check (-D rustdoc::broken-intra-doc-links). Use the same fully-qualified link style already used one line below for SecretKeyLookup. Signed-off-by: Jordi Gil <jgil@redhat.com>
…ents Addresses grid#59 review feedback (praxis-bot): - mock_kube_client_with_secrets was duplicated verbatim (~40 lines) between secret.rs::tests and endpoint_tls.rs::tests, with secret_with_key also duplicated in spirit. Extracted both into a new resources::test_doubles module (cfg(test)-gated at the mod declaration) so the two copies cannot drift independently. - Removed doc comments on read_secret_bytes_key_absent_from_existing_secret_returns_key_missing (secret.rs) and resolve_tls_config_ca_key_absent_from_existing_secret_yields_key_missing (endpoint_tls.rs) per the project convention (CLAUDE.md, Test Organization: function name is the documentation) -- both were redundant with the grid#58 regression context already carried by each test's assertion message. Signed-off-by: Jordi Gil <jgil@redhat.com>
…eads/main (#62) * fix(ci): resolve triggered-integration-test base SHA live from refs/heads/main The "Verify PR merge checkout" step in triggered-integration-test.yaml compared the PR merge ref's base parent against `base_sha`, which was read from `gh api pulls/<n>` -> `.base.sha`. That REST field is only refreshed by GitHub on PR synchronize events (e.g. a push to the head branch) and can lag main's actual tip by hours once a PR sits idle while main advances. Meanwhile `refs/pull/<n>/merge` is kept continuously current against main, so the two values can disagree even with no race at all, failing the check with "Checked-out merge does not combine the expected base and PR head" (observed on run 31811278059 testing PR #59: cached base_sha pointed at a main commit that was already superseded by 282a337 seven hours earlier). Resolve `base_sha` live via `git ls-remote refs/heads/main` in the same "Resolve source" step that resolves the merge ref, instead of trusting the cached PR resource field, so both values are re-derived consistently with each other. Signed-off-by: Jordi Gil <jgil@redhat.com> * fix(ci): resolve merge and base SHAs from one ls-remote call Addresses grid#62 review feedback (praxis-bot): the merge-ref and refs/heads/main resolutions were two sequential `git ls-remote` calls, leaving a window for main to advance between them -- the same class of staleness bug this PR fixes, just with a much narrower race window. Combine both into a single `git ls-remote` invocation so both SHAs are resolved from the same server-side view, making them atomically consistent with each other instead of merely "re-derived the same way." Signed-off-by: Jordi Gil <jgil@redhat.com> --------- Signed-off-by: Jordi Gil <jgil@redhat.com>
What
Fixes #58.
secret::read_secret_bytescollapsed two distinct cases intoOk(None): the referenced Secret not existing at all, and the Secret existing but lacking the requested key.endpoint_tls::read_secret_bytes_for_tls(shared byInferenceProvider's metrics/health-check TLS resolution, andverify_tls_accessible's reconcile-time check) could therefore only ever reportKeyMissingwhen a key was present with an empty value — never when it was absent entirely — contradictingTlsFailureReason::KeyMissing's own doc comment and misleading operators diagnosing a live misconfiguration ("Secret doesn't exist" vs. "Secret exists, wrong/missing key name").How
SecretKeyLookup(Found(Vec<u8>)/SecretMissing/KeyMissing) and changedread_secret_bytesto return it directly instead of collapsing toOption<Vec<u8>>.read_secret_bytes_for_tlsnow matches on the three-way result instead of guessing.verify_tls_accessible's privateTlsSecretCheck/read_tls_secret_for_verifyhad already implemented independently against its ownkube::Apicall — it now delegates to the same (fixed)read_secret_bytes, so there's a single source of truth instead of two copies that could drift.grid_site.rs's gateway-probe path doesn't need the distinction (it maps everything toTrustMaterialMissingregardless), so it usesSecretKeyLookup::into_bytes()to keep its existingOption<Vec<u8>>-based control flow unchanged.Testing
Added unit tests directly against a mocked
kube::Client(tower::service_fn, matching the existing pattern frommcp_probe.rs):secret.rs:read_secret_bytes— key found, secret absent, secret with nodatasection, key absent from an existing secret's data (the bug), key present but empty.endpoint_tls.rs:resolve_tls_configandverify_tls_accessible— secret absent →SecretMissing, key absent from an existing secret →KeyMissing(the regression this PR fixes), no TLS configured →None.cargo test -p operator --lib: 979/979 passing.make lint(workspace clippy-D warnings, nightlycargo fmt --check,cargo machete): clean.Scope note
This is independent of the still-open #56 (
AgentToolProvider) — #56 added a test inmcp_probe.rs(read_tls_material_key_absent_from_data_currently_misreported_as_secret_missing) that intentionally locked in this bug pending this fix, referencing this issue. I'll update that test to assert the corrected behavior when #56 rebases ontomainpost-merge, per the plan already noted in that test's doc comment.