Kubernetes-REST

 view release on metacpan or  search on metacpan

.claude/skills/perl-kubernetes-rest/SKILL.md  view on Meta::CPAN

allowed-tools: Read, Grep, Glob
model: sonnet
---

# Kubernetes::REST — K8s API Client for Perl

## Connect

```perl
my $api = Kubernetes::REST->new(
    server      => { endpoint => 'https://k8s.local:6443' },
    credentials => { token => $token },
);

# With SSL options
my $api = Kubernetes::REST->new(
    server => {
        endpoint          => 'https://k8s.local:6443',
        ssl_verify_server => 1,
        ssl_ca_file       => '/path/to/ca.crt',
    },
    credentials => { token => $token },
);
```

## CRUD

```perl

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

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

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

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

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

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

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

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.

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


=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

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


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

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


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

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


    # Verify
    kubectl get nodes

B<Key differences from Minikube:>

=over 4

=item * Kubeconfig lives at C</etc/rancher/k3s/k3s.yaml> (not C<~/.kube/config>)

=item * The server endpoint is C<https://127.0.0.1:6443>

=item * K3s includes Traefik as its default ingress controller

=item * K3s uses C<containerd> instead of Docker

=item * NodePort services are accessible directly on the host IP

=back

B<Connecting from Perl with K3s:>

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


=head1 INSTALL PERL DEPENDENCIES

    cpanm Kubernetes::REST

=head1 CONNECTING TO THE CLUSTER

=head2 Using the kubeconfig (recommended)

L<Kubernetes::REST::Kubeconfig> parses your kubeconfig and sets up
authentication, SSL certificates, and the server endpoint automatically:

    use Kubernetes::REST::Kubeconfig;

    my $kc  = Kubernetes::REST::Kubeconfig->new;
    my $api = $kc->api;

    say "Cluster version: " . $api->cluster_version;

If you have multiple contexts:

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

    say "Available: @$contexts";

    my $api = $kc->api('minikube');  # or 'default', 'kind-test', etc.

=head2 Manual configuration

    use Kubernetes::REST;

    my $api = Kubernetes::REST->new(
        server => {
            endpoint          => 'https://127.0.0.1:6443',
            ssl_verify_server => 0,
        },
        credentials => { token => $token },
        resource_map_from_cluster => 0,
    );

=head1 LISTING RESOURCES

=head2 Namespaces

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

=head2 Registering the CRD class

Register your class with the C<+> prefix in the resource map:

    my $api = Kubernetes::REST::Kubeconfig->new->api;
    $api->resource_map->{StaticWebSite} = '+My::StaticWebSite';

Or pass it at construction time:

    my $api = Kubernetes::REST->new(
        server      => { endpoint => '...' },
        credentials => { token => '...' },
        resource_map => {
            %{ IO::K8s->default_resource_map },
            StaticWebSite => '+My::StaticWebSite',
        },
    );

=head2 CRUD on Custom Resources

Once registered, CRDs work exactly like built-in resources:

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



has uri => (is => 'rw', isa => Str);


has method => (is => 'rw', isa => Str);


has url => (is => 'rw', isa => Str, lazy => 1, default => sub {
    my $self = shift;
    return $self->server->endpoint . $self->uri if $self->server;
    return '';
});


has headers => (is => 'rw', isa => HashRef, default => sub { {} });


has parameters => (is => 'rw', isa => HashRef);


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

                   . " and not running in-cluster";
    }

    $context_name //= $self->current_context_name;
    my $ctx = $self->context($context_name);
    my $cluster = $self->cluster($ctx->{cluster});
    my $user = $self->user($ctx->{user});

    # Build server config
    my %server = (
        endpoint => $cluster->{server},
    );

    if (my %ca = $self->_resolve_cert($cluster, 'certificate-authority')) {
        $server{ $ca{pem} ? 'ssl_ca_pem' : 'ssl_ca_file' } = $ca{pem} // $ca{file};
    }

    if ($cluster->{'insecure-skip-tls-verify'}) {
        $server{ssl_verify_server} = 0;
    } else {
        $server{ssl_verify_server} = 1;

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

    return undef unless -f $SA_TOKEN;

    my $host = $ENV{KUBERNETES_SERVICE_HOST} // 'kubernetes.default.svc';
    my $port = $ENV{KUBERNETES_SERVICE_PORT} // '443';
    open my $fh, '<', $SA_TOKEN or croak "Cannot read $SA_TOKEN: $!";
    my $token = do { local $/; <$fh> };
    chomp $token;

    return Kubernetes::REST->new(
        server => Kubernetes::REST::Server->new(
            endpoint          => "https://$host:$port",
            ssl_ca_file       => $SA_CA,
            ssl_verify_server => 1,
        ),
        credentials => Kubernetes::REST::AuthToken->new(token => $token),
    );
}

1;

__END__

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

package Kubernetes::REST::Server;
our $VERSION = '1.104';
# ABSTRACT: Kubernetes API server connection configuration
use Moo;
use Types::Standard qw/Str Bool/;


has endpoint => (is => 'ro', isa => Str, required => 1);


has ssl_verify_server => (is => 'ro', isa => Bool, default => 1);


has ssl_cert_file => (is => 'ro');


has ssl_cert_pem => (is => 'ro');

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


=head1 VERSION

version 1.104

=head1 SYNOPSIS

    use Kubernetes::REST::Server;

    my $server = Kubernetes::REST::Server->new(
        endpoint => 'https://kubernetes.local:6443',
        ssl_verify_server => 1,
        ssl_ca_file => '/path/to/ca.crt',
    );

=head1 DESCRIPTION

Configuration object for Kubernetes API server connection details.

=head2 endpoint

Required. The Kubernetes API server endpoint URL (e.g., C<https://kubernetes.local:6443>).

=head2 ssl_verify_server

Boolean. Whether to verify the server's SSL certificate. Defaults to C<1> (true).

Set to C<0> for development clusters with self-signed certificates.

=head2 ssl_cert_file

Optional. Path to client certificate file for mTLS authentication.

t/03_params2request.t  view on Meta::CPAN

use warnings;

use Test::More;
use Kubernetes::REST;
use Kubernetes::REST::Server;
use Kubernetes::REST::AuthToken;

# Test _build_path functionality
{
    my $api = Kubernetes::REST->new(
        server => Kubernetes::REST::Server->new(endpoint => 'http://example.com'),
        credentials => Kubernetes::REST::AuthToken->new(token => 'FakeToken'),
        resource_map_from_cluster => 0,  # Use IO::K8s defaults for testing
    );

    # Test expand_class with short names
    is($api->expand_class('Pod'), 'IO::K8s::Api::Core::V1::Pod', 'Pod expands correctly');
    is($api->expand_class('Deployment'), 'IO::K8s::Api::Apps::V1::Deployment', 'Deployment expands correctly');
    is($api->expand_class('Node'), 'IO::K8s::Api::Core::V1::Node', 'Node expands correctly');

    # Test with relative path

t/03_params2request.t  view on Meta::CPAN

}

# Test resource_map customization
{
    my $custom_map = {
        Pod => 'Api::Core::V1::Pod',
        MyCustomResource => 'Api::Custom::V1::MyCustomResource',
    };

    my $api = Kubernetes::REST->new(
        server => Kubernetes::REST::Server->new(endpoint => 'http://example.com'),
        credentials => Kubernetes::REST::AuthToken->new(token => 'FakeToken'),
        resource_map => $custom_map,
    );

    is($api->expand_class('MyCustomResource'), 'IO::K8s::Api::Custom::V1::MyCustomResource', 'Custom resource map works');
}

# Test _build_path pluralization rules
{
    my $api = Kubernetes::REST->new(
        server => Kubernetes::REST::Server->new(endpoint => 'http://example.com'),
        credentials => Kubernetes::REST::AuthToken->new(token => 'FakeToken'),
        resource_map_from_cluster => 0,
    );

    # Simple +s: pod -> pods
    my $path = $api->_build_path('IO::K8s::Api::Core::V1::Pod');
    like($path, qr{/pods$}, 'Pod -> pods (simple +s)');

    # Simple +s: node -> nodes
    $path = $api->_build_path('IO::K8s::Api::Core::V1::Node');

t/04_response2result.t  view on Meta::CPAN

use Test::More;
use Test::Exception;
use Kubernetes::REST;
use Kubernetes::REST::Server;
use Kubernetes::REST::AuthToken;

# Test that error responses are handled correctly
# (actual API calls would require a real cluster, so we just test setup)
{
    my $api = Kubernetes::REST->new(
        server => Kubernetes::REST::Server->new(endpoint => 'http://example.com'),
        credentials => Kubernetes::REST::AuthToken->new(token => 'FakeToken'),
        resource_map_from_cluster => 0,
    );

    # Verify API object is created correctly
    ok($api, 'API object created');
    is($api->server->endpoint, 'http://example.com', 'Server endpoint set correctly');
    is($api->credentials->token, 'FakeToken', 'Credentials set correctly');
}

# Test error handling for missing parameters
{
    my $api = Kubernetes::REST->new(
        server => Kubernetes::REST::Server->new(endpoint => 'http://example.com'),
        credentials => Kubernetes::REST::AuthToken->new(token => 'FakeToken'),
        resource_map_from_cluster => 0,
    );

    throws_ok(
        sub { $api->get('Pod') },
        qr/name required/,
        'get without name throws error'
    );

t/06_examples.t  view on Meta::CPAN

use Test::More;
use FindBin;
use lib "$FindBin::Bin/../lib";

use Kubernetes::REST;
use MIME::Base64 qw(encode_base64);

# Create a Kubernetes::REST instance for new_object (no server needed)
my $api = Kubernetes::REST->new(
    server => {
        endpoint          => 'https://127.0.0.1:6443',
        ssl_verify_server => 0,
    },
    credentials              => { token => 'test' },
    resource_map_from_cluster => 0,
);

ok($api, 'API object created');

# === Namespace ===
subtest 'Example: Namespace' => sub {

t/08_crd.t  view on Meta::CPAN

    $api->resource_map->{StaticWebSite} = '+My::StaticWebSite';
} else {
    diag "Running with MOCK responses";
    # Build mock API with CRD in resource map
    require Kubernetes::REST;
    require Kubernetes::REST::Server;
    require Kubernetes::REST::AuthToken;

    my $default_map = IO::K8s->default_resource_map;
    $api = Kubernetes::REST->new(
        server => Kubernetes::REST::Server->new(endpoint => 'http://mock.local'),
        credentials => Kubernetes::REST::AuthToken->new(token => 'MockToken'),
        resource_map_from_cluster => 0,
        resource_map => {
            %$default_map,
            StaticWebSite => '+My::StaticWebSite',
        },
        io => Test::Kubernetes::Mock::IO->new,
    );
}

t/09_crd_autogen.t  view on Meta::CPAN

    }
    $api->resource_map->{StaticWebSiteAG} = "+$crd_class";
} else {
    diag "Running with MOCK responses";
    require Kubernetes::REST;
    require Kubernetes::REST::Server;
    require Kubernetes::REST::AuthToken;

    my $default_map = IO::K8s->default_resource_map;
    $api = Kubernetes::REST->new(
        server => Kubernetes::REST::Server->new(endpoint => 'http://mock.local'),
        credentials => Kubernetes::REST::AuthToken->new(token => 'MockToken'),
        resource_map_from_cluster => 0,
        resource_map => {
            %$default_map,
            StaticWebSiteAG => "+$crd_class",
        },
        io => Test::Kubernetes::Mock::IO->new,
    );
}

t/10_watch.t  view on Meta::CPAN

use Kubernetes::REST::AuthToken;
use Kubernetes::REST::WatchEvent;
use IO::K8s;

my $container = { name => 'nginx', image => 'nginx:1.27' };

# Build a mock API with watch-capable IO
my $mock_io = Test::Kubernetes::Mock::IO->new;

my $api = Kubernetes::REST->new(
    server      => Kubernetes::REST::Server->new(endpoint => 'http://mock.local'),
    credentials => Kubernetes::REST::AuthToken->new(token => 'MockToken'),
    resource_map_from_cluster => 0,
    io          => $mock_io,
);

# === Test 1: Watch with ADDED/MODIFIED/DELETED events ===
subtest 'watch pod events' => sub {
    $mock_io->add_watch_events('/api/v1/namespaces/default/pods', [
        {
            type   => 'ADDED',

t/10_watch.t  view on Meta::CPAN

};

# === Test 4: Watch CRD class ===
subtest 'watch CRD class' => sub {
    require My::StaticWebSite;

    # Build API with CRD registered
    my $crd_io = Test::Kubernetes::Mock::IO->new;
    my $default_map = IO::K8s->default_resource_map;
    my $crd_api = Kubernetes::REST->new(
        server      => Kubernetes::REST::Server->new(endpoint => 'http://mock.local'),
        credentials => Kubernetes::REST::AuthToken->new(token => 'MockToken'),
        resource_map_from_cluster => 0,
        resource_map => {
            %$default_map,
            StaticWebSite => '+My::StaticWebSite',
        },
        io => $crd_io,
    );

    $crd_io->add_watch_events('/apis/homelab.example.com/v1/namespaces/default/staticwebsites', [

t/12_patch.t  view on Meta::CPAN

use Test::Kubernetes::Mock qw(mock_api);
use Kubernetes::REST;
use Kubernetes::REST::Server;
use Kubernetes::REST::AuthToken;
use IO::K8s;

# Build a mock API with programmable responses
my $mock_io = Test::Kubernetes::Mock::IO->new;

my $api = Kubernetes::REST->new(
    server      => Kubernetes::REST::Server->new(endpoint => 'http://mock.local'),
    credentials => Kubernetes::REST::AuthToken->new(token => 'MockToken'),
    resource_map_from_cluster => 0,
    io          => $mock_io,
);

# Simulated patched deployment response
my $deploy_response = {
    apiVersion => 'apps/v1',
    kind       => 'Deployment',
    metadata   => {

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

# ============================================================================

subtest 'REST default IO is LWPIO' => sub {
    use Test::Kubernetes::Mock qw(mock_api);
    # The mock API uses Mock::IO, so test by creating a non-mock one
    require Kubernetes::REST;
    require Kubernetes::REST::Server;
    require Kubernetes::REST::AuthToken;

    my $api = Kubernetes::REST->new(
        server => Kubernetes::REST::Server->new(endpoint => 'http://test.local'),
        credentials => Kubernetes::REST::AuthToken->new(token => 'test'),
        resource_map_from_cluster => 0,
    );
    isa_ok $api->io, 'Kubernetes::REST::LWPIO', 'default IO is LWPIO';
};

subtest 'REST allows HTTPTinyIO override' => sub {
    require Kubernetes::REST;
    require Kubernetes::REST::Server;
    require Kubernetes::REST::AuthToken;
    require Kubernetes::REST::HTTPTinyIO;

    my $api = Kubernetes::REST->new(
        server => Kubernetes::REST::Server->new(endpoint => 'http://test.local'),
        credentials => Kubernetes::REST::AuthToken->new(token => 'test'),
        resource_map_from_cluster => 0,
        io => Kubernetes::REST::HTTPTinyIO->new,
    );
    isa_ok $api->io, 'Kubernetes::REST::HTTPTinyIO', 'can override with HTTPTinyIO';
};

done_testing;

# ============================================================================

t/14_kubeconfig.t  view on Meta::CPAN


subtest 'context - not found' => sub {
    my $kc = Kubernetes::REST::Kubeconfig->new(kubeconfig_path => $config_file);
    throws_ok { $kc->context('nonexistent') } qr/Context not found: nonexistent/,
        'throws for unknown context';
};

subtest 'cluster - lookup by name' => sub {
    my $kc = Kubernetes::REST::Kubeconfig->new(kubeconfig_path => $config_file);
    my $cluster = $kc->cluster('prod-cluster');
    is $cluster->{server}, 'https://prod.k8s.local:6443', 'cluster server endpoint';
    is $cluster->{'certificate-authority'}, $ca_file, 'cluster CA file';
};

subtest 'cluster - not found' => sub {
    my $kc = Kubernetes::REST::Kubeconfig->new(kubeconfig_path => $config_file);
    throws_ok { $kc->cluster('nonexistent') } qr/Cluster not found/,
        'throws for unknown cluster';
};

subtest 'user - token user' => sub {

t/14_kubeconfig.t  view on Meta::CPAN

subtest 'user - not found' => sub {
    my $kc = Kubernetes::REST::Kubeconfig->new(kubeconfig_path => $config_file);
    throws_ok { $kc->user('nonexistent') } qr/User not found/,
        'throws for unknown user';
};

subtest 'api - token auth with CA file' => sub {
    my $kc = Kubernetes::REST::Kubeconfig->new(kubeconfig_path => $config_file);
    my $api = $kc->api('production');
    isa_ok $api, 'Kubernetes::REST';
    is $api->server->endpoint, 'https://prod.k8s.local:6443', 'server endpoint';
    is $api->server->ssl_verify_server, 1, 'SSL verification enabled';
    is $api->server->ssl_ca_file, $ca_file, 'CA file set';
    is $api->credentials->token, 'my-secret-token-12345', 'token set';
};

subtest 'api - cert auth with cert file references' => sub {
    my $kc = Kubernetes::REST::Kubeconfig->new(kubeconfig_path => $config_file);
    my $api = $kc->api('development');
    isa_ok $api, 'Kubernetes::REST';
    is $api->server->endpoint, 'https://dev.k8s.local:6443', 'dev server endpoint';
    is $api->server->ssl_cert_file, $cert_file, 'client cert file set';
    is $api->server->ssl_key_file, $key_file, 'client key file set';
    # CA from base64 data should be in-memory PEM, not a file
    ok $api->server->ssl_ca_pem, 'CA PEM data set (from base64 data)';
    ok !$api->server->ssl_ca_file, 'no CA file (in-memory instead)';
};

subtest 'api - insecure skip TLS verify' => sub {
    my $kc = Kubernetes::REST::Kubeconfig->new(kubeconfig_path => $config_file);
    my $api = $kc->api('insecure');
    is $api->server->ssl_verify_server, 0, 'SSL verification disabled';
    # No auth user should get empty token
    is $api->credentials->token, '', 'empty token for no-auth user';
};

subtest 'api - default context' => sub {
    my $kc = Kubernetes::REST::Kubeconfig->new(kubeconfig_path => $config_file);
    my $api = $kc->api;
    is $api->server->endpoint, 'https://prod.k8s.local:6443',
        'api() without args uses current-context';
};

subtest 'api - with context_name constructor arg' => sub {
    my $kc = Kubernetes::REST::Kubeconfig->new(
        kubeconfig_path => $config_file,
        context_name => 'development',
    );
    my $api = $kc->api;
    is $api->server->endpoint, 'https://dev.k8s.local:6443',
        'api() uses context_name from constructor';
};

subtest 'missing kubeconfig file' => sub {
    my $kc = Kubernetes::REST::Kubeconfig->new(
        kubeconfig_path => '/nonexistent/kubeconfig',
    );
    throws_ok { $kc->contexts } qr/Kubeconfig not found/,
        'throws for missing kubeconfig file';
};

t/15_advanced_api.t  view on Meta::CPAN

    lives_ok { $req->authenticate } 'authenticate with no credentials does not die';
    ok !exists $req->headers->{Authorization}, 'no Authorization header set';
};

subtest 'HTTPRequest - url builder from server + uri' => sub {
    require Kubernetes::REST::Server;
    my $req = Kubernetes::REST::HTTPRequest->new(
        method => 'GET',
        uri => '/api/v1/pods',
        headers => {},
        server => Kubernetes::REST::Server->new(endpoint => 'https://k8s.local:6443'),
    );
    is $req->url, 'https://k8s.local:6443/api/v1/pods', 'url built from server + uri';
};

# ============================================================================
# REST.pm - fetch_resource_map with mock OpenAPI spec
# ============================================================================

subtest 'fetch_resource_map - parses OpenAPI spec' => sub {
    my $api = mock_api();

t/16_cli_watch.t  view on Meta::CPAN

        }],
        users => [{
            name => 'test-user',
            user => { token => 'test-token' },
        }],
    });

    my $w = make_watcher(kubeconfig => $kc_file);
    my $api = $w->api;
    isa_ok $api, 'Kubernetes::REST', 'api built from kubeconfig';
    is $api->server->endpoint, 'https://test.k8s.local:6443', 'server endpoint from kubeconfig';
    is $api->credentials->token, 'test-token', 'token from kubeconfig';
};

subtest 'CLI::Role::Connection - api with context override' => sub {
    use File::Temp qw(tempdir);
    use YAML::XS ();

    my $tmpdir = tempdir(CLEANUP => 1);
    my $kc_file = "$tmpdir/kubeconfig";

t/16_cli_watch.t  view on Meta::CPAN

        ],
        contexts => [
            { name => 'default', context => { cluster => 'c1', user => 'u1' } },
            { name => 'other', context => { cluster => 'c2', user => 'u1' } },
        ],
        users => [{ name => 'u1', user => { token => 'tok' } }],
    });

    my $w = make_watcher(kubeconfig => $kc_file, context => 'other');
    my $api = $w->api;
    is $api->server->endpoint, 'https://c2.local', 'context override selects correct cluster';
};

done_testing;

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


use Test::Kubernetes::Mock qw(mock_api);
use Kubernetes::REST;
use Kubernetes::REST::Server;
use Kubernetes::REST::AuthToken;
use Kubernetes::REST::LogEvent;

my $mock_io = Test::Kubernetes::Mock::IO->new;

my $api = Kubernetes::REST->new(
    server      => Kubernetes::REST::Server->new(endpoint => 'http://mock.local'),
    credentials => Kubernetes::REST::AuthToken->new(token => 'MockToken'),
    resource_map_from_cluster => 0,
    io          => $mock_io,
);

# === Test 1: One-shot log retrieval ===
subtest 'one-shot log' => sub {
    $mock_io->add_log_lines('/api/v1/namespaces/default/pods/nginx-abc/log', [
        '2024-01-01T00:00:00Z Starting nginx',
        '2024-01-01T00:00:01Z Listening on port 80',

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

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

sub make_api {
    my ($io) = @_;
    return Kubernetes::REST->new(
        server      => Kubernetes::REST::Server->new(endpoint => 'https://mock.local'),
        credentials => Kubernetes::REST::AuthToken->new(token => 'MockToken'),
        resource_map_from_cluster => 0,
        io          => $io,
    );
}

subtest 'supports_duplex probe' => sub {
    my $basic = Test::PF::BasicIO->new;
    my $duplex = Test::PF::DuplexIO->new;
    ok(!$basic->supports_duplex, 'basic backend reports no duplex support');

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

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

sub make_api {
    my ($io) = @_;
    return Kubernetes::REST->new(
        server      => Kubernetes::REST::Server->new(endpoint => 'https://mock.local'),
        credentials => Kubernetes::REST::AuthToken->new(token => 'MockToken'),
        resource_map_from_cluster => 0,
        io          => $io,
    );
}

subtest 'exec requires name and command' => sub {
    my $api = make_api(Test::Exec::DuplexIO->new);

    throws_ok {

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

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

sub make_api {
    my ($io) = @_;
    return Kubernetes::REST->new(
        server      => Kubernetes::REST::Server->new(endpoint => 'https://mock.local'),
        credentials => Kubernetes::REST::AuthToken->new(token => 'MockToken'),
        resource_map_from_cluster => 0,
        io          => $io,
    );
}

subtest 'attach requires name' => sub {
    my $api = make_api(Test::Attach::DuplexIO->new);

    throws_ok {

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

    return 1;
}

# Get API - either mock or live based on environment
sub mock_api {
    require Kubernetes::REST;
    require Kubernetes::REST::Server;
    require Kubernetes::REST::AuthToken;

    return Kubernetes::REST->new(
        server => Kubernetes::REST::Server->new(endpoint => 'http://mock.local'),
        credentials => Kubernetes::REST::AuthToken->new(token => 'MockToken'),
        resource_map_from_cluster => 0,
        io => Test::Kubernetes::Mock::IO->new,
    );
}

sub live_api {
    require Kubernetes::REST::Kubeconfig;
    die "TEST_KUBERNETES_REST_KUBECONFIG must be set for live tests"
        unless $ENV{TEST_KUBERNETES_REST_KUBECONFIG};



( run in 1.593 second using v1.01-cache-2.11-cpan-524268b4103 )