Revision history for Kubernetes-REST

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::<Group>::<Version>::<Kind>, 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