Kubernetes-REST

 view release on metacpan or  search on metacpan

Changes  view on Meta::CPAN

   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.

Changes  view on Meta::CPAN

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

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

    }

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

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


    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 {

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

        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

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

    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.

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

            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.

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


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(

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

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)

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


sub execute {
    my ($self, $args, $chain) = @_;
    print "Usage: kube_client <command> [options]\n\n";
    print "Commands:\n";
    print "  get <Kind> [name]     Get resource(s)\n";
    print "  create -f <file>      Create resource from file\n";
    print "  delete <Kind> <name>  Delete resource\n";
    print "  raw <Group> <Method>  Raw API call\n";
    print "\nRun 'kube_client --help' for options.\n";
    print "See also: kube_watch <Kind> for live event streaming.\n";
    return 0;
}



1;

package Kubernetes::REST::CLI::Cmd::Get;
our $VERSION = '1.003';
use Moo;

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

        (defined $req->content) ? (content => $req->content) : (),
      }
    );

    return Kubernetes::REST::HTTPResponse->new(
       status => $res->{ status },
       (defined $res->{ content })?( content => $res->{ content } ) : (),
    );
  }

sub call_streaming {
    my ($self, $req, $data_callback) = @_;


    my $res = $self->ua->request(
      $req->method,
      $req->url,
      {
        headers => $req->headers,
        data_callback => $data_callback,
      }

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

=head2 ua

The underlying L<HTTP::Tiny> instance.

=head2 call

    my $response = $io->call($req);

Execute an HTTP request. Receives a fully prepared L<Kubernetes::REST::HTTPRequest> (URL, headers, content all set). Returns a L<Kubernetes::REST::HTTPResponse>.

=head2 call_streaming

    my $response = $io->call_streaming($req, sub { my ($chunk) = @_; ... });

Execute an HTTP request with streaming response. The C<$data_callback> is called with each chunk of data as it arrives.

Used internally by L<Kubernetes::REST/watch> for the Watch API.

=head1 SEE ALSO

=over

=item * L<Kubernetes::REST> - Main API client

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

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

    );

    my $res = $self->ua->request($http_req);

    return Kubernetes::REST::HTTPResponse->new(
       status => $res->code,
       (length $res->decoded_content) ? ( content => $res->decoded_content ) : (),
    );
  }

