Trial: rebuild Kuber.jl on OpenAPI.jl 1.0 - #69
Draft
tanmaykm wants to merge 11 commits into
Draft
Conversation
Start of the openapi-v1-trial line (see OpenAPIv1TrialBranchPlan.md §1): - deps reduced to Dates/HTTP/JSON/OpenAPI plus the Base64/UUIDs stdlibs the new generated modules import; Downloads, TimeZones and (unused) Random gone - HTTP is now a direct dep: it activates OpenAPI's HTTP extension and the discovery probes use it directly - [sources] pins quinnj/OpenAPI.jl @ 1ff9ba8 (package version 1.0.0); to be replaced by a tag + full regeneration when PR 103 merges - floors: Julia 1.11, HTTP 2, JSON 1.7 Also checks in the evaluation notes, the trial plan, the runnable prototypes every decision was verified against, and the watch-latency probe, so the branch is self-describing. src/ still references OpenAPI.Clients/Downloads, so `using Kuber` fails cleanly here by design; Phase 1 replaces the generated layer.
Replaces src/ApiImpl/api (openapi-generator, Swagger 2.0, k8s 1.23-era) plus api_typemap.jl/api_versions.jl with 17 OpenAPI.jl 1.0 client modules generated from upstream kubernetes/kubernetes v1.35.4 OpenAPI v3 group documents, and a registry of lookup tables over them. Pipeline (gen/openapi_v1/, all inputs and outputs checked in so fetch -> patch -> generate is reproducible and auditable): - fetch_specs.sh pulls pristine group documents from a release tag and records provenance in SPECS_ORIGIN - patch_k8s_spec.jq is the middle path: nullable meta.v1.Time, meta.v1.MicroTime and every array property (Go nil slices marshal as null), and NO /watch/ path rewriting — watching goes through the non-deprecated list ops - generate.jl generates each patched document in strict mode (~30s) - emit_registry.jl emits registry.jl The registry replaces the string-munging + `eval` lookups it used to take to find an operation: GROUP_MODULES apiVersion -> module (was APIVersionMap) MODULE_GVS module -> apiVersion KIND_TYPES (apiVersion, kind) -> type (was Typedefs + kuber_type sniffing) OPS (module, verb, kind, scope) -> operation function OP_PARAMS same key -> positional argument names Two departures from the plan sketch, both forced by the specs: - OPS is keyed by module as well as (verb, kind, scope). A 3-tuple cannot express two shipped versions of one kind, and the trial ships two: autoscaling/v1 and autoscaling/v2 both define HorizontalPodAutoscaler. - OP_PARAMS exists because generated positional order is path order — the namespace comes FIRST (`readcorev1namespacedpod(namespace, name)`) and a required body comes last, the reverse of the old client. Two things the emitter learned from the specs rather than assuming: - Subresource kinds come from the parent resource path, not the operation's own x-kubernetes-group-version-kind (which names the subresource type: pods/exec is PodExecOptions, pods/eviction is policy/v1 Eviction). Parent kind plus capitalized subresource reproduces the operationId tails exactly, so :PodLog reaches the table through the generic path instead of a special case. - Generated model and function identifiers are read off the planner, not recomputed. The naming rules normalize non-identifier characters, dodge Base/Core and reserved names, and disambiguate collisions with a counter; a hand-rolled version got io.k8s.apiextensions-apiserver... wrong on the first try and would have drifted silently later. Meta kinds (Status, DeleteOptions, WatchEvent, APIResourceList) are registered under every group-version in every document and each module has its own copy, so KIND_TYPES resolves them by policy: own group-version wins, then core, then alphabetically first. Cross-module identity for those types is therefore not guaranteed — `isa(res, kind_to_type(ctx, :Status))` on a non-core response is a Phase 3/4 item. src/Kuber.jl is reduced to the generated layer for now; helpers.jl and simpleapi.jl still target the 0.2.x runtime and stay on disk as the porting reference for Phases 2 and 3. Gate (§6.1): pipeline clean, `using Kuber` precompiles in ~23s and loads in ~2.2s, test/registry.jl green (3639 assertions, no cluster needed).
Plan §4.3 asks for this before k8s_retry_cond is written, since the exception types are runtime internals rather than a stable contract. A manual tool, not part of runtests.jl; rerun it whenever the OpenAPI pin moves. Six failure modes, probed against raw TCP servers (precise control over truncated bodies and dropped connections) and the live k3s cluster. Three results contradict what the plan assumed: - the watch call never throws on consumer close. It returns at the response head, so there is no in-flight call to retry and the `isopen(stream)` guard has nothing to guard; retry-vs-stop has to live in the re-watch loop instead. - a connection dropped on an item boundary closes the channel CLEANLY, exactly like a watch ending normally. A clean close therefore cannot be read as "stop" — only the consumer closing the channel can. - 410 Gone is not an ApiError. k8s answers an expired resourceVersion with HTTP 200 and an in-stream ERROR event carrying a Status(reason=Expired). The rest confirm the plan: retryable statuses arrive as ApiError with .status, transport failures as HTTP.ConnectError (is_request_interrupted is gone), and a truncated item closes the channel with DecodeError.
Context, clients, discovery, retries, exceptions and conversions, per plan §4. KuberContext now holds a server URL and a lazily-built client per group module, because a Runtime.Client is bound to its module compiled _SPEC and cannot be shared across modules. Each client registers the watch codec at construction; the codec is inert until a call passes accept=WATCH_MEDIA, so buffered calls on the same client keep decoding typed models. Discovery keeps its semantics (probe /api and /apis, tolerate groups we do not ship, honour the `override` kwarg, preferred version first) but issues the two probes with plain HTTP.jl: they were the only reason the old code needed the generated ApisApi/CoreApi wrappers. ctx.modelapi is built from KIND_TYPES rather than a names() scan, so it holds exactly the addressable kinds, and core is registered first with earlier registrations winning — the meta kinds every group redefines (Status, WatchEvent, DeleteOptions) resolve to core deterministically instead of by dictionary order. k8s_retry_cond encodes what test/characterize_retries.jl actually observed rather than what the plan predicted. ApiError.status covers the retryable statuses; transport failures are now stated as an exclusion over HTTP.HTTPError, since HTTP 2.x has no RequestError and puts everything under that supertype — retry the accidents, not the decisions (CanceledError, StatusError, TooManyRedirects, AddressInUse, RetryDenied). The old `isopen(stream)` guard is gone from the retry path entirely: a watch call returns at the response head, so it can never be the in-flight call being retried, and stop-vs-re-watch moves to the watch loop in Phase 3. Timeouts move from the client mutable timeout[] to request_options on the context, carrying HTTP 2.x names (request_timeout, connect_timeout, read_idle_timeout, and TLS config). set_timeout sets request_timeout, and watch calls drop it: a watch has no meaningful overall deadline, and k8s bounds one with the timeoutseconds query parameter instead. Gone with the 0.2.x runtime: check_api_response and the (result, response) tuples (operations throw ApiError now), get_return_type sniffing, the Downloads.Response header handling, the Connection header and httplib selection, the int-or-string val_format extension (v3 declares IoIntOrString as a real oneOf), and the convert() piracy on String/Dict. kuber_obj now decodes through KIND_TYPES, and _field() is added for reading fields that may be ABSENT — the one user-visible semantic change, since `nothing` now means an explicit JSON null. Gate (§6.2): test/helpers.jl green offline (82 assertions), and live discovery against k3s v1.35.4 maps 16 groups and 101 kinds with the server preferred versions ordered first.
The verbs keep their signatures and semantics; internals become registry
lookups (§5). Resolution is: module from the apiversion kwarg or ctx.modelapi,
then OPS[(module, verb, kind, scope)] with the old namespaced -> cluster ->
all-namespaces fallback chain as table probes instead of isdefined probes.
Positional arguments come from OP_PARAMS, so the namespace goes first and a
required body last. Snake_case kwargs are translated at the boundary
(label_selector -> labelselector) and nothing values are dropped rather than
forwarded, since generated optionals are Union{Absent,T} and an explicit
nothing fails request validation.
The watch rewrite is shaped by the characterization, not the plan sketch. The
generated call returns at the response head, so `list` cannot delegate and
return — it would close the stream immediately and yield zero events. Instead
its watch branch pumps the raw channel inline and returns only when the watch
is over, which keeps `watch(processor, ctx, list, O)` and both of its `finally
close(stream)` blocks working exactly as they did under the 0.2.x client. In the
loop: the consumer closing the public stream is the only stop signal (#67/#68),
a clean close of the raw channel means re-watch from the last resourceVersion
seen (a dropped connection on an item boundary is indistinguishable from a
normal end), a DecodeError means the same, and an in-stream ERROR event with
code 410 restarts without a resourceVersion — which is how k8s actually reports
an expired one.
Two further spec lies surfaced, both found by exercising verbs the evaluation
never had (it only ever listed and watched). Both are new patch rules, not
validation opt-outs:
- request bodies documented as `*/*`. Every create/replace body says `*/*`,
which no client can encode to: there is no `*/*` encoder, and the runtime
would send `Content-Type: */*` even if there were. Rewritten to
application/json, which is what we send and what k8s accepts. Patch bodies
keep their five explicit media types.
- DELETE 2xx responses documented as `Status`. A delete usually answers with
the deleted object instead — verified live: deleting a Job returns the Job,
deleting a Deployment returns a Status, so the ambiguity the old client hid
behind get_return_type sniffing is real and both shapes occur. `oneOf:
[Status, resource]` was tried and rejected (the generator emits one wrapper
type per response code per media type — eight for a single delete). The
responses now carry the empty schema, which states what is actually true, and
delete! restores the type from the payload kind/apiVersion through
KIND_TYPES — the same second-stage decode watch frames use.
update! wraps the patch in the generated Patch type (an open object, so a Dict
cannot be passed through) and validates patch_type against the documented media
list, since k8s offers no plain application/json for PATCH. Custom metrics throw
a clear out-of-trial error (§0). get_logs needs no special handling: the pod-log
response decodes to a String.
Registry gains OP_BODIES (body type + documented media types per operation) so
update! reads both from build-time tables rather than reflection.
Gate (§6.2): test/simpleapi.jl green offline (59 assertions, including the #67
watch-abort canary). Live against k3s v1.35.4: put!/update!/delete! round-trip a
Pod, a Deployment and a Job; watch delivers a typed initial list plus typed
ADDED/DELETED events; get_logs returns pod logs.
runtests.jl now runs the three offline suites first (registry, helpers, simpleapi) and then the integration suite, which is skipped with a warning when no API server is reachable and honours KUBER_TEST_SERVER. Expected diffs from the 0.2.x suite, all of them semantic rather than cosmetic: - the versioned-model test used batch/v1beta1 and batch/v2alpha1 CronJobs, and the override test used apps/v1beta2 and apiregistration.k8s.io/v1beta1. None of those exist on a 1.35 server; autoscaling is now the group that still serves one kind in two versions (v2 preferred, v1 available), so it is what exercises versioned typing and the `override` kwarg. - Typedefs.CoreV1.WatchEvent -> KuberEvent, and event.object is already typed, so the kuber_obj round-trip in the watch assertions is gone. - delete! assertions compare kuber_kind, not the type: each group module has its own Status type, so a batch/v1 delete can never be `isa` core Status. - the timeout test is rewritten against request_options (there is no DEFAULT_TIMEOUT_SECS to compare with; unset means no deadline) and also asserts a watch drops request_timeout. - new coverage: all-namespaces listing, a 404 surfacing as KuberException with a decoded Status, a rejected patch media type, and a Job + Deployment create/patch/delete round-trip. - the `killall kubectl` teardown is dropped: it worked around a libcurl segfault on exit, and Downloads.jl is no longer a dependency. - test_watch_processor_failure moved to test/simpleapi.jl, where it belongs with the other offline checks. watch_latency.jl is adapted for the new API, and running it surfaced a real usability trap: metadata.annotations decodes to a generated open struct, not a Dict, so reading it by index silently failed and every MODIFIED event was reported as MISSED. This is trap 6 of the plan (index open payloads via additional_properties) meeting ordinary code. Added `kuber_props` to normalize the shapes a k8s string map can take — ABSENT/null, a real Dict, or the struct — since labels and annotations are far too common to make callers reach into additional_properties themselves. Docs: README gains a trial-branch section covering every user-visible change (ABSENT vs nothing, lowercase field names, open string maps, KuberEvent, per-module Status types, request options, what is out of scope); SupportedAPIVersions.md is rewritten for the 17 generated group versions and records what is deliberately absent; WalkThrough.md and Metrics.md get banners, and WalkThrough loses a `!== nothing` check on a field that is now ABSENT. CLAUDE.md is rewritten for the two-layer-plus-registry architecture, the watch pump, and the five patch rules. CI drops Julia 1.6 for the 1.11 floor. Gates (§6.3-§6.5): full suite green against k3s v1.35.4 — 3639 + 89 + 59 offline and 303 live assertions. The prototype cross-check k8spristine_v3.jl passes 11/11 against the pinned OpenAPI, confirming upstream behaves as the evaluation recorded. watch_latency reports 5-11 ms reaction times after warmup, so the watch wrapper adds no buffering of its own.
…p spinning test/watch_recovery.jl exercises the four watch criteria of §6 that need failure injection rather than a cooperative cluster: a consumer stopping a watch, a stream ending mid-watch (#68), a truncated item, and an expired resourceVersion. It runs offline against a fake apiserver and is included from runtests.jl. Writing it turned up three things. The pump only noticed a stop when the next frame arrived. On a quiet resource that can be minutes, which left `watch()` hanging and — under `watch(streamprocessor, ...)` — kept the @sync alive after the processor died, the exact deaf watch #67 fixed. A watcher task now closes the raw channel when the consumer closes the public stream, so a stop takes effect in ~250ms regardless of traffic. The re-watch loop could spin. k8s_retry only wraps the *establish* call, so a server that answers 200 and ends the stream without delivering anything is not a failure and nothing throttled the next attempt — reachable with an unservable resourceVersion or a proxy dropping long connections, and it would have hammered the apiserver. Consecutive establishments that deliver no events now back off 0.25s to 8s, reset by any delivered event, and abandon the wait as soon as the consumer stops. `wait(stopwatcher)` is guarded so it can never mask the error that reached the finally block. Also fixed: `get(ctx, O, name)` in a watch context clobbered a caller-supplied field_selector with its own metadata.name filter; the two are now ANDed as k8s expects. The harness is built on HTTP.jl rather than raw TCP, after a hand-rolled chunked server turned out to deliver nothing to the HTTP.jl client until the connection closed — bytes curl streamed happily. Using one library on both ends keeps the framing beyond question; the real apiserver framing stays covered live by watch_latency.jl. Handlers hold responses open by polling a flag rather than sleeping, because close(server) waits for in-flight handlers and a sleeping one hangs teardown instead of failing the test. Verified after these changes: full suite green against k3s v1.35.4 (3639 + 89 + 59 + 32 offline, 299 live) and watch_latency reports 0 missed events with 5.6-11.2ms medians, so the stop watcher and backoff cost nothing in the steady state.
A connection aborted part-way through a chunk — an apiserver restart or a network drop, the one #68 shape a clean stream end does not stand in for — closes the watch channel with an HTTP.jl error (ParseError: unexpected EOF while reading HTTP/1 data), not the DecodeError a truncated *item* gives. The pump recovered from DecodeError only and rethrew everything else, so this killed the watch outright. The earlier characterization missed it because that probe used an unframed response body, where an abort closes cleanly; real k8s is chunked. Verified against an HTTP.jl server force-closed mid-chunk. The pump now re-establishes for anything k8s_retry_cond accepts as well as DecodeError, which strictly widens recovery and cannot regress the stop path — consumer close is checked first. Asserted in test/helpers.jl at the decision point, since reproducing it end-to-end needs the listener killed underneath the client, leaving nothing for the retry to reach. OpenAPIv1TrialResults.md records what the branch actually does: the five deviations from the plan with their reasons, the §6 measurements (precompile 22s, load 0.36s, TTFX ~15s for the first list(ctx, :Pod), steady state 11.5ms of which 78% is response validation, watch reactions 5.6-11.2ms), the test suite inventory, the expected diffs in the adapted tests, and the follow-ups. The plan gets a header pointing at it, since the plan is committed on this branch and is the first thing a reviewer reads — five of its statements are now false, and commit messages are not where design truth should live. Full suite green after both changes: 3639 + 94 + 59 + 32 offline, 307 live.
…ated from CI inherited master's kind pin (v0.11.1, node image kindest/node:v1.21.1). That was harmless on the 0.2.x line but not here: the client is generated from the v1.35.4 OpenAPI documents and strict response validation checks every reply against those schemas, so the cluster version is part of the contract. The first push failed exactly one assertion on all three Julia versions -- ctx.apis[:Autoscaling][1] is the v1 module because a 1.21 apiserver prefers autoscaling/v1 (v2 went GA in 1.23). All 3824 offline assertions passed unchanged on 1.11, 1 and nightly, so the branch was fine and the workflow was not. Pin kind v0.32.0 with the v1.35.5 node image (one patch release off the spec tag; patch releases do not move the API surface) and record the CI result in the results doc, including that the 1.11 compat floor is now exercised.
…ot provide Adds OpenAPIv1ConsumerGaps.md, a working document to iterate on. Surveys JuliaRun.jl (with JuliaHubK8sApi.jl), services/JobLoops, packages/K8sReflector, JuliaRunPool, AccessControl and BillingService against what this branch provides and what its tests cover. The headline is that test coverage is the second-order problem. The consumers do not use Kuber's generated layer at all: they use the verb layer over JuliaHubK8sApi, which is a drop-in replacement for api/Kubernetes.jl + api_typemap.jl + api_versions.jl, plugged in through KuberContext(apimodule) -- a parameter this branch removed. It also covers far more group versions than the 17 upstream ones shipped here, including CRD groups and custom.metrics.k8s.io that gen/openapi_v1/ deliberately cannot produce from release-tag specs. Split into seven hard incompatibilities (C1-C7) and sixteen test gaps (G1-G16), each with a checkbox and a stable identifier to cite in review. The one silent correctness item is G1: the pump swallows the in-stream 410 and re-watches with no resourceVersion, so K8sReflector's cache-invalidation path is dead code and its store keeps phantom entries for objects deleted while the watch was gone. Mechanism verified against the code and against a live cluster. Supersedes the narrower ABSENT/open-struct checklist line in the results doc.
… real usage The first revision framed C1 as one big blocking item. It is two problems with very different costs, and the mechanism half was overstated. C1a, the mechanism, is cheap: _new_client is already duck-typed on the module (mod.Client, no _SPEC reference anywhere in src/), the verb layer binds the registry tables rather than their contents (const bindings to mutable Dicts, so merge! is visible with no recompilation), and the keys make merging conflict-free -- OPS is (module, verb, kind, scope), KIND_TYPES is (apiVersion, kind). So registration is a merge! of six dicts, not a redesign. It must run in the downstream package's __init__, since mutations to another module's state do not survive precompilation. Restoring KuberContext(apimodule) is argued against: it saves no regeneration and reinstates the per-context scoping that keying OPS by module removed. C1b, the content, is small once scoped. Adds an appendix auditing what JuliaHubK8sApi's 43 group versions are actually used for: this branch's 17 are a strict subset, and of the 26 extras only metrics.k8s.io/v1beta1 and custom.metrics.k8s.io/v1beta1 are reachable through Kuber (v1beta2 needs a runtime check). No CRD group is used at all -- JuliaRun's ServiceMonitor and Prometheus manifests are kube-prometheus YAML applied by kubectl, and its one karpenter reference is a node label string. The audit records how the dynamic put!(cm, Symbol(job["kind"]), job) path was traced, since a grep for type names would have been wrong three times over. C1c prices the JuliaRun port: Typedefs is a generated tree of plain aliases, so its ~28 references survive verbatim if the tree is re-emitted over the new type names. Only the three WatchEvent/Status isa checks have to change. C1d records the dynamic register_crd! option as deferred future-proofing rather than a prerequisite, including that discovery already supplies plural, kind and scope, and that it would bypass response validation. C5 shrinks to the only remaining JuliaHub feature gap: two group versions plus reimplementing the two custom-metrics helpers.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What this is
A trial rebuild of Kuber.jl on OpenAPI.jl 1.0 (JuliaComputing/OpenAPI.jl#103), replacing the entire generated API layer and rewriting the verb layer against the new runtime. It follows
OpenAPIv1TrialBranchPlan.md;OpenAPIv1RewriteNotes.mdis the evaluation behind it.Read
OpenAPIv1TrialResults.mdfirst. It is the implementation record: what was built, the five places implementation contradicted the plan and why, the measured numbers, and what is left. Where the plan and the results doc disagree, the results doc is current.Draft, and it cannot merge as-is:
Project.tomlpins OpenAPI via[sources]to an unmerged commit (quinnj/OpenAPI.jl@1ff9ba8). Generated output is byte-stable only within a pinned commit, so when JuliaComputing/OpenAPI.jl#103 is merged and tagged the pin has to be dropped and everything regenerated.Shape of the change
src/ApiImpl/generated/— one generated module per Kubernetes group version (K8sV1,K8sAppsV1, …), 17 group versions, each carrying its own models, operations and embedded JSON Schemas. Replaces the oldsrc/ApiImpl/api/tree.src/ApiImpl/generated/registry.jl(also generated) — the lookup tables the verb layer resolves through:GROUP_MODULES,MODULE_GVS,KIND_TYPES,OPS,OP_PARAMS,OP_BODIES. These replaceapi_typemap.jl/api_versions.jland all the string-munging plusevallookups.src/helpers.jl,src/simpleapi.jl— rewritten.KuberContextnow holds oneRuntime.Clientper group module (a client is bound to its module's compiled spec and cannot be shared), HTTP.jl 2.x request options, and a retry condition built from an actual characterization of the runtime's exception types rather than guesswork (test/characterize_retries.jlrecords the findings).gen/openapi_v1/— the reproducible generation pipeline: fetch pristine OpenAPI v3 documents from akubernetes/kubernetesrelease tag, patch them withpatch_k8s_spec.jq, generate in strict mode, emit the registry. See its README.get,list,put!,update!,delete!,watch,sel) is unchanged in shape.Strict generation and strict response validation are on, throughout. A
SchemaValidationErroragainst a real cluster means the spec lies and the fix is a new patch rule — there is novalidate_responses=falseanywhere insrc/. Two of the five patch rules were found exactly that way.What changes for callers
ABSENT, notnothing— the one semantic change to watch for.nothingnow means an explicit JSONnull. Code testing "not set" withx.field === nothingmust useKuber._field(x.field) === nothing._suffix on collisions:metadata.resourceversion,obj.apiversion,event.type. Type names are unchanged (IoK8sApiCoreV1Pod).Dicts.metadata.labels/annotationsentries live inadditional_properties; usekuber_props(pod.metadata.annotations)["key"].KuberEventwith an already-typedevent.object; thekuber_objround-trip is gone.apps/v1'sStatusis not core's. Comparekuber_kind(res) == "Status", not the type — this matters fordelete!.set_timeoutsetsrequest_timeout; watches deliberately carry no overall deadline (bound them withtimeout_seconds).KuberException— no(result, response)tuples.Watches
The subtlest part, and worth reviewing closely. There are no dedicated watch operations — the deprecated
/watch/paths are deliberately not patched back in. Watching iswatch=trueon the list op with an accept-scoped codec (application/json;stream=watch), which fires only for calls that ask for it, because a real apiserver replies bareapplication/json. Since the generated call returns at the response head,list's watch branch pumps the stream inline and returns only when the watch is over — that is what keepswatch(processor, ctx, list, O)and itsfinally close(stream)blocks working.Four watch-lifecycle bugs were found and fixed during the trial (consumer-close as the only stop signal, resume from the last
resourceVersion, in-stream 410 restarting without one, and a re-watch spin on a 200-with-no-events).test/watch_recovery.jlcovers all of it against a fake apiserver.Testing
CI is green on Julia 1.11, 1 and nightly (run 31702176819) — 3824 offline assertions plus a live suite against a Kubernetes 1.35
kindcluster.test/registry.jltest/helpers.jltest/simpleapi.jltest/watch_recovery.jltest/runtests.jllive suiteregistry.jlis the generation gate: every table entry resolves and no deprecatedwatch*operation leaks in. The live suite is skipped with a warning when no server is reachable.Note that CI's cluster version is now load-bearing, which it was not on the 0.2.x line: responses are validated against the v1.35.4 schemas the client was generated from, so the workflow pins kind
v0.32.0with thev1.35.5node image. A spec bump has to move that pin with it.Not in
runtests.jl, kept as manual probes:test/characterize_retries.jl(pins the runtime's exception types) andtest/watch_latency.jl(measured 5.6–11.2 ms reactions, 0 missed events).Known limitations
Out of trial scope by decision:
Metrics.mdis annotated.custom.metrics.k8s.ioneeds a document captured from a cluster that serves it.metrics.k8s.io) and CRD groups likewise — neither appears in upstream release-tag specs.Found during the trial:
listis ~15 s. APrecompileToolsworkload is the obvious next step.Before this can come out of draft
[sources], set compat to the tag, regenerate everything.=== nothingon model fields and formetadata.labels/annotationsindexing.kubectl proxy, so the credential path is untested against real TLS and bearer tokens.Two things for reviewers to rule on: whether
gen/openapi_v1_prototype/(5 files of reference code from the evaluation) belongs in the repo or should be dropped before merge, and whether the three trial documents should be consolidated once this graduates.