Releases: guanchzhou/zig-klient
Release list
v0.8.2
v0.8.1
Changed
- Zig 0.17-dev is supported alongside Zig 0.16.0.
- The unused zig-protobuf dependency and its convenience re-exports were removed.
requestWithProtobufremains available for callers that own Kubernetes
Protobuf encoding and decoding. This is a breaking change to the pre-1.0
convenience API. - yaml-zig is pinned to the dual-compatible Zig 0.16/0.17 fix proposed upstream.
v0.8.0
Added
- Watch streams can use a caller-provided GET transport and report when the
response is established and when bookmark resource versions advance. - Buffered requests can return the HTTP status alongside the response body.
Changed
- Kubeconfigs without
current-contextparse successfully so callers can select
an explicit context.
Fixed
- IngressClass
specand CronJobsuspenddecode from their correct wire fields. - CPU metrics accept Kubernetes microcore (
u) quantities.
v0.7.0
Added
- Callback-scoped streaming GET through
K8sClient.streamGet(io, path, options, context, callback). The callback receives
copied response metadata and a decompressed body reader while the request, response,
redirect storage, decompressor, and buffers remain alive in the caller's task.
Bearer credentials are retained across same-origin redirects and omitted whenever
scheme, host, or effective port changes. - Structured watch completion through additive
watchOutcomeand
watchWithContextOutcomemethods.WatchOutcomedistinguishes EOF, cancellation,
401, 403, HTTP 410, throttling with copied integerRetry-After, server errors,
other HTTP failures, malformed events, KubernetesStatuserrors, transport
failures, and typed-object decode failures. Existingwatchmethods remain
compatibility wrappers, includingExpiredResourceVersionfor either HTTP 410 or
an in-streamStatuswith code 410 or reasonExpired. - Public exports for
resource_registry,StreamGetOptions,StreamResponseMeta,
WatchOutcome, and bounded, inline-ownedWatchErrorDetail. - Kubernetes 1.37 GA kinds, typed and registered:
DeviceTaintRule(resource.k8s.io/v1, cluster-scoped)ClusterTrustBundle(certificates.k8s.io/v1, cluster-scoped)PodCertificateRequest(certificates.k8s.io/v1, namespaced). The v1
schema dropsPKIXPublicKeyandProofOfPossessionvs v1beta1; those
fields are not modeled.
- Live integration entrypoint
test-k8s-137-crud— list → create → get →
delete ofDeviceTaintRule,ClusterTrustBundle, and
StorageVersionMigrationv1 throughkubectl proxy. Verified against
Kubernetes 1.37.0 (kindest/node).PodCertificateRequestis not created
live: the apiserver requires a real pod/node/service-account UID plus a
kubelet-shaped PKCS#10 stub. MetricsClient.initFromDiscovery/groupVersionFromDiscovery: use
metrics.k8s.io/v1only when this cluster's/apisprefers it. Default
initstays on v1beta1.
Changed
- WATCH framing accepts events up to 4 MiB and rejects larger frames without
unbounded allocation. Malformed events andERRORevents now terminate with an
inspectable outcome instead of losing their reason. StreamGetOptions.prettyis tri-state. Setting it tofalseemits
pretty=falseexplicitly, so compact LIST requests do not depend on omission
semantics; existing query parameters retain their order and fragments stay out of
the request target.StorageVersionMigrationregistry pinstoragemigration.k8s.io/v1beta1→
v1(GA in 1.37; v1beta1 is deprecated, removal targeted 1.40).- README coverage figures corrected: 73 kinds / 19 API groups (was still
advertising the pre-0.6.0 "65 / 20" count, listed EndpointSlice under the
wrong group, and linked a missing FEATURE_PARITY_STATUS.md). Architecture
tree no longer names the deletedkubeconfig_json.zig.
Fixed
- Corrupt or truncated compressed 401/403 bodies preserve the HTTP status in
caller-ownedApiErrordetail and are not misclassified as retryable transport
failures. - 307/308 responses to requests with bodies are rejected without replaying the body
and without leaking redirect storage. - BOOKMARK resource versions remain owned across callback teardown; malformed
BOOKMARK andERRORpayloads return bounded diagnostics. Code-only 410Status
events retain compatibility with informer relisting.
Notes
metrics.k8s.iodefault pin stays v1beta1. Re-checked 2026-08-30:
metrics-servermasterpkg/api/install.gostill only mapsv1beta1;
issue 1786
is open and PR 1855
is still a draft. The apiserver defining v1 in 1.37 does not change that —
metrics.k8s.io is an aggregated API.MetricsClient.initFromDiscoverywill
use v1 once a cluster's/apisprefers it.- Alpha 1.37 kinds stay untyped (tripwire in
tests/k8s_137_test.zig):
lifecycle.k8s.io/v1alpha1Eviction + EvictionRequest (EvictionRequestAPI,
default off; Pod field is.spec.evictionResponderson the 1.37.0 tag),
scheduling.k8s.io/v1alpha3CompositePodGroup, beta
scheduling.k8s.io/v1beta1Workload + PodGroup (GenericWorkload). Use
Discovery+DynamicClientif a cluster has the feature gates on.
Pods.evictremains the GApolicy/v1pod subresource. StorageVersionMigrationv1spec.resourceis GroupResource (group+
resourceonly). Sendingversionis a strict-decoding 400.- Direct HTTPS to an API server is still blocked in Zig std, not this
library.std.crypto.tls.Clienthas nocertificate_requestarm
(ziglang/zig#19521) on 0.16.0
and on 0.17.0-dev.1936.kubectl proxy/connectWithFallback()remain the
working path. Tripwire:tests/advanced_features_test.zig. - Zig 0.17-dev still cannot build this project. Reproduced on
0.17.0-dev.1936+5a625d5f3: yaml-zigbuild.zigcalls removed
b.pathFromRoot; zig-protobufbuild_util.zigsets removed
StepOptions.id(renamed totag). The library sources also fail on 0.17's
removal of the**splat. zig-protobufzig-master(50f1b1c) is a 0.17
port and would break 0.16; yaml-zig has no 0.17 port. Do not bump either pin.
This project's own source is still not compiled on 0.17. resource.k8s.io/v1beta1removal is a no-op: DRA kinds are already pinned
to v1 (GA 1.34). kubernetes#137924 stops serving v1beta1 in 1.41
(unsupported in 1.38); master still registers v1beta1Storage as of 2026-08-30.
Tripwire:tests/resource_registry_test.zig.
v0.6.0
Added
-
Core API discovery (
src/k8s/discovery.zig). Wraps the two coremeta/v1
endpoints —GET /apisandGET /apis/{group}/{version}— behindhasGroup,
preferredVersionandfindResource(group, kind), the last returning a ready-to-use
CRDInfocarrying the version, plural and scope the server actually reports.This is the general answer to "support an API if the cluster has it, ignore it
otherwise", and it needs no third-party types. Hardcoding group + version + plural
gives three chances to be wrong and the resulting 404 is usually swallowed, so the
feature silently never activates — which is exactly what had happened downstream with
Cedar (cedar.k8s.io/v1alpha1/cedarpoliciesagainst a real API of
cedar.k8s.aws/<served>/policies).Establishes the boundary: APIs that are GA and near-universal stay typed in the
resource registry, where they are cheaper; optional and vendor-specific ones are
discovered at runtime. -
Gateway API standard channel completed — all 10 kinds of Gateway API v1.6.1.
AddsTCPRoute,TLSRoute,UDPRoute,BackendTLSPolicyandListenerSet, all at
v1and namespaced. Versions, plurals, scopes and spec fields were read from each
upstream CRD'sopenAPIV3Schemarather than inferred fromHTTPRoute. Half-covering
a GA API is worse than either extreme: callers cannot tell what is supported without
reading the table.ReferenceGrantdeliberately remains onv1beta1. v1.6.1 serves it at bothv1and
v1beta1, andv1beta1is stillstorage: true— bumping a correct pin would be
churn. There is a comment in the registry and a test that names the condition for
revisiting.
Fixed
- The resource registry now asserts its own contents. The existing tests constructed a
ResourceClientfrom hand-written literals and then asserted those same literals, so
they passed regardless of what the table held and a wrong group or plural shipped
silently. The new assertions read the real entry viainitFromRegistry— safe with an
undefinedclient, since every field comes fromcomptime metaFor(T)and the pointer
is never dereferenced. Verified by mutation: breaking one plural in the table fails
the suite, restoring it passes.
Notes
- Both additions are additive; no existing API changed.
- On Zig 0.17-dev the build still fails with two errors, both in dependency
build.zig
files and none in this project's source (zig-protobufuses the removed
Build.Step.StepOptions.id;yaml-ziguses the removedb.pathFromRoot). Because
those are build-script failures, this project's own source is never compiled on 0.17 —
so its 0.17 compatibility remains unknown rather than confirmed.
v0.5.0
Changed
-
Kubeconfig YAML parsing switched from
guanchzhou/zig-yamlto
sakakibara/yaml-zig, and the
hand-written tree walkers replaced with comptime typed decoding (net -244
lines). Upstreamkubkon/zig-yamlhas been unmaintained since 2026-01, so the
fork inherited its bugs permanently and every future Zig migration by hand.
The replacement is 0.16-native, actively developed, and additionally supports
anchors, aliases and merge keys, which the fork rejects outright.The public API is unchanged —
Cluster/Context/User/Kubeconfig, the
lookup accessors, anddeinit(allocator)all keep their signatures.Kubeconfignow owns a heap-allocated arena and frees its whole graph at
once, replacing ~90 lines of per-field frees and errdefer unwinding.deinit
still accepts an allocator for compatibility but ignores it in favour of the
arena's ownchild_allocator, so a mismatched allocator can no longer
corrupt the free.
Fixed
- Double free in
loadInClusterConfig. Anerrdefer allocator.free(token)was
paired with an unconditionalallocator.free(token)a few lines later, so any
later failure unwound the errdefer and freed the same allocation twice. The
error.ServiceAccountCANotFoundpath immediately below it is reachable in a real
pod with noca.crt. - Use-after-free of the watch
resourceVersion. A BOOKMARK stored a slice that
pointed into the event's parse arena and then freed that arena, leaving a dangling
value that was read by the informer and interpolated into the next watch URL — so a
garbageresourceVersionwas sent to the API server.Watchernow owns a duped
copy and exposesdeinit()to release it; the borrowed initial value from
WatchOptionsis never freed. - Uninitialised read on every watch event.
WatchEnvelope.objectwas
T = undefinedand the informer dereferencedevent.object.metadata.namefor all
event types. It is now?T = null. - A single bookmark tore down the whole watch.
ObjectMeta.nameis required, but
a BOOKMARK's object carries onlyresourceVersionand an ERROR's object is a
Status, so neither binds toT— andallow_watch_bookmarksdefaults to true.
Events are now dispatched on a type-only envelope first, and a single malformed
object is logged and skipped instead of killing the stream. - Deadlock in the exec credential plugin.
.stderr = .pipewas requested and
never drained, so a plugin writing past the pipe buffer (~64 KiB, easily reached by
aws eks get-tokenemitting warnings) blocked on write while the client blocked in
child.wait(). stderr is now inherited, and a guardederrdeferreaps the child on
earlier failure paths. - Watch and pod-stream query parameters are percent-encoded. The 0.4.0 encoding
fix reachedListOptionsbut notwatch,exec,attachorport-forward, which
kept hand-building their query strings. A set-based selector from
LabelSelector.addIn("app in (a,b)") produced a malformed request line, so the
library's own selector API could not be used with its own watch. For the pod streams
it was also an injection: a command endingls&stdin=trueturned on stdin even
though the caller had not asked for it. emit_null_optional_fieldsis now false for resource, patch and cache
serialization. Since every Kubernetes type is?T = null, bodies were mostly nulls.
JsonPatch.buildemitted{"op":"remove","path":"/x","value":null,"from":null},
which RFC 6902 forbids and strict implementations reject.- A
Statuswithoutcodeno longer makes 404s and 403s retry. The code fell
back tonull, whichretry.shouldRetrycannot distinguish from a transport
failure, so such responses were retried the full budget with backoff. The HTTP
status is now used as the fallback. insecure-skip-tls-verifyis no longer silently dropped. The old
parseClusteraccepted only a.booleanYAML value, but zig-yaml built
.booleansolely on its stringify path and never when parsing, sotrue
arrived as the scalar"true"and was discarded. Still masked downstream by
theinsecure_skip_verifyguard inclient.zig, which reports it as
unsupported becausestd.http.Clientdoes not expose TLS internals.
Notes
zig build test-fuzz --fuzzdoes not compile on Zig 0.16.0 due to a bug in
the toolchain's owncompiler/test_runner.zig(*builtin.StackTracevs
*const debug.StackTrace), unrelated to this change and reproducible on an
unmodified tree. The single-shot fuzz run underzig build teststill works.
Removed
- Dead code, each verified unreferenced across
src/,tests/,docs/and the
README before deletion:client.KubeConfig(superseded by
kubeconfig_yaml.Kubeconfig),tls.readFileToAlloc(whose doc comment cited two
functions that do not exist), andretry.retryWithBackoff(which duplicated
sendWithRetry's loop and tookoperation: anytypebut invoked it with no
arguments). version/versionString()inklient.zigreported0.1.0-alphawhile the
manifest declared0.4.0— three releases stale, in two hand-maintained copies.
Both must now be bumped alongsidebuild.zig.zon; there is no compile-time link.
Build
test-comprehensiveand the WebSocket integration test now actually compile.
The former was_ = b.step(...)— a step with no dependencies, so it built and ran
nothing — and the latter was never referenced bybuild.zigat all. Between them
1,933 lines of test code had silently stopped compiling against Zig 0.16. Both are
wired up, and the suites migrated to the 0.16 APIs (std.process.run,
std.heap.DebugAllocator, and threadingstd.Io). They still require a live
cluster to run; compiling them in CI is what stops them rotting again.
v0.4.0
Fixed
- A non-JSON error body no longer discards the status code.
setApiErrorFromStatusJson
is best-effort, and the JSON request path had no fallback: when a load balancer or
ingress in front of the API server answered503with an HTML body, the caller got
error.K8sApiErrorwithlast_api_error == null— no reason, no message, not even
the status. The Protobuf path already handled this; both now do. last_api_errorno longer leaks across requests. It was cleared only when a
new KubernetesStatuserror replaced it, so a successful call — or any
transport-level failure, which never populates it — left the previous call's error
in place. A caller inspecting the field after catching an error (the documented
way to get error detail, and what the integration entrypoints do) reported a
stale, unrelated cause. It is now cleared at the start of every request attempt,
so it describes the current request or is null.- TLS options that cannot be honoured are now rejected instead of silently
dropped.K8sClient.initacceptedclient_cert_data,client_key_data,
client_cert_path,client_key_path,insecure_skip_verifyandserver_name
and then used none of them — only the CA was ever wired up. A caller configuring
client-certificate auth got an unauthenticated client and, much later, an opaque
error.TlsInitializationFailed. These now fail at construction with
error.ClientCertificatesUnsupported,error.InsecureSkipVerifyUnsupportedor
error.TlsServerNameUnsupported.- Zig 0.16's
std.crypto.tls.Clienthas no client-certificate support, and
std.http.Clientexposes no verification or SNI overrides, so none of these
can be implemented here today. - The README advertised mTLS as a supported feature with a worked example. That
claim is withdrawn.
- Zig 0.16's
error.TlsInitializationFailednow explains itself.std.http.Clientreturns
it for every handshake failure with the cause discarded. The real cause against a
Kubernetes cluster is thatstd.crypto.tls.Clienthas no handling for the
certificate_requesthandshake message (it appears nowhere in
std/crypto/tls/Client.zig), and an API server sends one whenever started with
--client-ca-file— the default everywhere. The handshake aborts with
TlsUnexpectedMessage. The client now logs the cause and thekubectl proxy
workaround. The README previously blamed self-signed certificates, which was
wrong: a publicly-trusted managed cluster fails identically.- A failed TLS handshake is no longer retried. It is deterministic, so the retry
loop only burned the backoff budget and repeated the diagnostic four times. K8sClient.initno longer leaks the system trust store on error paths. The CA
bundle rescan allocates before any later failure could return; added anerrdefer
and moved config validation ahead of all allocation.
Fixed (earlier in this release)
- Query values are now percent-encoded.
QueryWriter.addStringemitted values
raw, so any value containing a space or reserved character corrupted the HTTP
request target. This made the library's ownLabelSelector.addIn/addNotIn
unusable:app in (traefik,coredns)produced the request line
GET /...?labelSelector=app in (traefik,coredns) HTTP/1.1, which a spec-compliant
server rejects with 400 Bad Request — and the retry loop then repeated it
four times. Verified fixed against a live apiserver.- Continue tokens were never affected: the apiserver emits them with base64
RawURLEncoding, which is already URL-safe. - Query strings now read
fieldSelector=metadata.name%3Dmy-pod. The apiserver
decodes before parsing selectors, so behaviour is unchanged for values that
previously worked.
- Continue tokens were never affected: the apiserver emits them with base64
Performance
Pod.statusandNode.statusare typed rather thanstd.json.Value. An
untyped status is parsed into a DOM — one hash map per object, per item.
Measured on a 500-pod / 2.5 MB list: 5.33 ms -> 4.46 ms (-16%) and
10.5 MB -> 5.25 MB resident (-50%).PodStatus/ContainerStatusalready existed but nothing referenced them —
PodwasResource(PodSpec), whosestatusis dynamic. They are now used,
and extended to cover what real objects carry (conditions, podIPs/hostIPs,
qosClass, startTime, container state/lastState, image, containerID).ContainerStateis typed too, sostate.waiting.reason— the reason kubectl
prints in the STATUS column (CrashLoopBackOff,ImagePullBackOff) — no
longer needs a DOM lookup.NodeStatuscovers conditions, addresses, nodeInfo and images.capacity/
allocatablestay dynamic: their keys are open-ended (hugepages-*, vendor
devices).status.imagesis routinely the largest part of a Node object.- Reads of
pod.status.?.object.get("phase").?.stringbecomepod.status.?.phase.
getNodeCountno longer downloads the cluster. It listed every Node in full
and DOM-parsed the result to return one integer; it now requests?limit=1and
readsmetadata.remainingItemCount, so cost is independent of cluster size.ResourceClient.listPageswalks a collection page by page, following
continuetokens.list()buffers the entire collection, so memory scales with
the cluster and the request fails outright pastmax_response_size(16 MB by
default — roughly 3k pods).listPageskeeps both O(page). Covered by
tests/entrypoints/test_list_pages.zig, which needs no cluster.
Changed (breaking)
-
client.last_api_erroris removed; error detail is returned, not stored.
Capture it withrequestCapturing/requestCapturingWithContentType/
requestWithProtobufCapturing, or by settingResourceClient.error_sink. The
storage and the strings belong to the caller (ApiError.deinit(allocator)).The field was mutable per-request state on a shared object, with three consequences:
K8sClientcould not be shared across threads, even though thestd.http.Client
underneath is thread-safe ("Connections are opened in a thread-safe manner").
Every thread therefore needed its own client and its own connection pool. A
ResourceClientis a value, soerror_sinklives at the call site: one client now
serves many threads, each with its own sink. Exercised by
tests/entrypoints/test_error_capture.zig.- The strings were freed by the following request, so anything a caller kept became
a dangling reference. - It survived across calls (fixed earlier in this release, and now structurally
impossible).
A sink holds the detail of the most recent call made through it, or null. Each
call frees whatever the previous one left there, so a sink is safe to reuse and a
success clears it. (Assigning without freeing leaked the earlier status/message/
reason — reported by Cursor Bugbot on the PR — and leaving it in place brought
back the staleness this change set out to remove.)Migration:
// before _ = client.request(.GET, path, null) catch |err| { if (client.last_api_error) |e| { ... } }; // after var api_err: ?klient.K8sClient.ApiError = null; defer if (api_err) |*e| e.deinit(allocator); _ = client.requestCapturing(.GET, path, null, &api_err) catch |err| { if (api_err) |e| { ... } };
request,requestWithContentType,requestWithProtobufandrequestWithRetry
keep their signatures and simply report no detail.
Changed
- The JSON and Protobuf request paths now share one implementation.
requestWithProtobufcarried its own ~90-line copy of the send/receive logic —
URL building, auth, redirects, status handling, decompression, size limiting —
differing only in two headers. The copies had already drifted (only one had the
status-code fallback above, and a fix earlier in this release had to be applied
twice).K8sClient.WireFormatnow carries theContent-Type/Acceptpair and
sendOncetakes it.- Protobuf requests consequently follow the same retry policy as JSON ones
(idempotent methods retried, POST sent once). Previously they were never
retried, which was an accident of the duplication rather than a decision.
- Protobuf requests consequently follow the same retry policy as JSON ones
- The four list entry points share one fetch-and-parse body.
list,
listAll,listWithOptionsandlistAllWithOptionseach repeated the same
request/parse block; they are now thin wrappers that differ only in base path.
Verified against a logging server to produce byte-identical URLs.
Removed
PaginatedList— declared but never constructed by anything;listPages
supersedes it.src/k8s/kubeconfig_json.zig(212 LOC) — self-marked deprecated, shadowed by
kubeconfig_yaml.zig, and referenced by nothing: not bybuild.zig, not
re-exported fromklient.zig, not imported by any module or test.tls.TlsBundle,tls.createBundle,tls.CertInfo,tls.loadFromFilesand
tls.validateCertKeyPair— all existed to assemble or check a client
certificate/key pair, whichK8sClient.initnow rejects outright.createBundle,
CertInfoandloadFromFilesadditionally had no callers at all, and
loadFromFilesproduced aTlsConfigthatinitwould refuse. Supply a CA with
TlsConfig.ca_cert_path/ca_cert_datainstead.tls.decodeBase64Certstays —
it is still useful for CA data out of a kubeconfig.src/k8s/tls.zigshrank from 201 to 80 lines.
Added
EventSeries(count,lastObservedTime) andEvent.series. The modern
client-go/tools/eventsrecorder (kubeletBackOff,Unhealthy, …) collapses
repeated events intoseriesand leaves the deprecatedcount/lastTimestamp
unset. Consumers rendering COUNT / LAST-SEEN must preferserieswhen present,
as kubectl does — otherwise an aggregated series renders as count0with a
last-seen stuck at the first occurrence.PersistentVolumeSpec.claimRef(ObjectReference) for the PV ...
v0.3.2
Fixed
getNodeCount/ClusterInfo.node_countnow return?u32—nullmeans the count
could not be determined (request/parse failure), distinct from a real empty
cluster (0). Previously any error was silently reported as0.
Docs
- Corrected the README to match the 0.16 API: every example now passes
ioto
K8sClient.init/WebSocketClient.init/executeCredentialPlugin/isInCluster/
loadInClusterConfig;DebugAllocator(notGeneralPurposeAllocator); the Watch
example uses the real callback API;Informer.init's 5 args; snake_case
ListOptions; removed all Connection Pooling references; fixed the Auth Methods
parity (4/5, not 100% — no HTTP basic auth) and cluster-scoped count (30).
Notes
- The remaining review items (re-export chain, spin-lock mutex,
client.client
indirection) are blocked by hard Zig 0.16 constraints (no blocking mutex;
usingnamespaceremoved;.clientis public API) and are not pursued.
v0.3.1
Remaining review backlog: functional robustness, supply-chain, and CI/docs.
Fixed
- Informer relist on
410 Gone: an expired/compactedresourceVersionnow
surfaces aserror.ExpiredResourceVersion; the Informer re-lists from scratch
(clearing the cache) and resumes watching instead of spinning on a doomed watch. - Protobuf error detail: the protobuf request path now reads the error body and
parses the JSONStatusthe API server returns (message/reason/code), instead of
only the HTTP code. SharedsetApiErrorFromStatusJsonacross both paths.
Added
- Live kind-based integration CI (
integration.yml, manual + nightly): runs
test-via-proxy+test-pod-execagainst a real cluster. - Fuzz target for the kubeconfig YAML parser (
zig build test-fuzz --fuzz). - API docs:
zig build docs(autodoc) + a GitHub Pages deploy workflow. - Release signing: cosign keyless signatures for
SHA256SUMS+ the SBOM, with
verification instructions in SECURITY.md.
Removed
- Dead
examples/tree (referenced the removed connection pool, stale path dep,
never built). Usage lives in the README andtests/entrypoints/.
Notes (deliberate deferrals)
ExecConfig.envis still not applied (0.16 exposes no live-environ accessor to
build a merged env without dropping PATH/HOME — documented in code).- Spin-lock mutexes retained (0.16 has no blocking
std.Thread.Mutex; held briefly). - Cosmetic refactors (re-export chain,
client.clientindirection) deferred —
breaking the public surface for cosmetics.kcovcoverage deferred (immature on Zig).
v0.3.0
Functional-bug, correctness, and SDLC fixes from a full code review. Two more masked
Zig-0.16 build breaks (same class as the 0.2.2 streaming fix) were found and fixed.
Breaking
- Removed the connection-pool API (
ConnectionPool,PoolManager,PoolStats). It
was never wired intoK8sClient, leaked connections, andstd.http.Clientalready
pools internally. (H2)
Fixed
- JSON field binding:
List(T).metadata.continueandServiceSpec/SecretData/
deployment-strategytype(andWatchEvent.type) now bind the real wire names —
Zig'sstd.jsondoes no underscore stripping, so pagination tokens and Service
typewere silently dropped/mis-serialized. (H1) - Retry actually runs: idempotent CRUD (GET/PUT/DELETE/PATCH) is retried per
retry_config; previously every operation used the non-retrying path. POST stays
single-attempt. (H3) - apply/auth/crd JSON serialization didn't compile on 0.16 (used removed
std.json.stringify,ArrayList.writer, and managed map/list APIs) — masked by
lazy compilation. Migrated tostd.json.Stringify.valueAlloc+ unmanaged
collections;StrategicMergePatchnow stores a serializableObjectMap. - Plugged error-path memory leaks in the kubeconfig parse helpers (errdefer per
duped field). AddedClusterInfo.deinit.
Security / hardening
exec_credentiallogs viastd.loginstead of stderr; documented that
ExecConfig.envis not yet applied.proxy_fallbacknow logs the TLS→plaintext
downgrade instead of failing open silently. Documented thatK8sClientis
single-threaded (last_api_erroris unsynchronized).
CI / DevEx
build.zig.zongit-pins zig-yaml (was a relative path) so a clean clone builds;
CI drops the sibling checkout.- CI:
zig fmt --checkgate,{ubuntu, macos}build matrix, non-blocking Zig-master
canary. Release attachesSHA256SUMS. - Added
SECURITY.md,CONTRIBUTING.md,CODEOWNERS, a PR template, a README
Stability section; rewrote the staledocs/TESTING.md; removed dead
websocket_live_test.zig;zig fmtacross the tree. - The migration probe now force-compiles every public method body (
_ = &T.method),
so removed-API breakage fails the build instead of a downstream consumer.