sub call_streaming {
    my ($self, $req, $data_callback) = @_;


    my $http_req = HTTP::Request->new(
      $req->method,
      $req->url,
      [ %{$req->headers} ],
    );

    my $res = $self->ua->request($http_req, sub {

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

=head2 ua

The underlying L<LWP::UserAgent> instance. Access this to attach middleware such as L<LWP::ConsoleLogger> for HTTP debugging.

=head2 call

    my $response = $io->call($req);

Execute an HTTP request. Receives a fully prepared L<Kubernetes::REST::HTTPRequest> (URL, headers, content all set). Returns a L<Kubernetes::REST::HTTPResponse>.

=head2 call_streaming

    my $response = $io->call_streaming($req, sub { my ($chunk) = @_; ... });

Execute an HTTP request with streaming response. The C<$data_callback> is called with each chunk of data as it arrives.

Used internally by L<Kubernetes::REST/watch> for the Watch API.

=head1 SEE ALSO

=over

=item * L<Kubernetes::REST> - Main API client

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

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

package Kubernetes::REST::Role::IO;
our $VERSION = '1.104';
# ABSTRACT: Interface role for HTTP backends
use Moo::Role;


requires 'call';


requires 'call_streaming';


sub supports_duplex {
    my ($self) = @_;
    return $self->can('call_duplex') ? 1 : 0;
}


1;

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

    package My::AsyncIO;
    use Moo;
    with 'Kubernetes::REST::Role::IO';

    sub call {
        my ($self, $req) = @_;
        # Execute HTTP request, return Kubernetes::REST::HTTPResponse
        ...
    }

    sub call_streaming {
        my ($self, $req, $data_callback) = @_;
        # Execute HTTP request with streaming callback
        ...
    }

    # Optional: full-duplex transport (WebSocket/SPDY)
    sub call_duplex {
        my ($self, $req, %callbacks) = @_;
        ...
    }

=head1 DESCRIPTION

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

The default backend is L<Kubernetes::REST::LWPIO> (using L<LWP::UserAgent>). An alternative L<Kubernetes::REST::HTTPTinyIO> (using L<HTTP::Tiny>) is provided. To use an async event loop, implement this role with e.g. L<Net::Async::HTTP>.

=head2 call

    my $response = $io->call($req);

Required. Execute an HTTP request. Receives a L<Kubernetes::REST::HTTPRequest> with C<method>, C<url>, C<headers>, and optionally C<content> already set.

Must return a L<Kubernetes::REST::HTTPResponse> with C<status> and C<content>.

=head2 call_streaming

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

Required. Execute an HTTP request with streaming response. The C<$data_callback> is called with each chunk of data as it arrives: C<< $data_callback->($chunk) >>.

Must return a L<Kubernetes::REST::HTTPResponse> when the stream ends.

=head2 supports_duplex

    if ($io->supports_duplex) {
        ...
    }

Optional capability probe for full-duplex protocols used by Kubernetes

t/13_io_backends.t  view on Meta::CPAN


    my $res = $io->call($req);
    is $res->status, 201, 'POST returns 201';
    like $res->content, qr/uid/, 'response contains uid';

    # Verify the request body was passed
    is $mock_ua->last_request->content, '{"metadata":{"name":"test"}}',
        'request body preserved';
};

subtest 'LWPIO - call_streaming() with data callback' => sub {
    my $io = Kubernetes::REST::LWPIO->new;

    # For streaming, LWP calls the callback with chunks
    my $mock_ua = Test::MockLWP->new(
        code => 200,
        content => '',
        streaming_chunks => [
            qq|{"type":"ADDED","object":{"kind":"Pod","metadata":{"name":"pod-1"}}}\n|,
            qq|{"type":"MODIFIED","object":{"kind":"Pod","metadata":{"name":"pod-1"}}}\n|,
        ],
    );
    $io->{ua} = $mock_ua;

    my $req = Kubernetes::REST::HTTPRequest->new(
        method => 'GET',
        url => 'http://mock.local/api/v1/namespaces/default/pods?watch=true',
        headers => { Authorization => 'Bearer test-token' },
    );

    my @chunks;
    my $res = $io->call_streaming($req, sub { push @chunks, $_[0] });

    isa_ok $res, 'Kubernetes::REST::HTTPResponse';
    is $res->status, 200, 'streaming returns 200';
    is scalar @chunks, 2, 'received 2 chunks';
    like $chunks[0], qr/ADDED/, 'first chunk is ADDED';
    like $chunks[1], qr/MODIFIED/, 'second chunk is MODIFIED';
};

subtest 'LWPIO - call() with empty response' => sub {
    my $io = Kubernetes::REST::LWPIO->new;

    my $mock_ua = Test::MockLWP->new(
        code => 204,

t/13_io_backends.t  view on Meta::CPAN

    my $req = Kubernetes::REST::HTTPRequest->new(
        method => 'GET',
        url => 'http://mock.local/api/v1/pods',
        headers => {},
    );

    my $res = $io->call($req);
    is $res->status, 200, 'status ok with no content';
};

subtest 'HTTPTinyIO - call_streaming() with data callback' => sub {
    my $io = Kubernetes::REST::HTTPTinyIO->new;

    my @delivered_chunks;
    my $mock_ua = Test::MockHTTPTiny->new(
        status => 200,
        content => undef,
        on_data_callback => sub {
            my ($cb) = @_;
            $cb->(qq|{"type":"ADDED","object":{"kind":"Pod"}}\n|);
            $cb->(qq|{"type":"DELETED","object":{"kind":"Pod"}}\n|);

t/13_io_backends.t  view on Meta::CPAN

    );
    $io->{ua} = $mock_ua;

    my $req = Kubernetes::REST::HTTPRequest->new(
        method => 'GET',
        url => 'http://mock.local/api/v1/pods?watch=true',
        headers => {},
    );

    my @chunks;
    my $res = $io->call_streaming($req, sub { push @chunks, $_[0] });
    is $res->status, 200, 'streaming returns 200';
    is scalar @chunks, 2, 'received 2 chunks';
    like $chunks[0], qr/ADDED/, 'first chunk is ADDED';
    like $chunks[1], qr/DELETED/, 'second chunk is DELETED';
};

# ============================================================================
# Test that REST.pm uses LWPIO by default
# ============================================================================

subtest 'REST default IO is LWPIO' => sub {

t/13_io_backends.t  view on Meta::CPAN


package Test::MockLWP;
use strict;
use warnings;

sub new {
    my ($class, %args) = @_;
    bless {
        code => $args{code} // 200,
        content => $args{content} // '',
        streaming_chunks => $args{streaming_chunks} // [],
        last_request => undef,
    }, $class;
}

sub request {
    my ($self, $req, $content_cb) = @_;
    $self->{last_request} = $req;

    if ($content_cb && ref $content_cb eq 'CODE') {
        # Streaming mode: deliver chunks via callback
        for my $chunk (@{$self->{streaming_chunks}}) {
            $content_cb->($chunk);
        }
    }

    return Test::MockLWP::Response->new(
        code => $self->{code},
        content => $self->{content},
    );
}

t/13_io_backends.t  view on Meta::CPAN

        content => $args{content},
        on_data_callback => $args{on_data_callback},
        last_opts => undef,
    }, $class;
}

sub request {
    my ($self, $method, $url, $opts) = @_;
    $self->{last_opts} = $opts // {};

    # If there's a data_callback in opts and we have streaming setup, invoke it
    if ($opts->{data_callback} && $self->{on_data_callback}) {
        $self->{on_data_callback}->($opts->{data_callback});
    }

    return {
        status => $self->{status},
        (defined $self->{content} ? (content => $self->{content}) : ()),
    };
}

t/20_log.t  view on Meta::CPAN

    ]);

    my $text = $api->log('Pod', 'nginx-abc', namespace => 'default');

    like($text, qr/Starting nginx/, 'log contains first line');
    like($text, qr/Listening on port 80/, 'log contains second line');
    like($text, qr/Ready to accept connections/, 'log contains third line');
};

