Skip to content

fix: list-only kinds (OLM packagemanifests) poll instead of dying in the watch loop (#141) - #146

Merged
hellices merged 3 commits into
mainfrom
fix/packagemanifests-list-poll
Aug 1, 2026
Merged

fix: list-only kinds (OLM packagemanifests) poll instead of dying in the watch loop (#141)#146
hellices merged 3 commits into
mainfrom
fix/packagemanifests-list-poll

Conversation

@hellices

@hellices hellices commented Aug 1, 2026

Copy link
Copy Markdown
Owner

Fixes #141

Problem

:view packagemanifests on an OLM cluster was unusable: PackageManifest is served by OLM's aggregated packageserver, which supports only get/list — no watch. korvid's LIST→WATCH loop cleared and re-seeded the store on every retry (table flicker for ~5s), then killed the view with an error toast and removed it from WatchManager.active forever.

Fix

Follow the metrics-poller precedent, generically:

  1. Discovery captures verbs. _parse_resource_list keeps list-only kinds instead of dropping them (they were previously invisible on servers that declare verbs honestly), marking ResourceMeta.watchable=False.
  2. watch_objects degrades to LIST + poll. For unwatchable kinds — and for servers that advertise watch but reject it with 405 at runtime — the stream falls back to periodic re-LIST diffing (LIST_POLL_INTERVAL = 30s): upserts for present rows, DELETED for vanished ones. The stream stays alive and incremental — no store clear, no flicker, view stays in active so rendering, hierarchy lookups, and the operator install flow work unchanged. Other watch errors still propagate to the manager's retry/report loop.
  3. WatchManager treats 405 as deterministic (like 403 — no retry burn), but unlike a 403 the rows the successful LIST delivered stay visible; only authorization denials purge the bucket.

Tests (TDD — RED first)

  • test_discover_resources_keeps_list_only_kinds_as_unwatchable — list-only kinds discovered with watchable=False; kinds without even list stay excluded.
  • test_unwatchable_kind_polls_lists_and_diffs_instead_of_watching — poll rounds upsert/delete-diff; the Watch API is never touched.
  • test_watch_405_falls_back_to_list_polling — watch-advertising-but-405 servers degrade to polling.
  • test_watch_non_405_api_exception_still_raises — other errors keep the manager in charge.
  • test_405_reports_once_without_retries_and_keeps_listed_rows — manager: one attempt, one report, LISTed rows survive.

Full gate (make check) green: 2825 passed.

…the watch loop (#141)

PackageManifest is served by OLM's packageserver, which supports only
get/list: the LIST->WATCH loop cleared and re-seeded the store five
times (table flicker), then killed the view with an error toast.

- discovery keeps list-only kinds instead of dropping them, marked
  ResourceMeta.watchable=False (verbs are finally captured).
- watch_objects degrades those kinds - and any server that advertises
  watch but rejects it with 405 - to periodic re-LIST diffing
  (LIST_POLL_INTERVAL=30s): upserts for present rows, DELETED for
  vanished ones, so the stream stays alive and incremental and the
  view keeps rendering. Other watch errors still propagate to the
  manager retry loop.
- WatchManager treats 405 like 403 as deterministic (no retry burn),
  but keeps the LISTed rows - only authorization denials purge.

Fixes #141

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings August 1, 2026 10:35

Copilot AI 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.

Pull request overview

Adds polling support for Kubernetes resources that support LIST but not WATCH, notably OLM PackageManifests.

Changes:

  • Records resource watch capability during discovery.
  • Adds periodic LIST diffing and intended 405 fallback.
  • Makes WatchManager treat 405 responses as deterministic.

Reviewed changes

Copilot reviewed 6 out of 6 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
src/korvid/k8s/discovery.py Adds ResourceMeta.watchable.
src/korvid/k8s/client.py Implements polling and watch fallback.
src/korvid/core/watch.py Stops retrying deterministic 405 errors.
tests/k8s/test_discovery.py Tests list-only discovery.
tests/k8s/test_client.py Tests polling and fallback behavior.
tests/core/test_watch.py Tests manager handling of 405 errors.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/korvid/k8s/client.py Outdated

@my-reviewer-agent my-reviewer-agent 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.

총평 (issue #141 — list-only kinds 폴링 폴백): 깔끔한 구현입니다. discovery가 watch verb 없는 kind를 watchable=False로 보존하고, watch_objects가 LIST+주기적 재-LIST diff 스트림으로 우아하게 강등되며(스토어 clear 없음, 뷰는 active 유지), 런타임 405도 결정적 오류로 처리해 재시도 낭비를 막습니다. _watch_loop에서 405가 reconnect purge 이전에 반환되므로 'LIST된 행 유지' 주장도 head 코드로 확인했습니다. _list_path 헬퍼 추출로 중복 제거된 것도 좋고, 5개 테스트(RED-first)가 discovery/폴링 diff/405 폴백/비-405 전파/manager 단일 보고를 모두 커버합니다.

Suggestion (경계 사례): watch 스트림이 이벤트를 얼마간 소비한 뒤 중간에 405로 전환되는 경우, watch 이벤트로 ADDED된 행은 known에 반영되지 않습니다. 이후 폴링 라운드에서 그 행이 사라지면 known에도 current에도 없어 DELETED가 영원히 발생하지 않아 스토어에 잔류할 수 있습니다. 실제로 405는 스트림 시작 시점에 결정적으로 발생하므로 현실적 위험은 낮지만, 405 폴백 시 known 대신 스토어 스냅샷을 시드하거나 watch 이벤트를 known에도 upsert하면 닫을 수 있는 갭입니다.

APPROVE

Review round 1 on #146: the raw-watch callable surfaces HTTP errors as
ApiStatusError via _raise_for_status - not ApiException - so a real
packageserver 405 bypassed the fallback and killed the view through the
manager anyway. The watch phase now catches both exception types (both
carry .status/.reason); non-405 statuses re-raise as ApiStatusError as
before.

RED tests: test_watch_405_falls_back_to_list_polling now models the
adapter's ApiStatusError; ApiException(405) and the non-405
ApiStatusError passthrough are pinned separately.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Copilot AI 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.

Pull request overview

Copilot reviewed 6 out of 6 changed files in this pull request and generated no new comments.

Suppressed comments (1)

src/korvid/k8s/client.py:490

  • For a non-405 raw-watch failure, exc is already an ApiStatusError whose body was populated by _raise_for_status (client.py:1491-1504). Reconstructing it here drops that response body, so this does not actually propagate the error unchanged as the new test documents. Re-raise an existing ApiStatusError and only translate the Kubernetes client's ApiException.
            status = int(getattr(exc, "status", 0) or 0)
            if status != 405:
                raise ApiStatusError(status, str(getattr(exc, "reason", "") or "")) from exc

@my-reviewer-agent my-reviewer-agent 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.

재리뷰 (신규 커밋 1개, 라운드 1 지적 반영 확인) — APPROVE.

raw watch 어댑터가 _raise_for_status를 통해 ApiStatusError를 던지므로 실제 packageserver 405가 기존 ApiException-only 핸들러를 우회해 뷰를 죽이던 문제를 정확히 수정했습니다. 405 폴백이 이제 두 예외 타입을 모두 잡고, non-405는 종전과 같이 ApiStatusError로 전파됩니다. RED 테스트가 어댑터의 실제 예외 타입(ApiStatusError(405))을 모델링하도록 교체되었고, ApiException(405) 폴백과 non-405 ApiStatusError passthrough가 각각 별도 테스트로 고정된 점 좋습니다.

인라인에 사소한 Suggestion 1건(재래핑 시 .body 유실) 남겼습니다.

Comment thread src/korvid/k8s/client.py Outdated
Round 2 advisory on #146 (both reviewers): re-wrapping a non-405
ApiStatusError dropped .body, which same-status disambiguation (PDB
denial vs APF throttling, issue #109) depends on. The re-raise now
carries body through (ApiException exposes one too); pinned by the
extended non-405 passthrough test.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Copilot AI 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.

Pull request overview

Copilot reviewed 6 out of 6 changed files in this pull request and generated no new comments.

@my-reviewer-agent my-reviewer-agent 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.

재리뷰 (신규 커밋 1건, e69a9f8 이후): 지난 라운드에서 지적한 non-405 재래핑 시 .body 유실 문제가 정확히 수정되었습니다.

  • watch_objects의 non-405 경로가 이제 ApiStatusError(status, reason, body)로 body까지 보존하며, ApiException 쪽도 getattr(exc, "body", ...)로 안전하게 커버합니다. 동일 status 구분(PDB 거부 vs APF 스로틀링, #109)이 의존하는 정보가 유지됩니다.
  • 테스트가 excinfo.value.body == '{"kind":"Status"}'를 명시적으로 단언하도록 강화되어 회귀를 고정합니다.

추가 지적사항 없습니다. APPROVE

@hellices
hellices merged commit 9fba54c into main Aug 1, 2026
15 of 17 checks passed
hellices added a commit that referenced this pull request Aug 1, 2026
on_resources_updated can be dispatched during app teardown, after the
screen stack is emptied: App.screen then raises ScreenStackError, which
run_test() re-raises and fails whichever test happened to be running -
the intermittent test_ctx_switch CI failures (also seen on PRs #140 and
#146; the CI traceback names this exact frame). No screen simply means
no tree to refresh.

Fixes #147

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
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.

packagemanifests view fails: OLM packageserver has no watch verb — LIST→WATCH loop errors out (needs list+poll fallback)

2 participants