Revision history for Net-Async-Kubernetes

0.009     2026-10-01 03:40:55Z
 - Require IO::K8s 1.109: string-map fields (labels, annotations, ConfigMap
   data, ...) now serialize numeric values as JSON strings, which the API
   server requires.
 - Require `IO::Async::SSL`. `Net::Async::HTTP` and
   `Net::Async::WebSocket::Client` load it for every https request and wss
   session but only recommend it, so a consumer that installed just this
   distribution died with "Can't locate IO/Async/SSL.pm" on its first
   request to a cluster. The minimum, 0.12, is the version `Net::Async::HTTP`
   itself insists on.
 - Require `Kubernetes::REST` 1.109. Failed Futures carry its
   `Kubernetes::REST::APIError`, and `delete`'s `propagationPolicy` and
   `discover` need what 1.109 added.
 - Fix `list()` dropping `labelSelector` and `fieldSelector`. Both went to
   `build_path`, which ignores them, so a filtered list resolved to every
   object of the Kind. They are now sent as query parameters, as in
   `Kubernetes::REST`; a list without selectors sends the same request as
   before.
 - Add `patch_status()` and `update_status()`, the Future counterparts of
   `Kubernetes::REST`'s methods for the /status subresource. Once a resource
   has that subresource, the API server discards status on every write to
   the main endpoint and still answers 2xx. `patch_status` takes the same
   call forms as `patch()` but defaults to a merge patch, since custom
   resources reject a strategic one; `update_status` PUTs the whole object,
   like `update()`.
 - Add `ensure()`, `ensure_all()` and `ensure_only()`, the idempotent
   create-or-update of `Kubernetes::REST` as Future chains. `ensure` creates
   what is missing and updates what exists at its current resourceVersion,
   retrying a conflict once; an existing core v1 PersistentVolumeClaim and a
   running or succeeded batch/v1 Job are left alone, a failed one is
   replaced, and a custom resource reusing either Kind name is updated like
   any other object. An object that appears between the GET and the POST
   (409 AlreadyExists) is handled the same way, so a Job is never PUT onto
   its immutable Pod template. `ensure_all` applies its objects strictly one
   after another. `ensure_only` then deletes every object with the given
   label that it was not handed, matching by Kind, so a kinds entry written
   as group/version/Kind does not delete the objects just applied. A kinds
   entry it cannot list in a namespace, or a stale object it cannot delete,
   is skipped with a warning naming the Kind, namespace, name and reason; a
   404 on either stays silent, and the Future still resolves to the applied
   objects. A hashref's `apiVersion` selects the class, so an
   `autoscaling/v1` HorizontalPodAutoscaler is applied as v1 rather than as
   the v2 the bare Kind maps to; an `apiVersion` no class serves croaks
   before any request, naming the Kind and the version. A hashref resolved
   to a single-segment class of your own (`'+Gizmo'` in the resource_map)
   is built as that class, not as whatever the Kind name `Gizmo` maps to.
   `ensure_only` matches by each object's own API group and `kind`, so
   neither objects that share a class name but not a Kind nor the same Kind
   in two groups (Istio's and the Gateway API's Gateway) shield each other
   from the prune.
 - Fix a bare unknown Kind (`$kube->get('Bogus', ...)`) dying synchronously
   with "Can't locate IO/K8s/Bogus.pm". `Kubernetes::REST` resolves such a
   name to a fabricated `IO::K8s::Bogus` rather than undef, so it slipped
   past the unknown-resource guard. `list`, `get`, `delete`, `patch`,
   `patch_status`, `log`, `port_forward`, `exec`, `attach` and the cp
   helpers now fail their Future with the same "unknown resource" message
   as a qualified name; `expand_class`, `watcher` and `ensure` croak with
   it. A name that resolves to a class which does not load or compile (a
   typo in a `+Class` resource_map entry), or to an IO::K8s helper that is
   no resource class (a bare `List`, `Resource`, `Types` or
   `Unstructured`), no longer dies in `build_path` either: the same methods
   report the load error or "not a Kubernetes resource class" instead of
   "unknown resource". `expand_class` therefore now loads the class it
   returns.
 - `Net::Async::Kubernetes::Controller`'s `patch_status` and `update_status`
   now go through the client's methods of the same name instead of building
   their own requests. Signature, the merge default and failed Futures for
   bad input are unchanged; server-error messages now name the operation as
   `patch_status`/`update_status`. A bare unknown Kind, and the object form
   without `status` for a class that has no `status` attribute, now fail the
   Future instead of dying. The SYNOPSIS showed a call form that never
   worked and is fixed.
 - Fix `Net::Async::Kubernetes::Watcher` retrying a failed watch request
   silently, every second, forever. A request that fails (IO::Async::SSL
   missing, a TLS or connection error, an unreachable API server) or that
   the API server rejects (401, 403, 5xx) is now reported to `on_error` as
   a Status hashref with `reason` `WatchFailed`, the HTTP status in `code`
   (0 when no response arrived), the cause in `message` (a rejection as
   `HTTP 403 Forbidden: <message of the answer>`) and the next delay in
   `details.retryAfterSeconds`; without an `on_error` it is warned
   about. So is a watch stream that ends badly, which used to reconnect at
   once, in a tight loop against the API server: one that closes within
   the new `min_watch_duration` (1s, client-go's threshold for the same
   case) without delivering an event, or right after an ERROR event other
   than 410 Gone (`code` is then that event's). Reconnects back off
   exponentially from `reconnect_delay` (1s) to `max_reconnect_delay`
   (30s) and start over once an event arrives or a watch cycle runs its
   course; an ERROR event does not count. Each delay is shortened at
   random by up to the new `reconnect_jitter` (default 0.2, 0 for exact
   delays), so watchers that fail together do not reconnect together;
   `retryAfterSeconds` is the delay actually waited. The new `max_retries`
   (default: unlimited) stops the watcher after that many consecutive
   failed retries and says so. `stop()` also cancels a pending reconnect,
   and `start()` during one no longer opens a second watch. A controller's
   `on_watch_error` receives these reports as well. ERROR events are still
   dispatched as before, and 410 Gone still reconnects at once without a
   resourceVersion.
 - Fix the streaming transport behind `log()` with `on_line` and behind the
   watcher failing its Future at the end of every stream ("Can't call
   method "code""): a streamed log failed after its last line, and a watch
   cycle ending at its server-side timeout was taken for a connection
   error and reconnected a second late. An error response is no longer
   streamed either: its body reached `on_line` as a log line (and the
   watcher as a bogus event) and now ends up in the failure message.
 - Fix the object forms dying synchronously on an object that is no
   resource. `create`, `update`, `update_status`, `patch`, `patch_status`,
   `delete` and `ensure` handed the object's class straight to
   `build_path`, so an `IO::K8s::List`, a nested type such as a `PodSpec`,
   or a value that is no IO::K8s object at all died there, or in the
   metadata lookup before it, with an unrelated message. Each now reports
   "not a Kubernetes resource class" (or "requires an IO::K8s object") the
   way it reports its other bad arguments, before any request is sent: a
   failed Future from `create`, `delete`, `patch` and `patch_status`, a
   croak from `update`, `update_status` and `ensure`. The controller's
   `update_status` fails its Future for such an object instead of dying.
 - Fix every request for a custom resource that resolves to
   `IO::K8s::Unstructured` dying synchronously with "IO::K8s::Unstructured
   needs a Kind to build a path". With `resource_map_from_cluster`, a Kind
   the cluster's discovery lists but no class serves resolves to
   `IO::K8s::Unstructured`; its Kind and apiVersion now reach `build_path`
   wherever the client builds a path - `list`, `get`, `create`, `update`,
   `update_status`, `patch`, `patch_status`, `delete`, `ensure`,
   `ensure_only`, `log`, `port_forward`, `exec`, `attach` and the watcher -
   taken from the name (a qualified `group/version/Kind` stays in its own
   group and version) or from the `IO::K8s::Unstructured` object. `ensure`
   and `ensure_only` apply their Job, PersistentVolumeClaim and prune rules
   to such objects by their `kind` and `apiVersion`.
 - Fix answers inflating as the wrong class when a resource resolves to a
   single-segment class of your own (`'+Gizmo'` in the resource_map). With
   `Kubernetes::REST` 1.108, `list`, `get`, `create`, `update`,
   `update_status`, `patch`, `patch_status`, `ensure`, `ensure_only` and the
   watcher handed the resolved name `Gizmo` back to it, which read it as the
   Kind `Gizmo`: the answer died, list items were dropped, or everything
   inflated as whatever class the resource map gives that Kind - and
   `ensure_only` took its own objects for strangers and deleted them in that
   other group. The client now hands over the resolved class exactly,
   whichever Kubernetes::REST version is installed.
 - Fix requests for `IO::K8s::Unstructured` that have no path dying
   synchronously in Future-returning methods: without
   `resource_map_from_cluster`, for the explicit class name
   `IO::K8s::Unstructured`, and for an Unstructured object without `kind`,
   `build_path` croaked straight out of the call. `list`, `get`, `create`,
   `patch`, `patch_status`, `delete`, `log`, `port_forward`, `exec`,
   `attach` and the cp helpers now fail their Future with the reason;
   `update`, `update_status`, `ensure` and the watcher croak with it, as
   with their other bad arguments, now from the caller's line.
 - Fix a qualified `group/version/Kind` name, or an `IO::K8s::Unstructured`
   object or manifest whose `apiVersion` the cluster does not serve,
   reaching another group that serves a Kind of the same name. With
   `resource_map_from_cluster` and `Kubernetes::REST` 1.108,
   `example.org/v1/Widget` resolved through discovery to whichever group
   served a `Widget`, and `list`, `get`, `patch`, `delete`, `log`, the
   duplex methods, `ensure`, `ensure_only`'s prune and the watcher went
   there. The name is now an unknown resource and the object or manifest is
   refused before any request, as with Kubernetes::REST 1.109: a failed
   Future, a croak from `expand_class`, `update`, `update_status`, `ensure`
   and the watcher, a warning from `ensure_only`.
 - Fix a reference in place of a resource name - typically a manifest
   hashref handed to `patch` or `patch_status` - failing with "resource
   'HASH(0x...)' resolves to class IO::K8s::HASH(0x...), which cannot be
   loaded". Every method that takes a resource name now refuses it as
   "resource name must be a string, got a HASH reference", the way it
   reports its other bad arguments.
 - Add `propagationPolicy` to `delete()`, in every call form
   (`delete($object, propagationPolicy => 'Background')`, `delete('Job',
   'nightly', namespace => 'ns', propagationPolicy => 'Foreground')`,
   `delete('Job', name => 'nightly', ...)`), sent as a query parameter:
   `Background` and `Foreground` delete what the object owns, `Orphan` keeps
   it; without it the API server's default applies, which for a Job orphans
   its Pods. Any other value, and any option `delete` does not know, fails
   the Future before a request is sent, so a misspelt option no longer goes
   unnoticed; the messages are worded as in `Kubernetes::REST`
   (`Unknown propagationPolicy 'x' for delete() (use: Background,
   Foreground, Orphan)`, `Unknown argument 'x' to delete() (allowed: ...)`).
   The object form, which ignored every extra argument, now takes only
   `propagationPolicy`. `ensure_only` prunes with `Background` unless its
   new `propagationPolicy` option says otherwise (an unknown value croaks,
   worded the same way), and `ensure` deletes a failed Job with
   `Background` before recreating it.
 - Fail Futures for a refusal of the API server (status 400 and up) the way
   Future's convention has it: `->fail($error, 'http', $response)`. `$error`
   is exactly what `Kubernetes::REST`'s `check_response` throws, a
   `Kubernetes::REST::APIError` that stringifies to the message as before,
   and `$response` is the `Kubernetes::REST::HTTPResponse`, so a 404 or a
   409 can be told apart by `$response->status` instead of by parsing the
   message, e.g. with `->catch(http => sub { ... })`. This holds for every
   request method, a streamed `log()`, `ensure` and `ensure_all`; the
   watcher still reports to `on_error`.
 - Fix the constructor swallowing why a `context` given without `kubeconfig`
   could not be used: a context missing from the default kubeconfig, or no
   kubeconfig at all outside a cluster, was ignored and surfaced as "server
   or kubeconfig required" on the first request. The constructor now croaks
   with the reason (`Context not found: ...`), as it does for an explicit
   `kubeconfig`; without a context, a failed auto-detection stays silent as
   before.
 - Add the `with` constructor option: IO::K8s resource-map providers (CRD
   bundles such as `IO::K8s::GatewayAPI`), passed on to `Kubernetes::REST`,
   so their Kinds resolve to typed classes in names, lists, watches and
   `new_object`.
 - Add `discover()`, which reads the cluster's discovery documents
   (`GET /api`, `GET /apis`) through the client's own asynchronous
   transport and hands them to `Kubernetes::REST`, so names and the
   resource map resolve without a request of its own. With
   `resource_map_from_cluster`, `Kubernetes::REST` otherwise reads them on
   first use through its synchronous HTTP backend, blocking the loop; await
   `discover` once at start-up to avoid that. An error status fails the
   Future with category `http` and the response; legacy discovery
   (Kubernetes before 1.27) is left to the synchronous read on first use;
   without `resource_map_from_cluster` nothing is sent.
 - Refuse an option `list`, `get`, `log`, `patch`, `patch_status`,
   `port_forward`, `exec`, `attach`, `cp_to_pod`, `cp_from_pod`,
   `ensure_only` or `Net::Async::Kubernetes::Controller`'s `patch_status`
   does not take, instead of dropping it silently: a misspelt
   `labelselector` listed every object, `tail_lines` fetched the whole log,
   `namespace` for `namespaces` made `ensure_only` prune at cluster scope
   only, a misspelt `container` ran an exec in the pod's default container,
   a stray `subresource` landed in the exec, attach or port-forward path,
   and the controller sent a merge patch for `typ => 'json'`. The message
   names the first such option and those the method takes, as
   `Kubernetes::REST` words it (`Unknown argument 'tail_lines' to log()
   (allowed: ...)`); `ensure_only` croaks, the others fail their Future,
   before any request. `list` takes only `namespace`, `labelSelector` and
   `fieldSelector`, the object forms of `patch` and `patch_status` only
   `patch` and `type`, and that of the controller's `patch_status` only
   `status` and `type`.
 - Refuse an odd list of options in `get`, `list`, `patch`, `patch_status`,
   `ensure_only`, `watcher`, `controller` and
   `Net::Async::Kubernetes::Controller`'s `patch_status` and
   `watch_resource`, as `delete` and the keyed class forms already did.
   Perl only warned "Odd number of elements in hash assignment" and the
   call went on with the stray key's value undef: `get('Pod', 'web',
   namespace => 'default', 'namespace')` fetched the Pod at cluster scope,
   a trailing `'type'` patched with the default type, a lone `'status'`
   after an object made the controller patch the object's own status,
   `list('Pod', 'namespace')` listed every namespace, a trailing
   `'objects'` left `ensure_only` pruning everything carrying its label,
   and a trailing `'namespace'` made a watch cover the whole cluster. The
   message is `Invalid arguments to METHOD()`, before any request;
   `ensure_only`, `watcher`, `controller` and `watch_resource` croak, the
   others fail their Future. A lone argument after the Kind is still the
   name in `get`, even one spelled like an option.
 - Refuse a reference in the name position of `get()` and `delete()`.
   `get('Pod', $obj)` and `delete('Pod', {name => 'web'})` took the reference
   as the resource name and stringified it straight into the request path
   (`GET /api/v1/pods/IO::K8s::...=HASH(0x...)`), so the mistake surfaced only
   as the server's 404. Both now fail the Future before any request with
   `resource name must be a string, got a HASH reference` (a blessed object:
   `got an object of class ...`), the same wording already used for a
   reference in the class-name position (`patch($manifest, ...)`); the other
   name-taking methods (`log`, `patch`, the duplex methods) already rejected
   such input before a request. A string name, the object forms, and every
   other call are unchanged.

0.008     2026-09-22 04:13:56Z
 - Add public `rest` and `new_object($kind, %fields)` on the client: the
   underlying `Kubernetes::REST` instance and a typed `IO::K8s` object for
   `create`/`update`, so callers no longer reach through the private `_rest`.
 - Require `IO::K8s` 1.108 and `Kubernetes::REST` 1.108. Older IO::K8s
   resolved a bare Kind such as `Role`, `Event`, `Job` or `Service` to any
   loadable top-level package of that name -- `Class::Mite` ships a
   `lib/Role.pm` -- so creating the object died with `Can't locate object
   method "_k8s_attr_info"` on smokers carrying such a module (GitHub issue
   #2). The pair also brings UTF-8 request bodies (a manifest with a
   non-ASCII character died with "HTTP::Message content must be bytes"), the
   right API group paths for Storagemigration and Apiserverinternal, working
   inflation of KubeAggregator's `APIService`, and the upstream Kubernetes
   v1.37 sync with 11 new Kinds, all reached through the `Kubernetes::REST`
   seam with no change here.
 - Accept a qualified `group/version/Kind` resource name anywhere the client
   takes one: `list('autoscaling/v1/HorizontalPodAutoscaler')` addresses that
   version, while the bare Kind keeps the shipped default of v2.
 - Move `Net::Async::Kubernetes::PortForwardSession` into its own file. It was
   declared inline in `Net::Async::Kubernetes`, so `use`ing the class the POD
   of `port_forward()`, `exec()` and `attach()` names failed, and every
   `dzil build` warned about it. The code is unchanged.
 - Add `on_watch_error` to `Net::Async::Kubernetes::Controller`. Watch `ERROR`
   events (a `403` mid-stream, for example) were wired nowhere; they carry a
   `Status` hashref rather than a keyed object, so they go to the callback
   instead of the reconcile workqueue. An `on_error` passed to
   `watch_resource` keeps precedence for that watch.
 - Fix two reference cycles that leaked a controller and its client. The
   controller held a strong `kube` while the client held the controller as a
   child, and every reconcile context held a strong `controller` inside an
   entry the controller owns. Both are now weak, as in `Watcher`; a client
   the controller constructs itself stays owned by it.
 - Fix the controller keeping one workqueue entry per key for the lifetime of
   the process, each pinning the last object it saw. A key is dropped once it
   reconciles cleanly; keys still queued, dirty or waiting on a retry keep
   their entry, so backoff history survives.
 - Fix a controller key stalling after a stop/start cycle. `stop()` left the
   workqueue filled and its keys flagged queued or dirty, so the event a
   restarted watch delivered was deduplicated away with no drain scheduled,
   and a dirty key finishing its reconcile requeued past the stop. `stop()`
   now clears queue and flags together and the requeue skips while stopped;
   the failure counts stay, so a retrying key resumes at its next attempt.
 - Fix `stop()` leaving a stopped watcher attached to the client on every
   stop/start cycle, so a repeatedly restarted controller piled up notifiers.
 - Fix the duplex transport naming `port_forward` in its "not added to an
   IO::Async::Loop" failure whichever method was called; `exec()` and
   `attach()` now name themselves.
 - Fix a resource name that `IO::K8s` cannot resolve killing the caller with
   "argument is not a module name" from `build_path`. `list()`, `get()`,
   `patch()`, `delete()`, `log()`, `port_forward()`, `exec()`, `attach()` and
   the controller's `patch_status()` now return a failed `Future` naming the
   resource; `$kube->expand_class` and starting a watcher croak with the same
   message.
 - Stop `t/03-configure.t` reading the kubeconfig of the machine it runs on.
   One CPAN Testers smoker carried a kubeconfig with `contexts: null`, so
   every 0.005 report from it was a FAIL nobody else saw. The subtest now
   runs against a fixture in a temporary HOME, and a companion subtest holds
   the degenerate config to a catchable croak. Reported by Andreas Koenig,
   GitHub issue #1.
 - Load `Net::Async::Kubernetes::Watcher` and `::Controller` at compile time
   instead of `require`ing them in `watcher()` and `controller()`: a broken
   child module now surfaces on `use Net::Async::Kubernetes`.
 - Move the example scripts from `ex/` to `eg/`; the 0.007 entry's
   `ex/live_features.pl` ships as `eg/live_features.pl`.
 - Documentation pass: PodWeaver generates the `NAME` sections,
   `PortForwardSession` is documented and in `SEE ALSO`, `list()` and the `cp`
   helpers describe what they do, the qualified resource name and the
   status-subresource write-strip trap are written down, and the `Controller`
   methods state the `Future` they return.

0.007     2026-04-15 18:21:50Z
 - Add `Net::Async::Kubernetes::Controller` as a minimal controller runtime.
   Provides watch registration, keyed workqueue deduplication, serialized
   reconcile dispatch, retry hooks, and small helpers for object reload and
   status subresource patch/update.
 - Add `$kube->controller(...)` factory to bind controller runtimes directly
   to an existing async Kubernetes client.
 - Fix async TLS handling for kubeconfigs with inline CA/client PEM data by
   materializing PEM content to temporary files before passing SSL options to
   Net::Async::HTTP / IO::Socket::SSL. This unblocks mTLS-authenticated async
   clients and controller runtimes against real clusters using embedded certs.
 - Add active `exec()` API that builds Kubernetes pod exec requests
   (`/exec` subresource) and delegates to the websocket duplex transport.
   Supports command, container, stdin/stdout/stderr/tty toggles, and
   subprotocol override.
 - Add active `attach()` API that builds Kubernetes pod attach requests
   (`/attach` subresource) and delegates to the websocket duplex transport.
   Supports container, stdin/stdout/stderr/tty toggles, and subprotocol
   override.
 - Extend `Net::Async::Kubernetes::PortForwardSession` with `write_stdin`
   (writes to channel 0) and `resize` (writes TTY resize JSON to channel 4)
   helpers for exec/attach sessions.
 - Add async `cp_to_pod()` and `cp_from_pod()` helpers that stream tar
   archives through an exec session for file transfer.
 - Add mock test coverage for exec (`t/14-mock-exec.t`), attach
   (`t/15-mock-attach.t`), and cp (`t/16-mock-cp.t`) flows.

0.006     2026-03-09 08:37:54Z
 - Add async Pod Log API via `log()`.
   Supports one-shot mode (Future resolves to full text) and streaming mode
   (`on_line` callback with Kubernetes::REST::LogEvent objects).
   Streaming mode supports follow, tailLines, sinceSeconds, sinceTime,
   timestamps, previous, limitBytes, and container parameters.
 - Add mock log test coverage (`t/09-mock-log.t`) and extend mock transport
   with generic streaming chunk support for log streams.
 - Add active `port_forward()` API that builds Kubernetes port-forward requests
   and delegates to duplex transport (`_do_duplex_request`).
 - Implement default websocket duplex transport for `port_forward()` using
   Net::Async::WebSocket::Client, including channel frame decoding and a
   `Net::Async::Kubernetes::PortForwardSession` helper (`write_channel`,
   `close`).
 - Add duplex transport tests (`t/13-duplex-transport.t`) for websocket
   handshake wiring, frame handling, session writes/closes, and connect errors.
 - Add active `exec()` API that builds Kubernetes pod `/exec` requests with
   repeated `command=` parameters and stream toggles
   (stdin/stdout/stderr/tty).
 - Add async exec test coverage (`t/14-mock-exec.t`) and duplex transport
   assertions for exec in `t/13-duplex-transport.t`.
 - Add active `attach()` API that builds Kubernetes pod `/attach` requests with
   stream toggles (stdin/stdout/stderr/tty).
 - Add async attach test coverage (`t/15-mock-attach.t`) and duplex transport
   assertions for attach in `t/13-duplex-transport.t`.
 - Extend `Net::Async::Kubernetes::PortForwardSession` with convenience
   helpers `write_stdin()` (channel 0) and `resize()` (channel 4 terminal
   resize payload for exec/attach).
 - Add async cp helpers `cp_to_pod()` and `cp_from_pod()` built on `exec()`.
   Supports single-file upload/download via channel streaming with Future-based
   completion and status/error propagation.
 - Add cp helper mock tests in `t/16-mock-cp.t`.
 - Add live demo script `ex/live_features.pl` to exercise log, exec, attach,
   port-forward, and cp helpers against a real cluster/kubeconfig.
 - Raise minimum versions to Kubernetes::REST 1.101 and IO::K8s 1.008.

0.005     2026-03-04 17:14:31Z
 - Use public Kubernetes::REST building-block API (build_path,
   prepare_request, check_response, inflate_object, inflate_list,
   process_watch_chunk) instead of private underscore methods.
   Requires Kubernetes::REST >= 1.100.

0.004     2026-02-28 01:39:32Z

0.003     2026-02-28 01:11:24Z
 - Auto-detect kubeconfig and in-cluster service account via
   Kubernetes::REST::Kubeconfig (requires Kubernetes::REST >= 1.004).
   When no explicit server/kubeconfig is provided, construction silently
   tries default kubeconfig and in-cluster auth before falling back to
   lazy resolution.

0.003     2026-02-28 00:00:00Z

0.002     2026-02-18 22:52:19Z
 - Updating minimum requirements

0.001     2026-02-13 06:03:10Z
 - Initial release
 - Async Kubernetes client built on IO::Async and Kubernetes::REST
 - Future-based CRUD: list(), get(), create(), update(), patch(), delete()
 - Three patch types: strategic-merge (default), merge, json
 - Delete by class+name or by object reference
 - Net::Async::Kubernetes::Watcher for streaming watch with auto-reconnect
 - Separate on_added, on_modified, on_deleted, on_error callbacks
 - Catch-all on_event callback for raw WatchEvent access
 - Client-side event filtering: names (regex/string/array) and event_types
 - Smart event type auto-derivation from registered callbacks
 - Resumable watches via resourceVersion tracking
 - Automatic 410 Gone recovery (clear resourceVersion and restart)
 - Auto-reconnect on stream completion (server timeout) and connection errors
 - Kubeconfig support via Kubernetes::REST::Kubeconfig
 - SSL/TLS with client certificate support
 - Custom resource_map for CRD support
 - Pass Content-Type correctly to Net::Async::HTTP for all request bodies
 - Defer watcher stop() HTTP close to avoid Net::Async::HTTP spurious read errors
 - Comprehensive inline POD documentation for all public methods and attributes
