NAME
Kubernetes::REST - A Perl REST Client for the Kubernetes API
VERSION
version 1.108
SYNOPSIS
use Kubernetes::REST;
my $api = Kubernetes::REST->new(
server => {
endpoint => 'https://kubernetes.local:6443',
ssl_verify_server => 1,
ssl_ca_file => '/path/to/ca.crt',
},
credentials => { token => $token },
);
# List all namespaces
my $namespaces = $api->list('Namespace');
for my $ns (@{ $namespaces->items }) {
say $ns->metadata->name;
}
# List pods in a namespace
my $pods = $api->list('Pod', namespace => 'default');
# Get a specific pod
my $pod = $api->get('Pod', name => 'my-pod', namespace => 'default');
# Create a namespace
my $ns = $api->new_object(Namespace => {
metadata => { name => 'my-namespace' },
});
my $created = $api->create($ns);
# Create multiple namespaces
for my $i (1..10) {
$api->create($api->new_object(Namespace =>
metadata => { name => "test-ns-$i" },
));
}
# Update a resource (full replacement)
$pod->metadata->labels({ app => 'updated' });
my $updated = $api->update($pod);
# Patch a resource (partial update)
my $patched = $api->patch('Pod', 'my-pod',
namespace => 'default',
patch => { metadata => { labels => { env => 'staging' } } },
);
# Delete a resource
$api->delete($pod);
# or by name:
$api->delete('Pod', name => 'my-pod', namespace => 'default');
# Idempotent create-or-update (from a typed object or a manifest hashref)
$api->ensure($pod);
$api->ensure({
apiVersion => 'v1',
kind => 'Secret',
metadata => { name => 'my-secret', namespace => 'default' },
stringData => { password => 'hunter2' },
});
# Batch apply
$api->ensure_all(@objects);
# Apply a labeled set and prune anything with that label not in the set
$api->ensure_only(
label => 'app.kubernetes.io/component=queen',
objects => \@rbac_objects,
kinds => [qw(Role RoleBinding ClusterRoleBinding)],
namespaces => ['default', undef],
);
DESCRIPTION
This module provides a simple REST client for the Kubernetes API using IO::K8s resource classes. The IO::K8s classes know their own metadata (API version, kind, whether they're namespaced), so URL building is automatic.
server
Required. Kubernetes::REST::Server instance or hashref with server connection configuration.
server => { endpoint => 'https://kubernetes.local:6443' }
Automatically coerces hashrefs to Kubernetes::REST::Server objects.
credentials
Required. Authentication credentials. Can be a hashref, Kubernetes::REST::AuthToken, or any object with a token() method.
credentials => { token => $bearer_token }
Automatically coerces hashrefs to Kubernetes::REST::AuthToken objects.
io
HTTP backend for making requests. Must consume Kubernetes::REST::Role::IO. Defaults to Kubernetes::REST::LWPIO (LWP::UserAgent).
To use HTTP::Tiny instead:
use Kubernetes::REST::HTTPTinyIO;
my $api = Kubernetes::REST->new(
...,
io => Kubernetes::REST::HTTPTinyIO->new(...),
);
See "PLUGGABLE IO ARCHITECTURE" for custom backends.
with
Arrayref of external IO::K8s resource-map providers (CRD bundles), passed straight through to the inner "with" in IO::K8s. Each entry is a provider class name (or object, or plain hashref) whose typed classes are merged into the resource map, so the cluster's CRD Kinds resolve to real classes:
my $api = Kubernetes::REST->new(
server => { endpoint => 'https://k8s.local:6443' },
credentials => { token => $token },
with => ['IO::K8s::GatewayAPI'],
);
my $gw = $api->new_object(Gateway => { metadata => { name => 'gw' } });
# => IO::K8s::GatewayAPI::V1::Gateway
Defaults to []. See "with" in IO::K8s for the accepted provider forms.
k8s
IO::K8s instance configured with the same resource map. Automatically created when needed.
Provides delegated methods: new_object, inflate, json_to_object, struct_to_object, object_to_json, object_to_struct, load, load_yaml. (expand_class is implemented here rather than delegated, so that pure name resolution does not force the cluster resource-map fetch - see "expand_class".)
Delegation is a convenience only, every one of them behaves exactly as it does on IO::K8s, including its argument contract:
new_objecttakes a short or full class name and either a hashref or a flat hash of attributes:my $ns = $api->new_object(Namespace => { metadata => { name => 'foo' } }); my $ns = $api->new_object(Namespace => metadata => { name => 'foo' });inflatetakes a hashref, or JSON as UTF-8 bytes - its decoder isutf8 => 1.json_to_objectandstruct_to_objectauto-detect the class from the decodedkindfield when called with a single argument (JSON or hashref, respectively) - a leading class name is only needed to override that detection:my $pod = $api->json_to_object($json_with_kind); my $pod = $api->json_to_object('Pod', $json_string);object_to_jsonandobject_to_structare their inverses, serialising a typed object back to JSON or to a plain hashref.load_yamltakes a file name or YAML as characters. Handing it bytes turns every non-ASCII value into mojibake, so decode first (Encode::decode('UTF-8', $yaml)). A newline-free argument is taken as a file name.----separated multi-document YAML is supported - the common case for Kubernetes manifests - so this always returns an arrayref, even for a single document:for my $obj (@{ $api->load_yaml('deployment.yaml') }) { $api->create($obj); }loadreads a.pk8smanifest, which is Perl code and isevaled in-process:my $objects = $api->load('myapp.pk8s');Only load
.pk8sfiles you trust; for data-only manifests useload_yaml.
Not delegated: add, which registers extra classes in the IO::K8s resource map. That map is mirrored by this client's own "resource_map" attribute, and mutating one behind the other's back makes the two disagree - reach through $api->k8s->add(...) if you really mean to.
resource_map_from_cluster
Boolean. If true, dynamically loads the resource map from the cluster's OpenAPI spec. Defaults to 1.
Set to 0 to use IO::K8s built-in resource map instead (faster startup, but may not match your cluster version).
cluster_version
Read-only. The Kubernetes cluster version string (e.g., v1.31.0). Fetched automatically from the /version endpoint when first accessed.
resource_map
Hashref mapping short resource names to IO::K8s class paths. By default loads dynamically from the cluster (if resource_map_from_cluster is true) or uses IO::K8s built-in map.
Override for custom resources:
resource_map => {
%{ IO::K8s->default_resource_map },
MyResource => '+My::K8s::V1::MyResource',
}
The + prefix tells IO::K8s that this is a custom class (not in the IO::K8s:: namespace).
expand_class
my $class = $api->expand_class('Pod');
# => IO::K8s::Api::Core::V1::Pod
Resolve a short resource name ('Pod'), a domain-qualified name ('cilium.io/v2/NetworkPolicy'), a +-prefixed or an already fully-qualified class name to its IO::K8s class - the same contract as "expand_class" in IO::K8s, against this client's "resource_map".
Pure name resolution does not cost a cluster roundtrip: as long as the resource map has not been fetched yet (and none was passed to the constructor), a name the built-in IO::K8s map resolves to a loadable class is answered from that map directly. Only a name the built-in map cannot answer falls through to the cluster-backed map, fetching it on first use exactly as before.
fetch_resource_map
my $map = $api->fetch_resource_map;
Build the resource map from the cluster's aggregated discovery documents (GET /api and GET /apis). Returns a hashref mapping short resource names (e.g., Pod) to full IO::K8s class paths.
Called automatically if resource_map_from_cluster is enabled.
Discovery is fetched and cached once per instance (see "invalidate_discovery"); calling this again rebuilds the map from the cached catalog rather than re-querying the cluster. It does not download /openapi/v2 - that spec is fetched lazily only when "schema_for" or "compare_schema" need it.
Version selection (D17). When a group serves a Kind in more than one version, the bare short name (Pod, ServiceCIDR) resolves to the version the cluster marks preferred for that group - not to a fixed "stable beats alpha/beta" preference. A Kind served only outside the preferred version still maps, to its first served version.
Only Kinds whose IO::K8s class this distribution actually ships are recorded in the map. A CRD group with no bundled class (cilium.io, cert-manager.io, ...) is deliberately omitted here, so that its Kinds resolve through the inner IO::K8s - a provider merged via "with", then AutoGen from the fetched /openapi/v2, and finally a fail-closed error - rather than being mapped to a class name that does not exist.
invalidate_discovery
$api->invalidate_discovery;
Discard the cached discovery catalog and the resource map built from it, so the next resolution re-queries the cluster. Use it after a CustomResourceDefinition is installed or changed, to make the new Kind visible to this client instance.
schema_for
my $schema = $api->schema_for('Pod');
Get the OpenAPI schema definition for a resource type from the cluster. Accepts short names (Pod), full class names (IO::K8s::Api::Core::V1::Pod), or OpenAPI definition names (io.k8s.api.core.v1.Pod).
Returns a hashref with the OpenAPI v2 schema definition.
compare_schema
my $result = $api->compare_schema('Pod');
Compare the local IO::K8s class definition against the cluster's OpenAPI schema. Useful for detecting version skew between your IO::K8s installation and the cluster.
Returns the comparison result from $class->compare_to_schema, the method IO::K8s::Role::Resource provides on every resource class.
build_path
my $class = $api->expand_class('Pod');
my $path = $api->build_path($class, name => 'my-pod', namespace => 'default');
# => /api/v1/namespaces/default/pods/my-pod
my $status = $api->build_path($class,
name => 'my-pod',
namespace => 'default',
subresource => 'status',
);
# => /api/v1/namespaces/default/pods/my-pod/status
Build the REST API URL path for a resource class. Takes a fully-qualified class name (from expand_class) and optional name/namespace arguments.
The optional subresource argument appends a subresource segment (status, log, exec, attach, portforward) to the resource path. A subresource always addresses one named resource, so subresource without name croaks rather than returning a path pointing at the collection endpoint.
build_path also accepts kind, api_version, resource and namespaced arguments, but they matter only for IO::K8s::Unstructured. For any other class they are ignored: api_version, pluralisation and namespaced-ness always come from the class itself. Unstructured has no such class identity - its Kind is data on the instance - so the path metadata has to come from somewhere else:
my $path = $api->build_path('IO::K8s::Unstructured',
kind => 'MyCRD',
api_version => 'example.com/v1',
resource => 'mycrds',
namespaced => 1,
name => 'my-instance',
namespace => 'default',
);
# => /apis/example.com/v1/namespaces/default/mycrds/my-instance
Passing api_version, resource and namespaced together, as above, resolves the path directly with no discovery lookup - the case for a caller (such as an async wrapper) that already knows the resource's metadata. Otherwise kind is required (build_path croaks without it), and resource/namespaced/apiVersion are looked up in the client's cached discovery catalog instead, preferring the cluster's preferred version unless api_version pins a specific group/version; build_path croaks if discovery has no entry for the Kind, and equally if the catalog could not be fetched at all (cluster unreachable, expired token) - in that case the message names that failure rather than claiming a missing entry, and the fallback stays fail-closed either way.
This is a public API for async wrappers like Net::Async::Kubernetes that need to construct request paths independently.
prepare_request
my $req = $api->prepare_request('GET', $path,
parameters => \%params,
body => \%body,
);
Build a Kubernetes::REST::HTTPRequest with method, full URL, authorization headers, and optional query parameters or JSON body.
Query parameter values may be scalars or arrayrefs (arrayrefs are emitted as repeated key=value pairs). Extra request headers can be provided via headers => \%headers.
This is a public API for async wrappers that execute HTTP requests through their own event loop.
check_response
$api->check_response($response, "get Pod");
Validate an HTTP response. Croaks with a descriptive error if the status code is >= 400. Returns the response on success.
inflate_object
my $pod = $api->inflate_object($class, $response);
Decode the JSON response body and inflate it into a typed IO::K8s object.
inflate_list
my $list = $api->inflate_list($class, $response);
Decode the JSON response body and inflate the items array into an IO::K8s::List of typed objects.
An item the object model rejects - typically because the installed IO::K8s does not know a field the cluster version serves - is dropped from the list, and a warning names every dropped item (index, metadata.name, the inflation error) so an incomplete list is never silent. Promote it to a fatal error with local $SIG{__WARN__} = sub { die @_ } if partial results are unacceptable to you.
process_watch_chunk
my @results = $api->process_watch_chunk($class, \$buffer, $chunk);
Process a chunk of NDJSON watch data. Appends the chunk to the buffer, extracts complete lines, and returns a list of hashrefs with event (Kubernetes::REST::WatchEvent), resourceVersion, is_error, and error_code.
This is a public API for async wrappers that handle streaming watch responses through their own event loop.
process_log_chunk
my @events = $api->process_log_chunk(\$buffer, $chunk);
Process a chunk of plain-text log data. Appends the chunk to the buffer, extracts complete lines, and returns a list of Kubernetes::REST::LogEvent objects.
This is a public API for async wrappers that handle streaming log responses through their own event loop.
list
my $list = $api->list('Pod', namespace => 'default');
my $list = $api->list('Namespace', labelSelector => 'app=web');
List resources. Returns an IO::K8s::List object.
Accepts short class names (Pod) or full class paths. For namespaced resources, pass namespace parameter. Omit namespace to list cluster-scoped resources.
Supports labelSelector and fieldSelector query parameters for server-side filtering.
get
my $pod = $api->get('Pod', name => 'my-pod', namespace => 'default');
# or shorthand:
my $pod = $api->get('Pod', 'my-pod', namespace => 'default');
Get a single resource by name. Returns a typed IO::K8s object.
create
my $created = $api->create($pod);
Create a resource from an IO::K8s object. Returns the created object with server-assigned fields (UID, resourceVersion, etc.).
update
my $updated = $api->update($pod);
Update an existing resource. Replaces the entire object server-side. Returns the updated object.
For partial updates, use "patch" instead.
patch
my $patched = $api->patch('Pod', 'my-pod',
namespace => 'default',
patch => { metadata => { labels => { env => 'staging' } } },
);
# Or with an object:
my $patched = $api->patch($pod,
patch => { metadata => { labels => { env => 'staging' } } },
);
# JSON Patch (RFC 6902) instead: an array of operations, not a hashref
my $patched = $api->patch('Deployment', 'my-app',
namespace => 'default',
type => 'json',
patch => [
{ op => 'replace', path => '/spec/replicas', value => 3 },
{ op => 'add', path => '/metadata/labels/env', value => 'prod' },
],
);
Partially update a resource. Unlike update() which replaces the entire object, patch() only modifies specified fields.
Required: patch (a hashref, or an arrayref of operations when type is json) and, when passing a class rather than an object, name.
Optional: namespace (for namespaced resources) and type, the patch strategy:
strategic(default)-
Strategic Merge Patch. The Kubernetes-native patch type, which understands array merge semantics (e.g. adding a container to a pod spec without removing the existing ones).
merge-
JSON Merge Patch (RFC 7396). Simple recursive merge where
nullvalues delete keys; arrays are replaced entirely. json-
JSON Patch (RFC 6902): an array of operations, as in the third example above.
Returns the full updated object from the server.
patch_status
my $node = $api->patch_status('OCPNode', 'cp-1',
namespace => 'ocp',
patch => { status => { phase => 'Ready', ip => '10.0.0.7' } },
);
# Or with an object:
my $node = $api->patch_status($node,
patch => { status => { phase => 'Ready' } },
);
Partially update a resource's status through the /status subresource.
Once a CustomResourceDefinition declares subresources: {status: {}}, the API server strips the status stanza from every write to the main endpoint - create, update, patch and server-side apply alike - and still answers 2xx. The write appears to succeed and nothing is stored. Status has to go to /status, which is what this method addresses.
Takes the same arguments as "patch" (object or class plus name, both call forms, the same type values) and returns the full object from the server. The patch document is passed through unchanged, so it carries its own status key.
The default patch type is merge, not strategic as in "patch": custom resources do not support strategic merge patch and the API server rejects it with 415, and merge works for built-in kinds as well. Pass type => 'strategic' explicitly when patching the status of a built-in resource and you need array merge semantics.
update_status
my $node = $api->update_status($node);
Replace a resource's status through the /status subresource. The whole object is sent, as with "update", but the server only takes its status and leaves spec and metadata untouched. Returns the updated object.
This is the read-modify-write counterpart to "patch_status": it needs a current resourceVersion and fails with a 409 conflict when the object changed in the meantime. Prefer "patch_status" when you are setting individual status fields.
delete
$api->delete($pod);
# or by name:
$api->delete('Pod', name => 'my-pod', namespace => 'default');
# or shorthand:
$api->delete('Pod', 'my-pod', namespace => 'default');
Delete a resource. Returns true on success.
ensure
my $obj = $api->ensure($pod);
# or from a plain hashref (treated as a Kubernetes manifest):
my $secret = $api->ensure({
apiVersion => 'v1',
kind => 'Secret',
metadata => { name => 'foo', namespace => 'default' },
stringData => { password => 'hunter2' },
});
Idempotent create-or-update. Fetches the resource by kind/name/namespace; if it exists, updates it (preserving resourceVersion), otherwise creates it. Returns the resulting IO::K8s object.
Accepts either a typed IO::K8s object or a plain hashref. A hashref must carry a kind field and is inflated to a typed object via "struct_to_object" in IO::K8s. Hashref keys follow the Kubernetes API convention (camelCase, e.g. stringData, not string_data).
Handles common race conditions:
404 on initial get is treated as "does not exist" and falls through to create.
409 AlreadyExists on create (resource appeared between get and create) is retried as an update.
409 Conflict on update (resourceVersion changed server-side, e.g. a controller wrote status) is retried by re-fetching and re-applying.
Special-cases for kinds with server-side mutation constraints:
PersistentVolumeClaim- spec is immutable after creation, so an existing PVC is returned unchanged.Job- spec is immutable; an existing Job that is active or has succeeded is returned unchanged. A failed Job is deleted and recreated.
ensure_all
my @results = $api->ensure_all(@objects);
Batch version of "ensure". Applies create-or-update to each object in order and returns the list of resulting objects.
ensure_only
$api->ensure_only(
label => 'app.kubernetes.io/component=queen',
objects => \@objects,
kinds => [qw(Role RoleBinding ClusterRoleBinding)],
namespaces => ['default', 'kube-system', undef],
);
Like "ensure_all", but also deletes any resources matching the label selector in the given kinds and namespaces that are not present in objects. Use this for resources where stale objects must not survive (e.g. RBAC).
Pass undef inside namespaces to scan cluster-scoped resources. If namespaces is omitted, only cluster-scoped resources are scanned.
Returns the list of applied objects (from "ensure_all").
ensure_crd
# one class == one single-version CRD
$api->ensure_crd('My::K8s::StaticWebSite');
# several distinct CRDs at once
$api->ensure_crd(@crd_classes);
# with options (pass the classes as an arrayref):
$api->ensure_crd(\@crd_classes, timeout => 60, poll_interval => 2);
# several classes that are versions of the SAME CRD -> one multi-version CRD
$api->ensure_crd(
[ 'My::K8s::V1beta1::Widget', 'My::K8s::V1::Widget' ],
storage => 'v1',
);
Install (create-or-update) one or more CustomResourceDefinitions from typed IO::K8s classes, wait for each to reach the Established condition, then "invalidate_discovery" so the new Kinds resolve on this client instance.
Each class's CRD manifest comes from $class->to_crd (IO::K8s::CRD). The assembled CRD is applied with "ensure", so re-running is idempotent. After every CRD is applied, each is polled with "get" by name until its status.conditions carries type => 'Established', status => 'True', subject to a timeout. Only then is the discovery cache invalidated, so the next create/list of a custom resource does not race the apiserver registering the Kind (the reason plain ensure of the CRD is not enough: the following call almost always creates a CR, which 404s until Established).
Returns the list of established CustomResourceDefinition objects (the objects read back from the final poll, carrying their Established status).
Arguments: the CRD classes, either as a plain list (ensure_crd(@classes)) or, when options are needed, as an arrayref followed by named options (ensure_crd(\@classes, %opts)).
- timeout
-
Seconds to wait for the
Establishedcondition per CRD (default 30). On expiry the call croaks, naming the CRD and thatEstablishedwas not reached. - poll_interval
-
Seconds between polls (default 1). The current state is always checked once before the timeout is consulted, so
timeout => 0performs exactly one poll. - storage
-
Only consulted when two or more passed classes name the SAME CRD (same group, plural, kind and scope but different versions). Those are assembled into ONE multi-version CustomResourceDefinition via
IO::K8s::CRD->new(classes => [...], storage => ...)rather than applied as competing single-version CRDs. Because a class carries no marker for which version is authoritative, the storage version is not guessed: pass it as a bare version string (applies to the multi-version group) or as a hashref keyed by CRD name ({ 'widgets.example.com' => 'v1' }). A multi-version group with no storage version croaks, naming the CRD and the candidate versions.
watch
my $last_rv = $api->watch('Pod',
namespace => 'default',
on_event => sub {
my ($event) = @_;
say $event->type . ": " . $event->object->metadata->name;
},
timeout => 300,
resourceVersion => '12345',
labelSelector => 'app=web',
fieldSelector => 'status.phase=Running',
);
Watch for changes to resources. Uses the Kubernetes Watch API with chunked transfer encoding to stream events. The call blocks until the server-side timeout expires.
Required: on_event, a callback invoked with a Kubernetes::REST::WatchEvent for each event.
Optional:
- timeout
-
Server-side timeout in seconds (default: 300). The API server closes the connection after this many seconds.
- resourceVersion
-
Resume watching from a specific resource version - pass the return value of a previous
watch()call to avoid missing events. - labelSelector
-
Filter by label selector (e.g.
'app=web,env=prod'). - fieldSelector
-
Filter by field selector (e.g.
'status.phase=Running'). - namespace
-
For namespaced resources, the namespace to watch.
Returns the last resourceVersion seen. Croaks on 410 Gone once the given resourceVersion has expired - re-list to get a fresh one and resume from there:
my $rv;
while (1) {
$rv = eval {
$api->watch('Pod',
namespace => 'default',
resourceVersion => $rv,
on_event => \&handle_event,
);
};
if ($@ && $@ =~ /410 Gone/) {
# resourceVersion expired, re-list to get fresh version
my $list = $api->list('Pod', namespace => 'default');
$rv = undef; # start fresh
}
}
log
# One-shot: get full log as string
my $text = $api->log('Pod', 'my-pod',
namespace => 'default',
tailLines => 100,
);
# Streaming: callback per log line
$api->log('Pod', 'my-pod',
namespace => 'default',
follow => 1,
on_line => sub {
my ($event) = @_; # Kubernetes::REST::LogEvent
say $event->line;
},
);
Retrieve logs from a pod. Supports two modes:
One-shot (without on_line): Returns the full log text as a string.
Streaming (with on_line): Calls the callback for each log line with a Kubernetes::REST::LogEvent object. Blocks until the stream ends (or the server closes the connection).
Log output is returned as raw bytes in both modes - container output is not guaranteed to be UTF-8, or even text. Decode it yourself when you know it is: Encode::decode('UTF-8', $text). See "ENCODING".
The streaming mode is designed for event-based systems like IO::Async — see Net::Async::Kubernetes for async integration.
Also accepts, for namespaced resources, namespace; and as further optional arguments: container (name, for multi-container pods), sinceSeconds / sinceTime (show only recent output), timestamps (prepend a timestamp to each line), previous (logs from the container's previous run, after a restart), and limitBytes (byte cap on the response).
port_forward
my $session = $api->port_forward('Pod', 'my-pod',
namespace => 'default',
ports => [8080, 8443],
on_frame => sub { my ($channel, $payload) = @_; ... },
);
Start a full-duplex pod port-forward session.
Required: name and ports - one port number or an arrayref of them (e.g. [8080, 8443]).
Optional: namespace (for namespaced resources), subprotocol (WebSocket subprotocol, default v4.channel.k8s.io), and the duplex transport callbacks on_open, on_frame, on_close, on_error, passed through to the IO backend.
This method requires an IO backend that implements call_duplex. The default Kubernetes::REST::LWPIO and Kubernetes::REST::HTTPTinyIO backends do not currently provide duplex transport.
Returns whatever the IO backend returns for call_duplex (typically a session/handle object managed by that backend).
exec
my $session = $api->exec('Pod', 'my-pod',
namespace => 'default',
command => ['sh', '-c', 'echo hello'],
stdin => 0,
stdout => 1,
stderr => 1,
tty => 0,
on_frame => sub { my ($channel, $payload) = @_; ... },
);
Start a full-duplex pod exec session via the /exec subresource.
Required: name and command - a single string or an arrayref (e.g. ['sh', '-c', 'id']).
Optional: namespace, container (for multi-container pods), the stream toggles stdin/stdout/stderr/tty shown above (defaults: stdin and tty off, stdout and stderr on), subprotocol (WebSocket subprotocol, default v4.channel.k8s.io), and the duplex transport callbacks on_open, on_frame, on_close, on_error, passed through to the IO backend.
This method requires an IO backend that implements call_duplex. The default Kubernetes::REST::LWPIO and Kubernetes::REST::HTTPTinyIO backends do not currently provide duplex transport.
Returns whatever the IO backend returns for call_duplex (typically a session/handle object managed by that backend).
attach
my $session = $api->attach('Pod', 'my-pod',
namespace => 'default',
container => 'app',
stdin => 1,
stdout => 1,
stderr => 1,
tty => 0,
on_frame => sub { my ($channel, $payload) = @_; ... },
);
Start a full-duplex pod attach session via the /attach subresource.
Required: name.
Optional: namespace, container (for multi-container pods), the stream toggles stdin/stdout/stderr/tty shown above (defaults: stdin and tty off, stdout and stderr on), subprotocol (WebSocket subprotocol, default v4.channel.k8s.io), and the duplex transport callbacks on_open, on_frame, on_close, on_error, passed through to the IO backend.
This method requires an IO backend that implements call_duplex. The default Kubernetes::REST::LWPIO and Kubernetes::REST::HTTPTinyIO backends do not currently provide duplex transport.
Returns whatever the IO backend returns for call_duplex (typically a session/handle object managed by that backend).
NAME
Kubernetes::REST - A Perl REST Client for the Kubernetes API
UPGRADING FROM 0.02
WARNING: Version 1.00 contains breaking changes!
This version has been completely rewritten. Key changes that may affect your code:
New simplified API
The old method-per-operation API (e.g.,
$api->Core->ListNamespacedPod(...)) has been replaced with a simple API:list,get,create,update,patch,patch_status,update_status,delete,ensure,ensure_all,ensure_only,watch,log,port_forward,exec,attach.Old API still works but deprecated
The old API is still available for backwards compatibility but will emit deprecation warnings. Set
$ENV{HIDE_KUBERNETES_REST_V0_API_WARNING}to suppress warnings.Uses IO::K8s classes
Results are now returned as typed IO::K8s objects instead of raw hashrefs. Lists are returned as IO::K8s::List objects.
Note: IO::K8s has also been completely rewritten (Moose to Moo, API objects updated to a current Kubernetes release). See "UPGRADING FROM PREVIOUS VERSIONS" in IO::K8s for details.
Short resource names
You can now use short names like
'Pod'instead of full class paths. Theresource_mapattribute controls this mapping.Dynamic resource map
Use
resource_map_from_cluster => 1to load the resource map from the cluster's OpenAPI spec, ensuring compatibility with any Kubernetes version.
BUILDING BLOCKS FOR ASYNC WRAPPERS
Async wrappers like Net::Async::Kubernetes need access to the request/response pipeline without going through the synchronous convenience methods. The following public methods provide this:
expand_class($short)- Resolve short name to full classbuild_path($class, %args)- Build REST API URL pathprepare_request($method, $path, %opts)- Build HTTP request with authcheck_response($response, $context)- Validate HTTP statusinflate_object($class, $response)- JSON to typed objectinflate_list($class, $response)- JSON to typed list (an item the object model rejects is dropped and carped about, not silently lost - see "inflate_list")process_watch_chunk($class, \$buf, $chunk)- Parse NDJSON watch streamprocess_log_chunk(\$buf, $chunk)- Parse plain-text log stream
Example async integration:
# Build request using Kubernetes::REST
my $class = $rest->expand_class('Pod');
my $path = $rest->build_path($class,
name => $name,
namespace => $ns,
subresource => 'log',
);
my $req = $rest->prepare_request('GET', $path, parameters => { follow => 'true' });
# Execute through your own event loop
my $buffer = '';
$async_http->request($req->url, sub {
my ($chunk) = @_;
for my $event ($rest->process_log_chunk(\$buffer, $chunk)) {
$on_line->($event);
}
});
PLUGGABLE IO ARCHITECTURE
The HTTP transport is decoupled from request preparation and response processing. This makes it possible to swap the default LWP::UserAgent backend for HTTP::Tiny or an async backend (e.g. Net::Async::HTTP) without changing any API logic.
The pipeline for each API call:
1. prepare_request() - builds HTTPRequest (method, url, headers, body)
2. io->call() - executes request (pluggable backend)
3. check_response() - validates HTTP status
4. inflate_object/list - decodes JSON + inflates IO::K8s objects
For watch, step 2 uses io->call_streaming() and step 4 uses process_watch_chunk() which parses NDJSON and inflates each event.
For log, step 2 uses io->call_streaming() and step 4 uses process_log_chunk() which parses plain-text lines into Kubernetes::REST::LogEvent objects.
To implement a custom IO backend, consume Kubernetes::REST::Role::IO and implement call($req) and call_streaming($req, $callback). See Kubernetes::REST::LWPIO and Kubernetes::REST::HTTPTinyIO for reference implementations.
ENCODING
On the wire everything is bytes; in your program everything is characters. The boundary sits in this module, and you do not have to do anything for it:
Objects you pass to
create,update,patchand friends hold ordinary Perl character strings. They are UTF-8 encoded on the way into the request body, sodata => { note => "Caf\x{e9} \x{a7}" }is applied to the cluster unchanged.Objects you get back from
get,list,watchand friends hold decoded characters, solengthcounts characters and regexes match as expected.Exception messages from failed API calls are decoded too.
Two things stay bytes on purpose:
log- container output is an arbitrary byte stream, not necessarily UTF-8, and decoding it would corrupt anything binary. Decode it yourself if you know it is text:Encode::decode('UTF-8', $api->log('Pod', $name)). The same applies to$event->linein streaming mode.The
contentof Kubernetes::REST::HTTPRequest and Kubernetes::REST::HTTPResponse - these are the raw HTTP layer. Custom IO backends must honour that; see "Encoding contract" in Kubernetes::REST::Role::IO.
SEE ALSO
Related Modules
IO::K8s - Kubernetes resource classes (required dependency)
Net::Async::Kubernetes - Async Kubernetes client for IO::Async
Configuration and Authentication
Kubernetes::REST::Kubeconfig - Load settings from kubeconfig
Kubernetes::REST::Server - Server connection configuration
Kubernetes::REST::AuthToken - Authentication credentials
HTTP Backends
Kubernetes::REST::Role::IO - IO interface role
Kubernetes::REST::LWPIO - LWP::UserAgent backend (default)
Kubernetes::REST::HTTPTinyIO - HTTP::Tiny backend
LWP::ConsoleLogger - HTTP debugging for LWPIO
Data Objects
Kubernetes::REST::WatchEvent - Watch event object
Kubernetes::REST::LogEvent - Log event object
Kubernetes::REST::HTTPRequest - HTTP request object
Kubernetes::REST::HTTPResponse - HTTP response object
CLI Tools
Kubernetes::REST::CLI - CLI base class
Kubernetes::REST::CLI::Watch - kube_watch CLI tool
Kubernetes::REST::CLI::Role::Connection - Shared CLI options
Examples and Documentation
Kubernetes::REST::Example - Comprehensive examples with Minikube/K3s
https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.36/ - Kubernetes API reference
SUPPORT
Issues
Please report bugs and feature requests on GitHub at https://github.com/pplu/kubernetes-rest/issues.
IRC
Join #kubernetes on irc.perl.org or message Getty directly.
CONTRIBUTING
Contributions are welcome! Please fork the repository and submit a pull request.
AUTHORS
Torsten Raudssus <getty@cpan.org>
Jose Luis Martinez Torres <jlmartin@cpan.org>
COPYRIGHT AND LICENSE
This software is Copyright (c) 2019-2026 by Jose Luis Martinez Torres <jlmartin@cpan.org>.
This is free software, licensed under:
The Apache License, Version 2.0, January 2004