# === Test 2: Streaming log with on_line callback ===
subtest 'streaming log' => sub {
    $mock_io->add_log_lines('/api/v1/namespaces/default/pods/nginx-abc/log', [
        'line 1: hello',
        'line 2: world',
        'line 3: done',
    ]);

    my @events;
    $api->log('Pod', 'nginx-abc',
        namespace => 'default',
        follow    => 1,

t/20_log.t  view on Meta::CPAN

    ]);

    my $text = $api->log('Pod', 'big-pod',
        namespace  => 'default',
        limitBytes => 1024,
    );

    like($text, qr/limited output/, 'limitBytes log works');
};

# === Test 18: process_log_chunk with multiple chunks simulating real streaming ===
subtest 'process_log_chunk multi-chunk streaming' => sub {
    my $buffer = '';

    # First chunk: one complete line + start of second
    my @events = $api->process_log_chunk(\$buffer, "complete line\npartial li");
    is(scalar @events, 1, 'one complete line from first chunk');
    is($events[0]->line, 'complete line', 'first chunk complete line');
    is($buffer, 'partial li', 'buffer holds partial');

    # Second chunk: rest of second line + third line
    @events = $api->process_log_chunk(\$buffer, "ne here\nthird line\n");

t/20_log.t  view on Meta::CPAN

    my $json_line = '{"type":"ADDED","object":{"apiVersion":"v1","kind":"Pod","metadata":{"name":"watch-pod","namespace":"default","resourceVersion":"999"},"spec":{"containers":[{"name":"nginx","image":"nginx"}]},"status":{"phase":"Running"}}}' . "\n...

    my @results = $api->process_watch_chunk($class, \$buffer, $json_line);
    is(scalar @results, 1, 'one watch event from chunk');
    is($results[0]->{event}->type, 'ADDED', 'event type is ADDED');
    is($results[0]->{resourceVersion}, '999', 'resourceVersion tracked');
    isa_ok($results[0]->{event}->object, 'IO::K8s::Api::Core::V1::Pod', 'inflated to Pod');
    is($results[0]->{event}->object->metadata->name, 'watch-pod', 'pod name correct');
};

# === Test 24: log streaming returns undef ===
subtest 'log streaming returns undef' => sub {
    $mock_io->add_log_lines('/api/v1/namespaces/default/pods/void-pod/log', [
        'some output',
    ]);

    my $result = $api->log('Pod', 'void-pod',
        namespace => 'default',
        on_line   => sub {},
    );
    is($result, undef, 'streaming log returns undef');
};

