Kubernetes-REST
view release on metacpan or search on metacpan
lib/Kubernetes/REST.pm view on Meta::CPAN
my $api_version = $class->can('api_version') ? $class->api_version : undef;
croak "Cannot determine api_version for $class - override api_version() in your CRD class"
unless defined $api_version;
my $kind = $class->can('kind') ? $class->kind : (split('::', $class))[-1];
my $is_namespaced = $class->does('IO::K8s::Role::Namespaced');
# Use explicit resource_plural if available, otherwise auto-pluralize
my $resource;
if ($class->can('resource_plural') && $class->resource_plural) {
$resource = $class->resource_plural;
} else {
$resource = lc($kind);
if ($resource =~ /(?:ss|sh|ch|x|z)$/) {
$resource .= 'es'; # class -> classes, ingress -> ingresses
} elsif ($resource =~ /[^aeiou]y$/) {
$resource =~ s/y$/ies/; # policy -> policies
} elsif ($resource !~ /s$/) {
$resource .= 's'; # pod -> pods
}
}
# Build path based on API group
my $path;
if ($api_version =~ m{/}) {
# Has group: apps/v1 -> /apis/apps/v1/...
$path = "/apis/$api_version";
} else {
# Core: v1 -> /api/v1/...
$path = "/api/$api_version";
}
if ($is_namespaced && $args{namespace}) {
$path .= "/namespaces/$args{namespace}";
}
$path .= "/$resource";
if ($args{name}) {
$path .= "/$args{name}";
}
return $path;
}
# ============================================================================
# REQUEST / RESPONSE PIPELINE
#
# The API methods (list, get, create, etc.) are built on a 3-step pipeline:
#
# 1. _prepare_request - builds an HTTPRequest (method, url, headers, body)
# 2. io->call - executes the request (pluggable: HTTP::Tiny, async, mock)
# 3. _check_response / _inflate_object / _inflate_list - processes the response
#
# This separation allows different IO backends (sync, async, mock) to slot in
# at step 2 without touching request preparation or response processing.
# ============================================================================
sub _prepare_request {
my ($self, $method, $path, %opts) = @_;
my $url = $self->server->endpoint . $path;
my $content_type = $opts{content_type} // 'application/json';
my $body = $opts{body};
my $parameters = $opts{parameters};
my $extra_headers = $opts{headers} // {};
# Append query parameters to URL
if ($parameters && %$parameters) {
my @pairs;
for my $key (sort keys %$parameters) {
my $val = $parameters->{$key};
next unless defined $val;
if (ref($val) eq 'ARRAY') {
push @pairs, map { "$key=$_" } grep { defined } @$val;
} else {
push @pairs, "$key=$val";
}
}
if (@pairs) {
$url .= ($url =~ /\?/ ? '&' : '?') . join('&', @pairs);
}
}
my %headers = (
'Content-Type' => $content_type,
'Accept' => 'application/json',
);
# Only add Authorization header when a token is available
# (client-certificate auth doesn't need a Bearer token)
my $token = $self->credentials->token;
if (defined $token && length $token) {
$headers{'Authorization'} = 'Bearer ' . $token;
}
if ($extra_headers && ref($extra_headers) eq 'HASH') {
@headers{keys %$extra_headers} = values %$extra_headers;
}
return Kubernetes::REST::HTTPRequest->new(
method => $method,
url => $url,
headers => \%headers,
($body ? (content => $self->_json->encode($body)) : ()),
);
}
sub _check_response {
my ($self, $response, $context) = @_;
if ($response->status >= 400) {
croak "Kubernetes API error ($context): "
. $response->status . " " . ($response->content // '');
}
return $response;
}
sub _inflate_object {
my ($self, $class, $response) = @_;
return $self->k8s->json_to_object($class, $response->content);
}
sub _inflate_list {
lib/Kubernetes/REST.pm view on Meta::CPAN
my $on_frame = delete $args{on_frame};
my $on_close = delete $args{on_close};
my $on_error = delete $args{on_error};
my $class = $self->expand_class($short_class);
my $path = $self->_build_path($class, %args) . '/attach';
my %params = (
stdin => $stdin ? 'true' : 'false',
stdout => $stdout ? 'true' : 'false',
stderr => $stderr ? 'true' : 'false',
tty => $tty ? 'true' : 'false',
);
$params{container} = $container if defined $container;
my $req = $self->_prepare_request('GET', $path,
parameters => \%params,
headers => {
Accept => '*/*',
Connection => 'Upgrade',
Upgrade => 'websocket',
'Sec-WebSocket-Protocol' => $subprotocol,
},
);
my $io = $self->io;
unless ($io->can('call_duplex')) {
croak "IO backend does not support attach(): missing call_duplex()";
}
return $io->call_duplex($req,
on_open => $on_open,
on_frame => $on_frame,
on_close => $on_close,
on_error => $on_error,
);
}
1;
__END__
=pod
=encoding UTF-8
=head1 NAME
Kubernetes::REST - A Perl REST Client for the Kubernetes API
=head1 VERSION
version 1.104
=head1 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],
);
=head1 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.
=head2 server
Required. L<Kubernetes::REST::Server> instance or hashref with server connection configuration.
server => { endpoint => 'https://kubernetes.local:6443' }
Automatically coerces hashrefs to L<Kubernetes::REST::Server> objects.
=head2 credentials
Required. Authentication credentials. Can be a hashref, L<Kubernetes::REST::AuthToken>, or any object with a C<token()> method.
credentials => { token => $bearer_token }
Automatically coerces hashrefs to L<Kubernetes::REST::AuthToken> objects.
=head2 io
HTTP backend for making requests. Must consume L<Kubernetes::REST::Role::IO>. Defaults to L<Kubernetes::REST::LWPIO> (L<LWP::UserAgent>).
To use L<HTTP::Tiny> instead:
use Kubernetes::REST::HTTPTinyIO;
my $api = Kubernetes::REST->new(
...,
io => Kubernetes::REST::HTTPTinyIO->new(...),
);
See L</PLUGGABLE IO ARCHITECTURE> for custom backends.
=head2 k8s
L<IO::K8s> instance configured with the same resource map. Automatically created when needed.
Provides delegated methods: C<new_object>, C<inflate>, C<json_to_object>, C<struct_to_object>, C<expand_class>.
=head2 resource_map_from_cluster
Boolean. If true, dynamically loads the resource map from the cluster's OpenAPI spec. Defaults to C<1>.
Set to C<0> to use L<IO::K8s> built-in resource map instead (faster startup, but may not match your cluster version).
=head2 cluster_version
Read-only. The Kubernetes cluster version string (e.g., C<v1.31.0>). Fetched automatically from the C</version> endpoint when first accessed.
=head2 resource_map
Hashref mapping short resource names to L<IO::K8s> class paths. By default loads dynamically from the cluster (if C<resource_map_from_cluster> is true) or uses L<IO::K8s> built-in map.
Override for custom resources:
resource_map => {
%{ IO::K8s->default_resource_map },
MyResource => '+My::K8s::V1::MyResource',
}
The C<+> prefix tells L<IO::K8s> that this is a custom class (not in the IO::K8s:: namespace).
=head2 fetch_resource_map
my $map = $api->fetch_resource_map;
Fetch the resource map from the cluster's OpenAPI spec (C</openapi/v2> endpoint). Returns a hashref mapping short resource names (e.g., C<Pod>) to full L<IO::K8s> class paths.
Called automatically if C<resource_map_from_cluster> is enabled.
=head2 schema_for
my $schema = $api->schema_for('Pod');
Get the OpenAPI schema definition for a resource type from the cluster. Accepts short names (C<Pod>), full class names (C<IO::K8s::Api::Core::V1::Pod>), or OpenAPI definition names (C<io.k8s.api.core.v1.Pod>).
Returns a hashref with the OpenAPI v2 schema definition.
=head2 compare_schema
my $result = $api->compare_schema('Pod');
Compare the local L<IO::K8s> class definition against the cluster's OpenAPI schema. Useful for detecting version skew between your L<IO::K8s> installation and the cluster.
Returns the comparison result from C<< IO::K8s::Resource->compare_to_schema >>.
=head2 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
Build the REST API URL path for a resource class. Takes a fully-qualified class name (from L</expand_class>) and optional C<name>/C<namespace> arguments.
This is a public API for async wrappers like L<Net::Async::Kubernetes> that need to construct request paths independently.
=head2 prepare_request
my $req = $api->prepare_request('GET', $path,
parameters => \%params,
body => \%body,
);
Build a L<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 C<key=value> pairs). Extra request headers can be provided via
C<headers =E<gt> \%headers>.
This is a public API for async wrappers that execute HTTP requests through their own event loop.
=head2 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.
=head2 inflate_object
my $pod = $api->inflate_object($class, $response);
Decode the JSON response body and inflate it into a typed L<IO::K8s> object.
=head2 inflate_list
my $list = $api->inflate_list($class, $response);
lib/Kubernetes/REST.pm view on Meta::CPAN
Start a full-duplex pod attach session via the C</attach> subresource.
This method requires an IO backend that implements C<call_duplex>. The default
L<Kubernetes::REST::LWPIO> and L<Kubernetes::REST::HTTPTinyIO> backends do not
currently provide duplex transport.
Returns whatever the IO backend returns for C<call_duplex> (typically a
session/handle object managed by that backend).
=head1 NAME
Kubernetes::REST - A Perl REST Client for the Kubernetes API
=head1 UPGRADING FROM 0.02
B<WARNING: Version 1.00 contains breaking changes!>
This version has been completely rewritten. Key changes that may affect your code:
=over 4
=item * B<New simplified API>
The old method-per-operation API (e.g., C<< $api->Core->ListNamespacedPod(...) >>)
has been replaced with a simple API: C<list>, C<get>, C<create>, C<update>,
C<patch>, C<delete>, C<ensure>, C<ensure_all>, C<ensure_only>, C<watch>,
C<log>, C<port_forward>, C<exec>, C<attach>.
=item * B<Old API still works but deprecated>
The old API is still available for backwards compatibility but will emit deprecation
warnings. Set C<$ENV{HIDE_KUBERNETES_REST_V0_API_WARNING}> to suppress warnings.
=item * B<Uses IO::K8s classes>
Results are now returned as typed L<IO::K8s> objects instead of raw hashrefs.
Lists are returned as L<IO::K8s::List> objects.
B<Note:> L<IO::K8s> has also been completely rewritten (Moose to Moo, updated
to Kubernetes v1.31 API). See L<IO::K8s/"UPGRADING FROM 0.04"> for details.
=item * B<Short resource names>
You can now use short names like C<'Pod'> instead of full class paths. The
C<resource_map> attribute controls this mapping.
=item * B<Dynamic resource map>
Use C<resource_map_from_cluster =E<gt> 1> to load the resource map from the
cluster's OpenAPI spec, ensuring compatibility with any Kubernetes version.
=back
=head1 ATTRIBUTES
=head2 server
Required. Connection details for the Kubernetes API server. Can be a hashref or
a L<Kubernetes::REST::Server> object.
server => { endpoint => 'https://kubernetes.local:6443' }
=head2 credentials
Required. Authentication credentials. Can be a hashref or a L<Kubernetes::REST::AuthToken>
object.
credentials => { token => $bearer_token }
=head2 io
Optional. HTTP backend for making requests. Must consume the
L<Kubernetes::REST::Role::IO> role (i.e. implement C<call($req)> and
C<call_streaming($req, $callback)>; optional C<call_duplex($req, %callbacks)> for
full-duplex subresources such as pod port-forward). Defaults to L<Kubernetes::REST::LWPIO>
(L<LWP::UserAgent>), which supports L<LWP::ConsoleLogger> for HTTP debugging.
To use the lighter L<HTTP::Tiny> backend instead:
use Kubernetes::REST::HTTPTinyIO;
my $api = Kubernetes::REST->new(
server => ...,
credentials => ...,
io => Kubernetes::REST::HTTPTinyIO->new(
ssl_verify_server => 1,
),
);
To use an async event loop, provide your own IO backend:
my $api = Kubernetes::REST->new(
server => ...,
credentials => ...,
io => My::AsyncIO->new(loop => $loop),
);
=head2 k8s
Optional. L<IO::K8s> instance configured with the same resource map as this client.
Automatically created when needed.
=head2 resource_map_from_cluster
Optional boolean. If true, loads the resource map dynamically from the cluster's
OpenAPI spec. Defaults to true (loads from cluster).
resource_map_from_cluster => 1
=head2 resource_map
Optional hashref. Maps short resource names to IO::K8s class paths. By default
loads dynamically from the cluster (if C<resource_map_from_cluster> is true) or
uses L<IO::K8s> built-in map. Can be overridden for custom resources.
resource_map => { MyResource => 'Custom::V1::MyResource' }
=head2 cluster_version
Read-only. The Kubernetes cluster version string (e.g., "v1.31.0"). Fetched
automatically from the /version endpoint when first accessed.
=head1 METHODS
=head2 new_object($class, \%attrs) or new_object($class, %attrs)
Create a new IO::K8s object. Accepts short class names (e.g., 'Pod', 'Namespace')
and either a hashref or a hash of attributes.
# With hashref
my $ns = $api->new_object(Namespace => { metadata => { name => 'foo' } });
# With hash
my $ns = $api->new_object(Namespace => metadata => { name => 'foo' });
=head2 list($class, %args)
List resources. Returns an L<IO::K8s::List>.
my $pods = $api->list('Pod', namespace => 'default');
=head2 get($class, %args)
Get a single resource by name.
my $pod = $api->get('Pod', name => 'my-pod', namespace => 'default');
=head2 create($object)
Create a resource from an IO::K8s object.
my $created = $api->create($pod);
=head2 update($object)
Update an existing resource.
my $updated = $api->update($pod);
=head2 patch($class_or_object, %args)
Partially update a resource. Unlike C<update()> which replaces the entire
object, C<patch()> only modifies the fields you specify.
# Add a label (strategic merge patch - default)
my $patched = $api->patch('Pod', 'my-pod',
namespace => 'default',
patch => { metadata => { labels => { env => 'staging' } } },
);
# Same thing with an object reference
my $patched = $api->patch($pod,
patch => { metadata => { labels => { env => 'staging' } } },
);
# Explicit patch type
my $patched = $api->patch('Deployment', 'my-app',
namespace => 'default',
type => 'merge',
patch => { spec => { replicas => 5 } },
);
lib/Kubernetes/REST.pm view on Meta::CPAN
=item namespace - Namespace (for namespaced resources)
=item container - Container name (for multi-container pods)
=item stdin, stdout, stderr, tty - Stream toggles (defaults: stdin=false, stdout=true, stderr=true, tty=false)
=item subprotocol - WebSocket subprotocol (default: C<v4.channel.k8s.io>)
=item on_open, on_frame, on_close, on_error - Duplex transport callbacks passed to IO backend
=back
Requires an IO backend implementing C<call_duplex>. The default sync backends
currently do not provide duplex transport.
=head2 attach($class, $name, %args)
Start a Kubernetes pod attach session via the C</attach> subresource.
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) = @_; ... },
on_close => sub { ... },
on_error => sub { my ($err) = @_; ... },
);
B<Required arguments:>
=over 4
=item name - Pod name
=back
B<Optional arguments:>
=over 4
=item namespace - Namespace (for namespaced resources)
=item container - Container name (for multi-container pods)
=item stdin, stdout, stderr, tty - Stream toggles (defaults: stdin=false, stdout=true, stderr=true, tty=false)
=item subprotocol - WebSocket subprotocol (default: C<v4.channel.k8s.io>)
=item on_open, on_frame, on_close, on_error - Duplex transport callbacks passed to IO backend
=back
Requires an IO backend implementing C<call_duplex>. The default sync backends
currently do not provide duplex transport.
=head2 fetch_resource_map()
Fetch the resource map from the cluster's OpenAPI spec (/openapi/v2 endpoint).
Returns a hashref mapping short resource names (e.g., "Pod") to full IO::K8s
class paths. This method is called automatically if C<resource_map_from_cluster>
is enabled.
=head1 BUILDING BLOCKS FOR ASYNC WRAPPERS
Async wrappers like L<Net::Async::Kubernetes> need access to the request/response
pipeline without going through the synchronous convenience methods. The following
public methods provide this:
=over 4
=item * C<expand_class($short)> - Resolve short name to full class
=item * C<build_path($class, %args)> - Build REST API URL path
=item * C<prepare_request($method, $path, %opts)> - Build HTTP request with auth
=item * C<check_response($response, $context)> - Validate HTTP status
=item * C<inflate_object($class, $response)> - JSON to typed object
=item * C<inflate_list($class, $response)> - JSON to typed list
=item * C<process_watch_chunk($class, \$buf, $chunk)> - Parse NDJSON watch stream
=item * C<process_log_chunk(\$buf, $chunk)> - Parse plain-text log stream
=back
Example async integration:
# Build request using Kubernetes::REST
my $class = $rest->expand_class('Pod');
my $path = $rest->build_path($class, name => $name, namespace => $ns) . '/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);
}
});
=head1 PLUGGABLE IO ARCHITECTURE
The HTTP transport is decoupled from request preparation and response
processing. This makes it possible to swap the default L<LWP::UserAgent>
backend for L<HTTP::Tiny> or an async backend (e.g. L<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
( run in 2.592 seconds using v1.01-cache-2.11-cpan-524268b4103 )