Kubernetes-REST

 view release on metacpan or  search on metacpan

lib/Kubernetes/REST.pm  view on Meta::CPAN


    my @events;
    while ($$buffer_ref =~ s/^([^\n]*)\n//) {
        my $line = $1;
        next unless length $line;

        my $data = eval { $self->_json->decode($line) };
        next unless $data;

        my $type = $data->{type} // '';
        my $raw_object = $data->{object} // {};

        # Track resourceVersion for resumability
        my $rv;
        if ($raw_object->{metadata} && $raw_object->{metadata}{resourceVersion}) {
            $rv = $raw_object->{metadata}{resourceVersion};
        }

        # Inflate the object (ERROR events stay as hashrefs)
        my $object;
        if ($type eq 'ERROR') {
            $object = $raw_object;
        } else {
            $object = eval { $self->k8s->struct_to_object($class, $raw_object) }
                // $raw_object;
        }

        push @events, {
            event => Kubernetes::REST::WatchEvent->new(
                type   => $type,
                object => $object,
                raw    => $raw_object,
            ),
            resourceVersion => $rv,
            is_error        => ($type eq 'ERROR' ? 1 : 0),
            error_code      => ($type eq 'ERROR' ? ($raw_object->{code} // 0) : 0),
        };
    }

    return @events;
}

sub _process_log_chunk {
    my ($self, $buffer_ref, $chunk) = @_;
    $$buffer_ref .= $chunk;

    my @events;
    while ($$buffer_ref =~ s/^([^\n]*)\n//) {
        my $line = $1;
        push @events, Kubernetes::REST::LogEvent->new(line => $line);
    }

    return @events;
}

# ============================================================================
# PUBLIC BUILDING BLOCKS FOR ASYNC WRAPPERS
#
# These methods expose the internal request/response pipeline as a stable API
# for async wrappers (e.g. Net::Async::Kubernetes) that need to build requests,
# process responses, and handle streaming without going through the sync
# convenience methods (list, get, watch, log, port_forward, exec, attach, etc.).
# ============================================================================

sub build_path {
    my ($self, @args) = @_;


    return $self->_build_path(@args);
}

sub prepare_request {
    my ($self, @args) = @_;


    return $self->_prepare_request(@args);
}

sub check_response {
    my ($self, @args) = @_;


    return $self->_check_response(@args);
}

sub inflate_object {
    my ($self, @args) = @_;


    return $self->_inflate_object(@args);
}

sub inflate_list {
    my ($self, @args) = @_;


    return $self->_inflate_list(@args);
}

sub process_watch_chunk {
    my ($self, @args) = @_;


    return $self->_process_watch_chunk(@args);
}

sub process_log_chunk {
    my ($self, @args) = @_;


    return $self->_process_log_chunk(@args);
}

# Convenience: prepare + call in one step (used by sync CRUD methods)
sub _request {
    my ($self, $method, $path, $body, %opts) = @_;
    my $req = $self->_prepare_request($method, $path,
        body => $body,
        %opts,
    );
    return $self->io->call($req);

lib/Kubernetes/REST.pm  view on Meta::CPAN

        (my $kind = ref $obj) =~ s/.*:://;
        my $key = join("\0", $kind, $obj->metadata->namespace // '');
        $expected{$key}{$obj->metadata->name} = 1;
    }

    for my $kind (@kinds) {
        for my $ns (@namespaces) {
            my %list_args = (labelSelector => $label);
            $list_args{namespace} = $ns if defined $ns;
            my $list = eval { $self->list($kind, %list_args) };
            next unless $list;
            for my $item (@{$list->items}) {
                my $item_ns = $item->metadata->namespace // '';
                my $key = join("\0", $kind, $item_ns);
                next if $expected{$key} && $expected{$key}{$item->metadata->name};
                eval { $self->delete($item) };
            }
        }
    }

    return @results;
}

sub watch {
    my ($self, $short_class, %args) = @_;


    my $on_event = delete $args{on_event}
        or croak "watch requires 'on_event' callback";
    my $timeout          = delete $args{timeout} // 300;
    my $resource_version = delete $args{resourceVersion};
    my $label_selector   = delete $args{labelSelector};
    my $field_selector   = delete $args{fieldSelector};

    my $class = $self->expand_class($short_class);
    my $path = $self->_build_path($class, %args);

    my %params = (
        watch          => 'true',
        timeoutSeconds => $timeout,
    );
    $params{resourceVersion} = $resource_version if defined $resource_version;
    $params{labelSelector}   = $label_selector   if defined $label_selector;
    $params{fieldSelector}   = $field_selector   if defined $field_selector;

    my $req = $self->_prepare_request('GET', $path, parameters => \%params);

    my $buffer = '';
    my $last_rv = $resource_version;
    my $got_410 = 0;

    my $data_callback = sub {
        my ($chunk) = @_;
        for my $result ($self->_process_watch_chunk($class, \$buffer, $chunk)) {
            $last_rv = $result->{resourceVersion} if $result->{resourceVersion};
            $got_410 = 1 if $result->{error_code} == 410;
            $on_event->($result->{event});
        }
    };

    my $response = $self->io->call_streaming($req, $data_callback);

    $self->_check_response($response, "watch $short_class");

    croak "Watch expired (410 Gone): resourceVersion too old, re-list to get a fresh resourceVersion"
        if $got_410;

    return $last_rv;
}

sub log {
    my ($self, $short_class, @rest) = @_;


    # Support: log('Pod', 'name', ...) and log('Pod', name => 'name', ...)
    my %args;
    if (@rest >= 1 && !ref($rest[0]) && $rest[0] !~ /^(name|namespace|container|follow|tailLines|sinceSeconds|sinceTime|timestamps|previous|limitBytes|on_line)$/) {
        $args{name} = shift @rest;
        %args = (%args, @rest);
    } elsif (@rest % 2 == 0) {
        %args = @rest;
    } else {
        croak "Invalid arguments to log()";
    }

    croak "name required for log" unless $args{name};

    my $on_line      = delete $args{on_line};
    my $container    = delete $args{container};
    my $follow       = delete $args{follow};
    my $tail_lines   = delete $args{tailLines};
    my $since_seconds = delete $args{sinceSeconds};
    my $since_time   = delete $args{sinceTime};
    my $timestamps   = delete $args{timestamps};
    my $previous     = delete $args{previous};
    my $limit_bytes  = delete $args{limitBytes};

    my $class = $self->expand_class($short_class);
    my $path = $self->_build_path($class, %args) . '/log';

    my %params;
    $params{container}    = $container     if defined $container;
    $params{follow}       = 'true'         if $follow;
    $params{tailLines}    = $tail_lines    if defined $tail_lines;
    $params{sinceSeconds} = $since_seconds if defined $since_seconds;
    $params{sinceTime}    = $since_time    if defined $since_time;
    $params{timestamps}   = 'true'         if $timestamps;
    $params{previous}     = 'true'         if $previous;
    $params{limitBytes}   = $limit_bytes   if defined $limit_bytes;

    if ($on_line) {
        # Streaming mode
        my $req = $self->_prepare_request('GET', $path, parameters => \%params);

        my $buffer = '';
        my $data_callback = sub {
            my ($chunk) = @_;
            for my $event ($self->_process_log_chunk(\$buffer, $chunk)) {
                $on_line->($event);
            }
        };

        my $response = $self->io->call_streaming($req, $data_callback);
        $self->_check_response($response, "log $short_class");

        # Process any remaining data in buffer (last line without trailing newline)
        if (length $buffer) {
            $on_line->(Kubernetes::REST::LogEvent->new(line => $buffer));
        }

        return;
    } else {
        # One-shot mode
        my $response = $self->_request('GET', $path, undef,
            %params ? (parameters => \%params) : (),
        );
        $self->_check_response($response, "log $short_class");

        return $response->content;
    }
}

sub port_forward {
    my ($self, $short_class, @rest) = @_;


    my %args;
    if (@rest >= 1 && !ref($rest[0]) && $rest[0] !~ /^(name|namespace|ports|subprotocol|on_open|on_frame|on_close|on_error)$/) {
        $args{name} = shift @rest;
        %args = (%args, @rest);
    } elsif (@rest % 2 == 0) {
        %args = @rest;
    } else {
        croak "Invalid arguments to port_forward()";
    }

    croak "name required for port_forward" unless $args{name};

    my $ports = delete $args{ports};
    croak "ports required for port_forward" unless defined $ports;
    $ports = [$ports] unless ref($ports) eq 'ARRAY';
    croak "ports required for port_forward" unless @$ports;
    for my $p (@$ports) {
        croak "invalid port '$p' for port_forward"
            unless defined($p) && $p =~ /^\d+$/ && $p > 0 && $p <= 65535;
    }

    my $subprotocol = delete $args{subprotocol} // 'v4.channel.k8s.io';
    my $on_open  = delete $args{on_open};
    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) . '/portforward';

    my $req = $self->_prepare_request('GET', $path,
        parameters => { ports => $ports },
        headers    => {
            Accept                 => '*/*',
            Connection             => 'Upgrade',
            Upgrade                => 'websocket',
            'Sec-WebSocket-Protocol' => $subprotocol,

lib/Kubernetes/REST.pm  view on Meta::CPAN

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);

Decode the JSON response body and inflate the C<items> array into an L<IO::K8s::List> of typed objects.

=head2 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 C<event> (L<Kubernetes::REST::WatchEvent>), C<resourceVersion>, C<is_error>, and C<error_code>.

This is a public API for async wrappers that handle streaming watch responses through their own event loop.

=head2 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 L<Kubernetes::REST::LogEvent> objects.

This is a public API for async wrappers that handle streaming log responses through their own event loop.

=head2 list

    my $list = $api->list('Pod', namespace => 'default');
    my $list = $api->list('Namespace', labelSelector => 'app=web');

List resources. Returns an L<IO::K8s::List> object.

Accepts short class names (C<Pod>) or full class paths. For namespaced resources, pass C<namespace> parameter. Omit C<namespace> to list cluster-scoped resources.

Supports C<labelSelector> and C<fieldSelector> query parameters for server-side filtering.

=head2 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 L<IO::K8s> object.

=head2 create

    my $created = $api->create($pod);

Create a resource from an L<IO::K8s> object. Returns the created object with server-assigned fields (UID, resourceVersion, etc.).

=head2 update

    my $updated = $api->update($pod);

Update an existing resource. Replaces the entire object server-side. Returns the updated object.

For partial updates, use L</patch> instead.

=head2 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' } } },
    );

Partially update a resource. Unlike C<update()> which replaces the entire object, C<patch()> only modifies specified fields.

Supports three patch strategies via the C<type> parameter:

=over 4

=item C<strategic> (default) - Strategic Merge Patch (Kubernetes-native, understands array merge semantics)

=item C<merge> - JSON Merge Patch (RFC 7396, simple recursive merge)

=item C<json> - JSON Patch (RFC 6902, array of operations)

=back

lib/Kubernetes/REST.pm  view on Meta::CPAN

    $api->ensure_only(
        label      => 'app.kubernetes.io/component=queen',
        objects    => \@objects,
        kinds      => [qw(Role RoleBinding ClusterRoleBinding)],
        namespaces => ['default', 'kube-system', undef],
    );

Like L</ensure_all>, but also B<deletes> any resources matching the label
selector in the given kinds and namespaces that are not present in C<objects>.
Use this for resources where stale objects must not survive (e.g. RBAC).

Pass C<undef> inside C<namespaces> to scan cluster-scoped resources. If
C<namespaces> is omitted, only cluster-scoped resources are scanned.

Returns the list of applied objects (from L</ensure_all>).

=head2 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.

Returns the last C<resourceVersion> seen. Croaks on 410 Gone (resourceVersion too old).

See L<Kubernetes::REST/watch> for detailed documentation and resumable watch patterns.

=head2 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:

B<One-shot> (without C<on_line>): Returns the full log text as a string.

B<Streaming> (with C<on_line>): Calls the callback for each log line with a L<Kubernetes::REST::LogEvent> object. Blocks until the stream ends (or the server closes the connection).

The streaming mode is designed for event-based systems like L<IO::Async> — see L<Net::Async::Kubernetes> for async integration.

=head2 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.

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).