# === Test 25: log one-shot returns string ===
subtest 'log one-shot returns string' => sub {
    $mock_io->add_log_lines('/api/v1/namespaces/default/pods/string-pod/log', [
        'line A',
        'line B',
    ]);

    my $text = $api->log('Pod', 'string-pod', namespace => 'default');

t/21_port_forward.t  view on Meta::CPAN


{
    package Test::PF::BasicIO;
    use Moo;
    with 'Kubernetes::REST::Role::IO';

    sub call {
        return Kubernetes::REST::HTTPResponse->new(status => 200, content => '{}');
    }

    sub call_streaming {
        return Kubernetes::REST::HTTPResponse->new(status => 200, content => '');
    }
}

{
    package Test::PF::DuplexIO;
    use Moo;
    with 'Kubernetes::REST::Role::IO';

    has last_req  => (is => 'rw');
    has last_opts => (is => 'rw');

    sub call {
        return Kubernetes::REST::HTTPResponse->new(status => 200, content => '{}');
    }

    sub call_streaming {
        return Kubernetes::REST::HTTPResponse->new(status => 200, content => '');
    }

    sub call_duplex {
        my ($self, $req, %opts) = @_;
        $self->last_req($req);
        $self->last_opts(\%opts);
        return { ok => 1, type => 'duplex-session' };
    }
}

t/22_exec.t  view on Meta::CPAN


{
    package Test::Exec::BasicIO;
    use Moo;
    with 'Kubernetes::REST::Role::IO';

    sub call {
        return Kubernetes::REST::HTTPResponse->new(status => 200, content => '{}');
    }

    sub call_streaming {
        return Kubernetes::REST::HTTPResponse->new(status => 200, content => '');
    }
}

{
    package Test::Exec::DuplexIO;
    use Moo;
    with 'Kubernetes::REST::Role::IO';

    has last_req  => (is => 'rw');
    has last_opts => (is => 'rw');

    sub call {
        return Kubernetes::REST::HTTPResponse->new(status => 200, content => '{}');
    }

    sub call_streaming {
        return Kubernetes::REST::HTTPResponse->new(status => 200, content => '');
    }

    sub call_duplex {
        my ($self, $req, %opts) = @_;
        $self->last_req($req);
        $self->last_opts(\%opts);
        return { ok => 1, type => 'duplex-session' };
    }
}

t/23_attach.t  view on Meta::CPAN


{
    package Test::Attach::BasicIO;
    use Moo;
    with 'Kubernetes::REST::Role::IO';

    sub call {
        return Kubernetes::REST::HTTPResponse->new(status => 200, content => '{}');
    }

    sub call_streaming {
        return Kubernetes::REST::HTTPResponse->new(status => 200, content => '');
    }
}

{
    package Test::Attach::DuplexIO;
    use Moo;
    with 'Kubernetes::REST::Role::IO';

    has last_req  => (is => 'rw');
    has last_opts => (is => 'rw');

    sub call {
        return Kubernetes::REST::HTTPResponse->new(status => 200, content => '{}');
    }

    sub call_streaming {
        return Kubernetes::REST::HTTPResponse->new(status => 200, content => '');
    }

    sub call_duplex {
        my ($self, $req, %opts) = @_;
        $self->last_req($req);
        $self->last_opts(\%opts);
        return { ok => 1, type => 'duplex-session' };
    }
}

t/lib/Test/Kubernetes/Mock.pm  view on Meta::CPAN

sub add_watch_events {
    my ($self, $path, $events) = @_;
    $self->watch_events->{$path} = $events;
}

sub add_log_lines {
    my ($self, $path, $lines) = @_;
    $self->log_lines->{$path} = $lines;
}

sub call_streaming {
    my ($self, $req, $callback) = @_;

    my $path = $req->url // '';
    $path =~ s{^https?://[^/]+}{};
    # Strip query parameters for key lookup
    $path =~ s{\?.*}{};

    # Check for log lines first (log paths end with /log)
    if (my $lines = $self->log_lines->{$path}) {
        for my $line (@$lines) {

t/lib/Test/Kubernetes/Mock.pm  view on Meta::CPAN

            status => 200,
        );
    }

    # Check for watch events
    my $events = $self->watch_events->{$path};

    unless ($events) {
        return Test::Kubernetes::Mock::Response->new(
            status => 404,
            content => '{"kind":"Status","status":"Failure","message":"no streaming data for path"}',
        );
    }

    my $json = JSON::MaybeXS->new;
    for my $event (@$events) {
        my $line = $json->encode($event) . "\n";
        $callback->($line, undef);
    }

    return Test::Kubernetes::Mock::Response->new(



( run in 2.046 seconds using v1.01-cache-2.11-cpan-600a1bdf6e4 )