Revision history for Kubernetes-REST 1.109 2026-10-01 03:32:13Z - Require IO::K8s 1.109: string-map fields (labels, annotations, ConfigMap data, ...) now serialize numeric values as JSON strings, which the API server requires. - ensure and ensure_only resolve a hashref manifest through its apiVersion: an autoscaling/v1 HorizontalPodAutoscaler is no longer sent as autoscaling/v2. An apiVersion no known class serves croaks, naming the Kind and apiVersion, instead of falling back to the Kind's default version. - ensure special-cases only the built-in core v1 PersistentVolumeClaim and batch/v1 Job, matched by apiVersion and Kind; a custom resource reusing either Kind name is updated like any other object. An object that appears between ensure's GET and POST (409 AlreadyExists) is handled like one that existed, so a Job is kept or recreated instead of PUT onto its immutable Pod template. - ensure tells a missing object (404), AlreadyExists and Conflict (409) apart by the response status, and ensure_crd ends its wait for Established with the API error on any failed poll but a 404. An error whose text merely contains "404" or "409" no longer makes ensure create, re-fetch or retry, nor ensure_crd poll on to its timeout. - ensure_only no longer deletes the objects it has just applied when a kinds entry is qualified ('autoscaling/v1/HorizontalPodAutoscaler') or resolves to IO::K8s::Unstructured. Listed items match on their own API group, Kind, namespace and name, so a labelled resource of the same Kind in another group (Istio vs Gateway API Gateway) is deleted too. - ensure_only warns when it cannot list a kinds entry in a namespace or delete a stale object, naming Kind, namespace, name and reason, instead of skipping silently; a 404 stays silent and the return value is unchanged. - delete takes propagationPolicy => 'Background', 'Foreground' or 'Orphan', sent as a query parameter, and croaks on any other argument instead of ignoring it, so a misspelt option no longer silently orphans a Job's Pods. ensure_only prunes with Background unless given another propagationPolicy; ensure deletes the failed Job it recreates with Background. The v0 Delete* methods pass propagationPolicy on and still ignore their other parameters. - ensure_only, log, list, get, watch, patch, patch_status, ensure_crd, port_forward, exec and attach croak on an argument they do not take, naming it and the ones they do, before anything is applied or sent: a misspelt propagation_policy, namespace, tail_lines, label_selector or containr no longer silently changes what the call does. get takes subresource, e.g. 'status'. name on list and watch croaks - select one object with fieldSelector => 'metadata.name=NAME'. With an object, patch and patch_status take only patch and type; ensure croaks on anything after its object. The v0 List*, Read*, Watch* and Patch* methods pass on only what the new methods take and ignore their other parameters. - A qualified name ('example.org/v1/Widget', or a Kind plus apiVersion) resolved to IO::K8s::Unstructured addresses exactly its own group and version in list, get, delete, patch and ensure_only. One the cluster does not serve stays unresolved - build_path croaks naming the apiVersion - instead of going to another group or version serving the Kind. - list, get, patch, patch_status, delete, watch, log, port_forward, exec, attach and compare_schema croak "unknown resource 'NAME'" for a name nothing resolves - an unknown bare Kind, or a qualified name whose group/version the cluster does not serve - naming a failed discovery with its reason, instead of "argument is not a module name" or an @INC dump. ensure and ensure_only do the same for a manifest without apiVersion whose Kind nothing resolves. No request is sent, as before. - Objects of a single-segment class registered as '+Gizmo' inflate as that class - in ensure, CRUD, watch and inflate_object/inflate_list/ process_watch_chunk - instead of dying on IO/K8s/Gizmo.pm, dropping list items or becoming the class the Kind Gizmo is mapped to. - prepare_request percent-encodes query keys and values where the API server would otherwise split or change them - &, +, #, %, ;, a space, a control character, non-ASCII (sent as UTF-8) - so such a fieldSelector arrives intact. Everything else, '=', ',', '!', '/', '(', ')' and ':' included, is sent as written; typical label and field selectors render byte for byte as before. - schema_for finds the definition of a class whose name does not map onto one - apiextensions, apiregistration, a '+My::Class' or `with` provider CRD class, IO::K8s::Unstructured - by its x-kubernetes-group-version-kind instead of answering undef, so compare_schema works for the typed ones too; a name resolving to no class answers undef without warnings. compare_schema on IO::K8s::Unstructured, which has no local schema, croaks saying so before fetching /openapi/v2. - An HTTP error status dies with a Kubernetes::REST::APIError instead of a plain string - from every call, check_response, absorb_discovery and the /openapi/v2 fetch behind schema_for and compare_schema - carrying code, reason, message and details of the Status body, the decoded body, context and response, plus is_not_found and is_conflict. It stringifies to the old message, caller line included, so printing or matching $@ is unaffected; code that tests ref($@) sees an object now. Two messages change: "Could not fetch OpenAPI spec: 503" becomes "Kubernetes API error (fetch OpenAPI spec): 503 ", and "discovery GET /api failed: 401" becomes "Kubernetes API error (discovery GET /api): 401 ", also inside the built-in resource map fallback warning and the croaks of fetch_resource_map and build_path. - New prepare_discovery_requests and absorb_discovery let an async client read discovery (GET /api, GET /apis) through its own event loop and hand the responses over; with aggregated discovery, name resolution and the resource map then send no request of their own. Legacy discovery answers false and is read synchronously on first use, as before. - Legacy discovery (clusters before Kubernetes 1.27) warns when a group/version answers with an HTTP error, naming its apiVersion and the error, instead of silently leaving its Kinds out of the resource map; a 404 stays silent and the other groups are still read. - The client keeps its own copy of a resource_map passed to it, and invalidate_discovery - with it ensure_crd - keeps that map instead of rebuilding it, so its '+My::Class' entries, the Kind ensure_crd has just installed included, still resolve afterwards. `with` providers are merged into that copy, no longer into the caller's hash or, after a failed cluster fetch, into IO::K8s's process-wide built-in map. A change to the passed hash after construction is not seen; change $api->resource_map instead. - The v0 status methods use the /status subresource: Read*Status (ReadNamespacedPodStatus, ReadNodeStatus, ...) read it and their deprecation warning names get(..., subresource => 'status'), and Replace*Status and Patch*Status write through update_status and patch_status. They wrote the main endpoint, where the API server drops the status stanza and still answers 2xx, so the status was lost. - The v0 layer resolves a Kind whose own name ends in Status, such as ComponentStatus, as one whole Kind: ListComponentStatus and ReadComponentStatus reach the componentstatuses endpoint instead of dying while loading a non-existent Component class. - The built-in resource map fallback warning, and an error from a call through the v0 compatibility layer (an APIError or a croak raised in list/get/update/... behind it), name the caller's line instead of one inside Kubernetes/REST.pm or V0Group.pm. 1.108 2026-09-22 02:22:33Z - New ensure_crd(@classes): create-or-update CustomResourceDefinitions from typed IO::K8s classes, wait until Established, then invalidate_discovery. Several versions of one CRD need storage => ''. - A Kind the cluster serves but no class resolves now inflates to IO::K8s::Unstructured instead of failing closed; shipped, `with` and AutoGen classes still win. build_path gains additive kind, api_version, resource and namespaced arguments for such paths. - The cluster resource map no longer invents IO::K8s::Api:: class names for CRD groups without a shipped class; those resolve through the inner IO::K8s (`with` providers, then AutoGen) or fail closed. - A short name served in several versions resolves to the version the cluster marks preferred, not to a fixed stable-over-beta ranking. - New constructor attribute `with`: external IO::K8s resource-map providers (CRD bundles) passed through to the inner IO::K8s, e.g. with => ['IO::K8s::GatewayAPI']. - The cluster resource map comes from aggregated discovery (/api, /apis) instead of /openapi/v2, now fetched lazily for schema_for/compare_schema only; a failed discovery fetch warns and uses the built-in map. - New invalidate_discovery: drop the cached discovery catalog and resource map so a freshly installed CRD resolves without a new client. - New CLI tool kube_crd2class: renders a CustomResourceDefinition (from a file with -f, or from the cluster with --from_cluster ) as typed IO::K8s class source, to STDOUT or a tree with -d. - Fix kube_watch taking the first option as the Kind when options precede it ("kube_watch -n default Pod" watched Kind "-n"). - New CLI tool kube_test_minikube: fetches minikube (SHA-256 verified), runs a profile with its own kubeconfig, exports the live-test variables and runs the given command, e.g. "kube_test_minikube prove -lr t/". - Require IO::K8s 1.108 (Kubernetes v1.37 sync). - inflate_list() warns once per list naming every item it drops instead of discarding uninflatable items silently. - expand_class() is a client method that resolves names the built-in map knows without fetching the cluster resource map. - Kubeconfig context()/cluster()/user() croak with a message naming what is missing (no entries, no current-context, name not found) instead of an uninitialized-value warning and an empty "Context not found: ". 1.107 2026-08-15 16:15:27Z - New patch_status() and update_status(), writing through the /status subresource. A resource with a status subresource -- every CRD carrying a "status" entry under "subresources", and the built-in kinds that always had one -- has its status stripped from every write to the main endpoint by the API server, which then answers 2xx anyway. create(), update(), patch() and server-side apply all looked like they worked and stored nothing. patch_status() takes the same arguments as patch() (object or class plus name, both call forms, the same patch types) but defaults to merge patch rather than strategic, because custom resources answer 415 to a strategic merge patch. update_status() is the PUT counterpart of update(). - build_path() -- the published path-building seam used by Net::Async::Kubernetes -- takes an optional subresource argument that appends /status, /log, /exec, /attach or /portforward to the resource path. This is additive: existing calls keep their signature and return exactly the paths they did before. A subresource without a name now croaks instead of silently producing a path pointing at the collection endpoint. log(), exec(), attach() and port_forward() build their paths through this argument now rather than concatenating their own suffix; the URLs they request are unchanged. - Fix Kubernetes::REST::Core->new(...) -- and its 16 sibling v0 group classes -- dying with 'Can't call method "_v0_group" on an undefined value'. Each v0 group accessor in Kubernetes::REST shares its name with the class it wraps, so once Kubernetes::REST is loaded Perl resolves the bareword against the sub and calls it as Kubernetes::REST::Core()->new(), without an invocant. The accessors now hand back the class name in that case, putting the ->new(...) back on the class the caller was aiming at. Reaching a group through $api->Core is unaffected and behaves as before. - Reword the v0 compatibility modules so their ABSTRACT says what they do instead of shouting their status. Kubernetes::REST::Core and its 16 siblings now read "Compatibility helper for deprecated v0 Core calls" rather than "DEPRECATED - v0 API group for Core resources", and the same goes for Kubernetes::REST::Error and ::RemoteError. These modules are working translation layers onto the v1 API, not dead weight -- the ones that really were dead left in 1.105. Behaviour is unchanged: every v0 call still warns unless HIDE_KUBERNETES_REST_V0_API_WARNING is set. - Fix the v0 groups Apiextensions and Apiregistration dying with "Can't locate IO/K8s/Api/Apiextensions/V1/CustomResourceDefinition.pm in @INC" instead of reaching the cluster. V0Group named every class IO::K8s::Api::::::, but those two groups keep their IO::K8s classes in the upstream staging namespaces they are generated from -- ApiextensionsApiserver::Pkg::Apis::Apiextensions:: and KubeAggregator::Pkg::Apis::Apiregistration::. fetch_resource_map already special-cased both; that knowledge is now one table shared by the two callers rather than a copy in each, so they cannot drift apart again. The other 15 v0 groups resolve to exactly the class names they did before, and fetch_resource_map returns exactly the map it did before: the exception is still matched on the full group name, so a custom resource served under a group like apiextensions.example.com keeps mapping to Api::Apiextensions:: the way any other group does. - kube_client create reads YAML manifests, not only JSON. YAML is the usual format for Kubernetes manifests and the documentation claimed support for it, but the file went straight into inflate(), which is a JSON decode -- so every "kube_client create -f deployment.yaml" died on the first line of the file. Manifests are parsed by IO::K8s load_yaml() now. The format is detected from the content rather than the file name, so "-f -" behaves exactly like a named file: a manifest starting with "{" after optional whitespace still takes the JSON path it always took, everything else is read as YAML. - kube_client create accepts multi-document YAML, the common shape of a Kubernetes manifest. Every "---"-separated document is created in the order it appears in the file -- so a manifest listing a Namespace before the objects inside it works -- and each created object is printed in the --output format. If one document fails, the error names which one; the documents before it have already been created. A manifest with no documents at all, or an empty file, is now an error naming the source instead of an obscure decode failure. - Fix the usage line of "kube_client raw" and five examples in kube_client's own documentation naming the v0 groups with a version suffix, "kube_client raw CoreV1 ListNamespace". The groups are called Core, Apps, Batch and so on, without a version, so anyone following the example got an error. - Fix kube_client and kube_watch ignoring the KUBECONFIG environment variable. Their shared --kubeconfig option defaulted to "$ENV{HOME}/.kube/config" and passed that value on unconditionally, so Kubernetes::REST::Kubeconfig never reached its own default and anyone selecting a cluster through KUBECONFIG -- the normal way to juggle several -- was silently pointed at the home one. The option now has no default of its own and is only passed on when given, making the precedence --kubeconfig, then KUBECONFIG, then ~/.kube/config, which is what kubectl does and what the library already documented. Note that KUBECONFIG is read as a single path here, not as the ":"-separated list of files kubectl merges. Scripts that relied on --kubeconfig always being set are unaffected; the option is only undef when it was not given. - Kubernetes::REST delegates four more IO::K8s methods to its k8s attribute: load_yaml, load, object_to_json and object_to_struct. The first two mean a YAML or .pk8s manifest can be read with $api->load_yaml($yaml) instead of reaching through $api->k8s->load_yaml($yaml), and the last two complete the pairs whose inflating halves, json_to_object and struct_to_object, were delegated already. This is purely additive: reaching through k8s keeps working and is the same call. Mind that load_yaml parses characters while inflate takes UTF-8 bytes, and that a .pk8s manifest handed to load is Perl code that gets evaluated in-process. IO::K8s' add() is deliberately not delegated, because it mutates a resource map this client mirrors in its own resource_map attribute. - KUBECONFIG is read as the list of files kubectl merges, not as a single path. "KUBECONFIG=/a/config:/b/config" went into one open() and died with "Cannot open /a/config:/b/config: No such file or directory" -- a hard failure on a setting that works everywhere else in the ecosystem, and the normal shape of the variable for anyone layering a per-project kubeconfig over a base one with direnv. Kubernetes::REST::Kubeconfig now splits kubeconfig_path on the platform path separator (":" everywhere but Win32, where it is ";") and merges the files the way kubectl does: clusters, contexts and users are unioned by name and the first file defining a name wins, whole entry at a time rather than field by field, and current-context comes from the first file that sets one. Entries naming a file that is not there are skipped silently, as are empty entries, so "/a::/b" and a trailing ":" are harmless; relative entries resolve against the working directory. kubeconfig_path keeps its shape -- a single path behaves exactly as it did, is still returned unsplit, and now also accepts an arrayref of paths -- and the new read-only kubeconfig_paths attribute is the split list. --kubeconfig on kube_client and kube_watch takes a list too, since it is passed straight through. - Fix Kubernetes::REST::Kubeconfig warning "Use of uninitialized value $ENV{HOME} in concatenation" and then looking for "/.kube/config" when HOME is unset -- the case in cron jobs, some systemd units and minimal containers, where the resulting "Cannot open /.kube/config" named everything except the real problem. With neither KUBECONFIG nor HOME set there is nothing to guess, so kubeconfig_path defaults to undef and kubeconfig_paths is empty. api() then goes straight to the in-cluster service account, which makes a pod without HOME but with a mounted token work rather than fail; when there is no token either, and for the other methods, the error is "Kubeconfig not found: neither KUBECONFIG nor HOME is set". An empty KUBECONFIG counts as unset, as it does for kubectl. contexts(), current_context_name(), context(), cluster() and user() croak with that message instead of dying on an undefined hashref whenever no kubeconfig could be read. - Relative file references inside a kubeconfig -- certificate-authority on a cluster, client-certificate and client-key on a user -- are resolved against the directory of the kubeconfig file that defines the entry, the way kubectl resolves them, instead of against the process working directory. This changes what an existing kubeconfig means, not just when it works: a kubeconfig saying "certificate-authority: ca.crt" used to find the file only when the program was started from the directory holding it, and now always finds the one sitting next to the kubeconfig. A setup that relies on the old reading -- a cert deliberately placed in the working directory rather than beside the config -- stops finding it, and fails with a TLS error naming the CA file it now looks for; a kubeconfig checked in next to its certs starts working from any directory instead of exactly one. It matters most for a merged KUBECONFIG list, where the directory belongs to the entry rather than to the configuration: a cluster that won from /a/config resolves its CA against /a even when the current context came from /b/config. Resolution happens as each file is read, before the merge, so cluster(), user() and api() hand back absolute paths that a later chdir cannot invalidate. Absolute references, the inline base64 of the *-data fields, and an exec plugin's command -- a PATH lookup, not a file reference -- are untouched, and a leading "~" is not expanded, no more than kubectl expands it. - A kubeconfig user's tokenFile is read and used as the bearer token. The field was ignored: such a user matched neither "token" nor "exec", fell into the client-certificate branch and was given an empty bearer token, so a kubeconfig pointing at a projected service account token or at a file some login helper refreshes authenticated as nobody and collected 401s, next to a kubectl working from the very same file. The order is kubectl's, read off client-go: an inline "token" wins over a "tokenFile", and either wins over an "exec" block, which is then not run at all -- so no kubeconfig that works today changes meaning. A relative tokenFile resolves against the directory of the kubeconfig that defines the user, like the certificate fields. Leading and trailing whitespace is stripped, since such a file ends in a newline more often than not and a newline inside an Authorization header does not reach the server as intended. A tokenFile that cannot be read, or that holds nothing but whitespace, is fatal and names the user and the resolved path, rather than falling through to the next mechanism -- that would trade a clear error for a 401 with nothing pointing at the file. The in-cluster service account token is read through the same code now, so it gets the same treatment instead of losing exactly one trailing newline. - New Kubernetes::REST::AuthTokenFile: credentials whose bearer token lives in a file and are re-read when that file changes. Kubernetes rotates those files -- the kubelet replaces a projected service account token well before it expires -- so a client that read the token once at startup went on sending it and started collecting 401s an hour in, with a valid token sitting in the file next to it. A user's tokenFile and the in-cluster service account token are now handed to Kubernetes::REST as one of these, so a rotated token is picked up on the first request after the rotation without rebuilding anything. The trigger is the file, not a timer: every token() call stats the file and re-reads when device, inode, size or modification time differ from the last read. Inode is what carries it, because the kubelet does not overwrite the token -- it writes a new timestamped directory and renames a new "..data" symlink over the old one, so the path resolves to a different file while the old inode is never touched. That costs one stat per request, which is deliberate and documented. What it cannot see is an in-place rewrite that keeps the inode, the exact size and the modification time; nothing short of reading the file every time would, and nothing Kubernetes does looks like that. A read that fails after a successful one -- the file gone for a moment mid-rotation, or momentarily empty -- keeps the last good token instead of dying, the way client-go does; only the first read, when the client is built, is fatal, exactly as before. Set refresh_token_files => 0 on Kubernetes::REST::Kubeconfig to switch the whole thing off and get one read at build time and no stat per request. Note that $api->credentials is therefore no longer always a Kubernetes::REST::AuthToken for a kubeconfig with a tokenFile or an in-cluster client. It still answers token(), which is all the credentials attribute has ever required of it and all this distribution calls; code testing the class rather than the method is the only thing that notices. 1.106 2026-08-08 19:03:58Z - Fix "HTTP::Message content must be bytes" on any request body containing a non-ASCII character. The internal JSON encoder now uses utf8 => 1 (plus canonical and convert_blessed), so request bodies leave as UTF-8 bytes. Applying manifests with e.g. a section sign or an en dash in a description -- the upstream cert-manager CRDs, among others -- died before ever reaching the cluster. - Fix silent mojibake on non-ASCII response values under the default LWP backend. LWPIO used decoded_content(), handing back characters that IO::K8s then decoded a second time; it now uses decoded_content(charset => 'none'), matching HTTPTinyIO. Both backends now inflate identical objects from identical bytes, which t/24_encoding.t pins. - Response bodies and streaming chunks are bytes across every IO backend -- documented in Kubernetes::REST::Role::IO as the encoding contract that custom backends must honour, and in a new ENCODING section in Kubernetes::REST. Objects handed to and returned by the API carry characters; log() output stays bytes, as container output is not guaranteed to be text. - API error messages are now UTF-8 decoded before being croaked, so a non-ASCII error body from the cluster is readable instead of garbled. - The CLI's JSON output encoder now sets utf8, so non-ASCII values print as correct UTF-8 instead of triggering "Wide character in print". - Require IO::K8s 1.105 (was 1.008). IO::K8s has decoded with utf8 => 1 since 1.000, so the encoding fix above works on the old minimum too -- the bump just tracks the current release. - Move the five packages that shared a file with another package into files of their own: Kubernetes::REST::RemoteError (was in Error.pm) and the four Kubernetes::REST::CLI::Cmd::* command classes (were in CLI.pm). All five reported $VERSION 1.003, the version they were written in, because RewriteVersion and BumpVersionAfterRelease only ever rewrite the first "our $VERSION" per file -- while the metadata reported the release version, so 1.105 shipped a CLI::Cmd::Get saying 1.003 in the code and 1.105 in META. One package per file removes the whole class of problem; t/25_one_package_per_file.t keeps it removed. - Kubernetes::REST::RemoteError has to be loaded explicitly now. Loading Kubernetes::REST::Error no longer brings it along -- RemoteError inherits from it, so the parent file has to finish loading first, which a circular use cannot do. Code catching these deprecated exceptions is unaffected; code throwing one needs "use Kubernetes::REST::RemoteError". - Give bin/kube_client and bin/kube_watch a $VERSION of their own; they were the only files in the distribution without one. 1.105 2026-08-08 02:34:04Z - Remove the old v0 API's per-endpoint Call::* classes (1002) and 10 v0-only helper modules (CallContext, Apis, Logs, Extensions, Result2Hash, ListToRequest, Result2Object, Settings, Version, Auditregistration). Since the 1.000 v1 rewrite these only ever emitted a deprecation warning on load and had no other function; nothing in this distribution referenced them anymore. Their old module names are now tombstoned in Kubernetes-REST-Deprecated so PAUSE doesn't keep resolving them to this dist's stale 1.104 release. - Note: the Kubernetes::REST::V0Group family ($api->Core, $api->Apps, etc.) is NOT affected -- it's a still-functional AUTOLOAD-based compatibility layer, not a dead stub, and continues to ship as before. 1.104 2026-04-14 16:41:31Z - Add ensure(), ensure_all(), ensure_only() for idempotent create-or-update. Handles 404/409 race conditions and server-side immutability for PersistentVolumeClaim and Job. Accepts either typed IO::K8s objects or plain hashrefs (inflated via struct_to_object). 1.103 2026-03-25 20:09:12Z - Add Claude Code skills (.claude/skills/) for kubernetes-rest API reference, @Author::GETTY bundle, and dist.ini analysis - Add .gitignore rules for .claude/ directory (track skills, ignore session data) - Fix CPAN testers: replace ->@* postfix dereference with @{ } in t/08_crd.t and t/09_crd_autogen.t for Perl < 5.24 compatibility 1.102 2026-03-09 08:36:25Z - Pod Exec API: new `exec()` method in active v1 API. Builds `/exec` requests, supports repeated `command=` parameters and stream toggles (stdin/stdout/stderr/tty), and passes duplex callbacks (on_open/on_frame/on_close/on_error) to IO backends. - Pod Attach API: new `attach()` method in active v1 API. Builds `/attach` requests with stream toggles (stdin/stdout/stderr/tty) and passes duplex callbacks (on_open/on_frame/on_close/on_error) to IO backends. 1.101 2026-03-08 23:49:18Z - Raise minimum required IO::K8s version to 1.008. Needed for CRD YAML serialization correctness with JSON booleans in passthrough/hash fields (fix in IO::K8s 1.008). - Pod Port-Forward API: new port_forward() method in active v1 API. Builds /portforward requests, supports multiple ports, and passes duplex callbacks (on_open/on_frame/on_close/on_error) to IO backends. - _prepare_request now supports array query parameters (repeated key=value) and explicit header overrides/additions. - Kubernetes::REST::Role::IO adds supports_duplex() capability probe and documents optional call_duplex() transport hook. 1.100 2026-03-04 16:41:39Z - Pod Log API: new log() method for retrieving and streaming pod logs. Supports one-shot (returns full log 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. - New Kubernetes::REST::LogEvent class for typed log line events - Public building block methods for async wrappers: build_path(), prepare_request(), check_response(), inflate_object(), inflate_list(), process_watch_chunk(), process_log_chunk(). These provide a stable public API for event-based systems like Net::Async::Kubernetes to integrate without relying on internal methods. 1.004 2026-02-28 18:46:30Z - Fix resource name pluralization in _build_path: correctly handle words ending in ss/sh/ch/x/z (e.g. Ingress -> ingresses, NetworkPolicy -> networkpolicies), consonant+y (e.g. Policy -> policies), and avoid double-s for words already ending in s. 1.003 2026-02-28 01:05:55Z - Kubernetes::REST::Kubeconfig: add in-cluster service account auto-detection. When no kubeconfig file is found, automatically falls back to using the mounted service account token at /var/run/secrets/kubernetes.io/serviceaccount/token. This enables seamless use inside Kubernetes pods without needing a kubeconfig file. 1.002 2026-02-23 02:41:32Z - Fix client-certificate (mTLS) auth: skip empty Authorization header when no bearer token is configured (was sending "Bearer " causing 401) 1.001 2026-02-20 05:03:44Z - Fix inline kubeconfig certificates (use IO::Socket::SSL::Utils for in-memory PEM) - Kubeconfig: respect KUBECONFIG environment variable 1.000 2026-02-13 05:38:17Z - Updated original author email and copyright holder - Complete rewrite for v1 API - Now uses IO::K8s for Kubernetes resource classes - Simplified API: list(), get(), create(), update(), patch(), delete(), watch() - Default HTTP backend switched from HTTP::Tiny to LWP::UserAgent (enables LWP::ConsoleLogger for HTTP traffic debugging) - New Kubernetes::REST::LWPIO backend (HTTPTinyIO still available as alternative) - Pluggable IO architecture via Kubernetes::REST::Role::IO - Patch support: strategic-merge-patch, JSON merge patch (RFC 7396), JSON patch (RFC 6902) - Watch API: streaming resource changes via Kubernetes Watch API - Resumable watches via resourceVersion tracking - Label and field selector support for list() and watch() - Automatic URL building from IO::K8s class metadata - Custom Resource Definition (CRD) support with the standard API - Resource map for short class names (Pod -> IO::K8s::Api::Core::V1::Pod) - Resource map '+' prefix for external classes ('+My::CRD' uses class as-is) - Dynamic resource map loading from cluster (/openapi/v2) - Kubeconfig support (token auth, client certs, exec credential plugins) - New kube_watch CLI tool for watching Kubernetes resource events - Backwards compatibility via deprecated v0 API wrappers (with warnings) - Updated maintainer to GETTY All pre-v1 releases by Jose Luis Martinez Torres (JLMARTIN) 0.02 2019-02-11 00:00:00Z - Fix all calls that have a body parameter: Create*, Patch*, Replace*, etc.. 0.01 2019-01-03 00:00:00Z - First release into an unsuspecting world