=head2 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 C</exec> 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).

=head2 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 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).

lib/Kubernetes/REST.pm  view on Meta::CPAN

=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' });

lib/Kubernetes/REST.pm  view on Meta::CPAN

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

For watch, step 2 uses C<io-E<gt>call_streaming()> and step 4 uses
C<process_watch_chunk()> which parses NDJSON and inflates each event.

For log, step 2 uses C<io-E<gt>call_streaming()> and step 4 uses
C<process_log_chunk()> which parses plain-text lines into L<Kubernetes::REST::LogEvent> objects.

To implement a custom IO backend, consume L<Kubernetes::REST::Role::IO>
and implement C<call($req)> and C<call_streaming($req, $callback)>.
See L<Kubernetes::REST::LWPIO> and L<Kubernetes::REST::HTTPTinyIO> for
reference implementations.

=head1 SEE ALSO

=head2 Related Modules

=over

=item * L<IO::K8s> - Kubernetes resource classes (required dependency)

=item * L<Net::Async::Kubernetes> - Async Kubernetes client for L<IO::Async>

=back

=head2 Configuration and Authentication

=over

=item * L<Kubernetes::REST::Kubeconfig> - Load settings from kubeconfig

=item * L<Kubernetes::REST::Server> - Server connection configuration

=item * L<Kubernetes::REST::AuthToken> - Authentication credentials

=back

=head2 HTTP Backends

=over

=item * L<Kubernetes::REST::Role::IO> - IO interface role

=item * L<Kubernetes::REST::LWPIO> - LWP::UserAgent backend (default)

=item * L<Kubernetes::REST::HTTPTinyIO> - HTTP::Tiny backend

=item * L<LWP::ConsoleLogger> - HTTP debugging for LWPIO

=back

=head2 Data Objects

=over

=item * L<Kubernetes::REST::WatchEvent> - Watch event object

=item * L<Kubernetes::REST::LogEvent> - Log event object

=item * L<Kubernetes::REST::HTTPRequest> - HTTP request object

=item * L<Kubernetes::REST::HTTPResponse> - HTTP response object

=back

=head2 CLI Tools

=over

=item * L<Kubernetes::REST::CLI> - CLI base class



( run in 0.974 second using v1.01-cache-2.11-cpan-600a1bdf6e4 )