NAME

Kubernetes::REST::Example - Working examples for Kubernetes::REST with Minikube, K3s, and other clusters

VERSION

version 1.000

DESCRIPTION

This document walks you through setting up Kubernetes::REST with a local Kubernetes cluster and shows how to manage real resources from Perl using IO::K8s typed objects.

Every code snippet is ready to copy-paste into a script once you have a running cluster and the Perl dependencies installed. A comprehensive runnable demo script is included in eg/demo.pl.

NAME

Kubernetes::REST::Example - Working examples for Kubernetes::REST with Minikube, K3s, and other clusters

CLUSTER SETUP

Kubernetes::REST works with any Kubernetes cluster. Below are setup instructions for common local development environments. All of them write a kubeconfig to ~/.kube/config that Kubernetes::REST::Kubeconfig picks up automatically.

Minikube

Minikube creates a single-node cluster inside a Docker container or VM.

# Install (Debian/Ubuntu)
curl -LO https://storage.googleapis.com/minikube/releases/latest/minikube-linux-amd64
sudo install minikube-linux-amd64 /usr/local/bin/minikube

# Start with Docker driver
minikube start --driver=docker

# Verify
minikube status

Minikube uses self-signed certificates and token authentication. The kubeconfig context is named minikube.

NodePort access:

minikube service <service-name> -n <namespace>

K3s

K3s is a lightweight Kubernetes distribution from Rancher. It installs as a single binary and runs directly on the host (no Docker required).

# Install
curl -sfL https://get.k3s.io | sh -

# K3s writes its own kubeconfig to a different path
export KUBECONFIG=/etc/rancher/k3s/k3s.yaml

# Or copy it so Kubernetes::REST::Kubeconfig finds it automatically
mkdir -p ~/.kube
sudo cp /etc/rancher/k3s/k3s.yaml ~/.kube/config
sudo chown $USER ~/.kube/config
chmod 600 ~/.kube/config

# Verify
kubectl get nodes

Key differences from Minikube:

  • Kubeconfig lives at /etc/rancher/k3s/k3s.yaml (not ~/.kube/config)

  • The server endpoint is https://127.0.0.1:6443

  • K3s includes Traefik as its default ingress controller

  • K3s uses containerd instead of Docker

  • NodePort services are accessible directly on the host IP

Connecting from Perl with K3s:

use Kubernetes::REST::Kubeconfig;

# If you copied the kubeconfig to ~/.kube/config:
my $api = Kubernetes::REST::Kubeconfig->new->api;

# If using the K3s path directly:
my $api = Kubernetes::REST::Kubeconfig->new(
    kubeconfig_path => '/etc/rancher/k3s/k3s.yaml',
)->api;

Kind (Kubernetes in Docker)

Kind runs Kubernetes nodes as Docker containers. Good for CI and testing.

# Install
go install sigs.k8s.io/kind@latest

# Create cluster
kind create cluster --name perl-test

# Context is named: kind-perl-test
my $api = Kubernetes::REST::Kubeconfig->new(
    context_name => 'kind-perl-test',
)->api;

Any cluster with a kubeconfig

If you have a kubeconfig (EKS, GKE, AKS, self-managed, etc.):

use Kubernetes::REST::Kubeconfig;

# Uses current context from ~/.kube/config
my $api = Kubernetes::REST::Kubeconfig->new->api;

# Or specify a context
my $api = Kubernetes::REST::Kubeconfig->new(
    context_name => 'my-production-cluster',
)->api;

INSTALL PERL DEPENDENCIES

cpanm Kubernetes::REST

CONNECTING TO THE CLUSTER

Using the kubeconfig (recommended)

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:

my $contexts = $kc->contexts;
say "Available: @$contexts";

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

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

LISTING RESOURCES

Namespaces

my $list = $api->list('Namespace');

for my $ns ($list->items->@*) {
    printf "%-20s %s\n",
        $ns->metadata->name,
        $ns->status->phase // 'Unknown';
}

Pods in a namespace

my $pods = $api->list('Pod', namespace => 'kube-system');

for my $pod ($pods->items->@*) {
    my $name   = $pod->metadata->name;
    my $phase  = $pod->status->phase // 'Unknown';
    my $ip     = $pod->status->podIP // 'pending';
    printf "%-45s %-10s %s\n", $name, $phase, $ip;
}

Services

my $services = $api->list('Service', namespace => 'default');

for my $svc ($services->items->@*) {
    my $name  = $svc->metadata->name;
    my $type  = $svc->spec->type // 'ClusterIP';
    my $ports = join ', ', map {
        $_->port . '/' . ($_->protocol // 'TCP')
    } ($svc->spec->ports // [])->@*;
    printf "%-20s %-12s %s\n", $name, $type, $ports;
}

Nodes

my $nodes = $api->list('Node');

for my $node ($nodes->items->@*) {
    my $info = $node->status->nodeInfo;
    printf "%-20s OS=%-8s kubelet=%s\n",
        $node->metadata->name,
        $info->operatingSystem // '?',
        $info->kubeletVersion // '?';
}

GETTING A SINGLE RESOURCE

get() returns a fully typed IO::K8s object:

my $pod = $api->get('Pod', 'my-pod', namespace => 'default');

say "Name:      " . $pod->metadata->name;
say "Namespace: " . $pod->metadata->namespace;
say "Kind:      " . $pod->kind;          # "Pod"
say "API:       " . $pod->api_version;   # "v1"
say "Phase:     " . $pod->status->phase;
say "Node:      " . ($pod->spec->nodeName // 'unscheduled');

# Access labels (hashref)
my $labels = $pod->metadata->labels // {};
for my $key (sort keys %$labels) {
    say "  $key = $labels->{$key}";
}

CREATING RESOURCES

Namespace

my $ns = $api->new_object(Namespace =>
    metadata => { name => 'perl-test' },
);

my $created = $api->create($ns);
say "Created: " . $created->metadata->name;

ConfigMap

my $cm = $api->create($api->new_object(ConfigMap =>
    metadata => {
        name      => 'app-config',
        namespace => 'perl-test',
    },
    data => {
        'database.host' => 'postgres.perl-test.svc.cluster.local',
        'database.port' => '5432',
        'app.debug'     => 'true',
    },
));

Secret

use MIME::Base64 qw(encode_base64);

my $secret = $api->create($api->new_object(Secret =>
    metadata => {
        name      => 'db-credentials',
        namespace => 'perl-test',
    },
    type => 'Opaque',
    data => {
        username => encode_base64('admin', ''),
        password => encode_base64('s3cret', ''),
    },
));

LimitRange

Set default resource limits for all containers in a namespace:

my $lr = $api->create($api->new_object(LimitRange =>
    metadata => {
        name      => 'default-limits',
        namespace => 'perl-test',
    },
    spec => {
        limits => [{
            type => 'Container',
            default        => { cpu => '200m', memory => '128Mi' },
            defaultRequest => { cpu => '50m',  memory => '64Mi' },
        }],
    },
));

Deployment with volumes, probes, and ServiceAccount

my $deploy = $api->create($api->new_object(Deployment =>
    metadata => {
        name      => 'my-app',
        namespace => 'perl-test',
    },
    spec => {
        replicas => 2,
        selector => {
            matchLabels => { app => 'my-app' },
        },
        template => {
            metadata => {
                labels      => { app => 'my-app' },
                annotations => { 'managed-by' => 'perl' },
            },
            spec => {
                serviceAccountName => 'default',
                containers => [{
                    name  => 'nginx',
                    image => 'nginx:1.27-alpine',
                    ports => [{ containerPort => 80, name => 'http' }],
                    env => [{
                        name => 'APP_ENV',
                        valueFrom => {
                            configMapKeyRef => {
                                name => 'app-config',
                                key  => 'app.debug',
                            },
                        },
                    }, {
                        name => 'DB_USER',
                        valueFrom => {
                            secretKeyRef => {
                                name => 'db-credentials',
                                key  => 'username',
                            },
                        },
                    }],
                    volumeMounts => [{
                        name      => 'config-volume',
                        mountPath => '/etc/nginx/conf.d',
                    }, {
                        name      => 'data-volume',
                        mountPath => '/data',
                    }],
                    resources => {
                        requests => { cpu => '50m',  memory => '32Mi' },
                        limits   => { cpu => '100m', memory => '64Mi' },
                    },
                    livenessProbe => {
                        httpGet => { path => '/', port => 80 },
                        initialDelaySeconds => 5,
                        periodSeconds => 10,
                    },
                    readinessProbe => {
                        httpGet => { path => '/', port => 80 },
                        initialDelaySeconds => 3,
                        periodSeconds => 5,
                    },
                }],
                volumes => [{
                    name => 'config-volume',
                    configMap => {
                        name  => 'app-config',
                        items => [{
                            key  => 'nginx.conf',
                            path => 'default.conf',
                        }],
                    },
                }, {
                    name => 'data-volume',
                    persistentVolumeClaim => {
                        claimName => 'my-storage',
                    },
                }],
            },
        },
    },
));

Service (NodePort)

my $svc = $api->create($api->new_object(Service =>
    metadata => {
        name      => 'my-app',
        namespace => 'perl-test',
    },
    spec => {
        type     => 'NodePort',
        selector => { app => 'my-app' },
        ports    => [{
            port       => 80,
            targetPort => 80,
            protocol   => 'TCP',
        }],
    },
));

my $node_port = $svc->spec->ports->[0]->nodePort;
say "NodePort: $node_port";

# Minikube: minikube service my-app -n perl-test
# K3s:      curl http://localhost:$node_port

Job

my $job = $api->create($api->new_object(Job =>
    metadata => {
        name      => 'batch-job',
        namespace => 'perl-test',
    },
    spec => {
        backoffLimit => 2,
        template => {
            spec => {
                restartPolicy => 'Never',
                containers => [{
                    name    => 'worker',
                    image   => 'busybox:latest',
                    command => ['sh', '-c', 'echo "done"; exit 0'],
                }],
            },
        },
    },
));

CronJob

my $cron = $api->create($api->new_object(CronJob =>
    metadata => {
        name      => 'scheduled-job',
        namespace => 'perl-test',
    },
    spec => {
        schedule => '*/5 * * * *',
        jobTemplate => {
            spec => {
                template => {
                    spec => {
                        restartPolicy => 'OnFailure',
                        containers => [{
                            name    => 'worker',
                            image   => 'busybox:latest',
                            command => ['sh', '-c', 'date'],
                        }],
                    },
                },
            },
        },
    },
));

RBAC (Role + RoleBinding)

my $role = $api->create($api->new_object(Role =>
    metadata => {
        name      => 'pod-reader',
        namespace => 'perl-test',
    },
    rules => [{
        apiGroups => [''],
        resources => ['pods'],
        verbs     => ['get', 'list', 'watch'],
    }],
));

my $binding = $api->create($api->new_object(RoleBinding =>
    metadata => {
        name      => 'read-pods',
        namespace => 'perl-test',
    },
    roleRef => {
        apiGroup => 'rbac.authorization.k8s.io',
        kind     => 'Role',
        name     => 'pod-reader',
    },
    subjects => [{
        kind      => 'ServiceAccount',
        name      => 'default',
        namespace => 'perl-test',
    }],
));

PersistentVolumeClaim

Works out of the box on Minikube (hostpath provisioner) and K3s (local-path provisioner).

my $pvc = $api->create($api->new_object(PersistentVolumeClaim =>
    metadata => {
        name      => 'my-storage',
        namespace => 'perl-test',
    },
    spec => {
        accessModes => ['ReadWriteOnce'],
        resources => {
            requests => { storage => '100Mi' },
        },
    },
));

ResourceQuota

my $quota = $api->create($api->new_object(ResourceQuota =>
    metadata => {
        name      => 'ns-quota',
        namespace => 'perl-test',
    },
    spec => {
        hard => {
            pods               => '20',
            'requests.cpu'     => '2',
            'requests.memory'  => '1Gi',
        },
    },
));

UPDATING RESOURCES

Fetch a resource, modify it, send it back:

# Scale the deployment
my $deploy = $api->get('Deployment', 'my-app',
    namespace => 'perl-test',
);
$deploy->spec->replicas(3);
my $updated = $api->update($deploy);
say "Scaled to " . $updated->spec->replicas . " replicas";

# Rolling update (change image)
$deploy = $api->get('Deployment', 'my-app', namespace => 'perl-test');
$deploy->spec->template->spec->containers->[0]->image('nginx:1.27-bookworm');
my $ann = $deploy->spec->template->metadata->annotations // {};
$ann->{'kubernetes.io/change-cause'} = 'Image update via Perl';
$deploy->spec->template->metadata->annotations($ann);
$api->update($deploy);

# Add a label
my $pod = $api->get('Pod', 'my-pod', namespace => 'perl-test');
my $labels = $pod->metadata->labels // {};
$labels->{environment} = 'testing';
$pod->metadata->labels($labels);
$api->update($pod);

PATCHING RESOURCES

patch() modifies specific fields without fetching and replacing the entire object. This avoids conflicts when multiple clients update the same resource.

Strategic Merge Patch (default)

The Kubernetes-native patch type. Arrays are merged intelligently based on the field's merge strategy.

# Add a label without touching other labels
my $patched = $api->patch('Pod', 'my-pod',
    namespace => 'perl-test',
    patch     => {
        metadata => { labels => { environment => 'staging' } },
    },
);

# Scale a deployment (one field only)
$api->patch('Deployment', 'my-app',
    namespace => 'perl-test',
    patch     => { spec => { replicas => 5 } },
);

JSON Merge Patch

Simple merge where null deletes a key. Arrays are replaced entirely.

# Remove an annotation
use JSON::PP ();
$api->patch('Pod', 'my-pod',
    namespace => 'perl-test',
    type      => 'merge',
    patch     => {
        metadata => { annotations => { 'old-key' => JSON::PP::null } },
    },
);

JSON Patch (RFC 6902)

An array of precise operations for surgical edits.

$api->patch('Deployment', 'my-app',
    namespace => 'perl-test',
    type      => 'json',
    patch     => [
        { op => 'replace', path => '/spec/replicas', value => 3 },
        { op => 'add', path => '/metadata/labels/version', value => '2.0' },
    ],
);

Patch vs Update

Use patch() when you only need to change a few fields - it's safer because it won't overwrite changes made by other clients between your get() and update(). Use update() when you need to replace the entire spec.

WAITING FOR READINESS

Kubernetes operations are asynchronous. Poll the API to wait for a desired state:

# Wait for a deployment's pods to be ready
for my $i (1..60) {
    my $d = $api->get('Deployment', 'my-app', namespace => 'perl-test');
    my $ready   = $d->status->readyReplicas // 0;
    my $desired = $d->spec->replicas // 1;
    if ($ready == $desired) {
        say "All $ready replicas ready!";
        last;
    }
    sleep 1;
}

# Wait for a job to complete
for my $i (1..60) {
    my $j = $api->get('Job', 'batch-job', namespace => 'perl-test');
    if (($j->status->succeeded // 0) >= 1) {
        say "Job completed at " . $j->status->completionTime;
        last;
    }
    sleep 1;
}

# Wait for a pod to reach Running phase
for my $i (1..30) {
    my $p = $api->get('Pod', 'my-pod', namespace => 'perl-test');
    my $phase = $p->status->phase // 'Unknown';
    if ($phase eq 'Running') {
        say "Pod running on " . $p->spec->nodeName;
        last;
    }
    sleep 2;
}

INSPECTING POD STATUS

my $pod = $api->get('Pod', 'my-pod', namespace => 'perl-test');

say "Phase: " . ($pod->status->phase // 'Unknown');
say "IP:    " . ($pod->status->podIP // 'pending');
say "Node:  " . ($pod->spec->nodeName // 'unscheduled');

# Container-level status
for my $cs (($pod->status->containerStatuses // [])->@*) {
    say "Container: " . $cs->name;
    say "  Ready:    " . ($cs->ready ? 'yes' : 'no');
    say "  Restarts: " . ($cs->restartCount // 0);
    say "  Image:    " . $cs->image;
}

ERROR HANDLING

create() croaks if the resource already exists. Use eval to handle this gracefully:

my $ns = eval {
    $api->create($api->new_object(Namespace =>
        metadata => { name => 'perl-test' },
    ));
};
if ($@) {
    say "Already exists, fetching instead";
    $ns = $api->get('Namespace', 'perl-test');
}

SERIALIZATION

Every IO::K8s object can be serialized to JSON or YAML:

my $pod = $api->get('Pod', 'my-pod', namespace => 'perl-test');

# To JSON
my $json = JSON::MaybeXS->new(
    canonical => 1, pretty => 1, utf8 => 1,
)->encode($pod->TO_JSON);

# To YAML (suitable for kubectl apply -f)
say $pod->to_yaml;

# Save to file
$pod->save('/tmp/my-pod.yaml');

# Round-trip: hashref -> object
my $pod2 = $api->inflate($pod->TO_JSON);
say $pod2->metadata->name;

WATCHING FOR CHANGES

The Watch API streams events as resources are added, modified, or deleted. This is the foundation for building Kubernetes controllers.

Basic watch

$api->watch('Pod',
    namespace => 'default',
    timeout   => 60,
    on_event  => sub {
        my ($event) = @_;
        printf "%-10s %s\n",
            $event->type,
            $event->object->metadata->name;
    },
);

Resumable watch loop

# Watch indefinitely, surviving timeouts and disconnects
my $rv;
while (1) {
    $rv = eval {
        $api->watch('Pod',
            namespace       => 'default',
            resourceVersion => $rv,
            on_event        => sub {
                my ($event) = @_;
                say $event->type . ": " . $event->object->metadata->name;
            },
        );
    };
    if ($@ && $@ =~ /410 Gone/) {
        # resourceVersion too old, re-list to get fresh state
        warn "Watch expired, re-listing...\n";
        $api->list('Pod', namespace => 'default');
        $rv = undef;
    } elsif ($@) {
        warn "Watch error: $@\n";
        sleep 5;
    }
    # Normal timeout, just restart the watch
}

Watch with selectors

$api->watch('Pod',
    namespace     => 'default',
    labelSelector => 'app=web',
    fieldSelector => 'status.phase=Running',
    on_event      => sub {
        my ($event) = @_;
        say $event->type . ": " . $event->object->metadata->name;
    },
);

Watch any resource type

# Watch deployments
$api->watch('Deployment',
    namespace => 'default',
    on_event  => sub {
        my ($event) = @_;
        my $deploy = $event->object;
        printf "%s %s: %d/%d ready\n",
            $event->type,
            $deploy->metadata->name,
            $deploy->status->readyReplicas // 0,
            $deploy->spec->replicas // 0;
    },
);

# Watch namespaces (cluster-scoped)
$api->watch('Namespace',
    on_event => sub {
        my ($event) = @_;
        say $event->type . ": " . $event->object->metadata->name;
    },
);

DELETING RESOURCES

# Delete by object
$api->delete($pod);

# Delete by name
$api->delete('Pod',        'my-pod',    namespace => 'perl-test');
$api->delete('Deployment', 'my-app',    namespace => 'perl-test');
$api->delete('Namespace',  'perl-test');

When cleaning up a full namespace, delete dependent resources first to avoid errors (e.g. delete Pods and Deployments before the ServiceAccount they reference):

for my $r (
    ['CronJob',              'scheduled-job'],
    ['Job',                  'batch-job'],
    ['Service',              'my-app'],
    ['Deployment',           'my-app'],
    ['RoleBinding',          'read-pods'],
    ['Role',                 'pod-reader'],
    ['PersistentVolumeClaim','my-storage'],
    ['Secret',               'db-credentials'],
    ['ConfigMap',            'app-config'],
    ['ResourceQuota',        'ns-quota'],
    ['LimitRange',           'default-limits'],
) {
    my ($kind, $name) = @$r;
    eval { $api->delete($kind, $name, namespace => 'perl-test') };
}
eval { $api->delete('Namespace', 'perl-test') };

RESOURCE QUOTA INSPECTION

my $q = $api->get('ResourceQuota', 'ns-quota',
    namespace => 'perl-test',
);
my $used = $q->status->used // {};
my $hard = $q->status->hard // {};
for my $key (sort keys %$hard) {
    printf "%-25s %s / %s\n", $key,
        $used->{$key} // '0', $hard->{$key};
}

CUSTOM RESOURCE DEFINITIONS (CRDs)

Kubernetes::REST can manage Custom Resources the same way it manages built-in ones. You define a Perl class for your CRD, register it in the resource map, and use the standard CRUD API.

Defining the CRD in Kubernetes

First, apply a CRD to your cluster. Example - a StaticWebSite resource for hosting static websites in your homelab:

# staticwebsite-crd.yaml
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
  name: staticwebsites.homelab.example.com
spec:
  group: homelab.example.com
  versions:
    - name: v1
      served: true
      storage: true
      schema:
        openAPIV3Schema:
          type: object
          properties:
            spec:
              type: object
              required: [domain, image]
              properties:
                domain:   { type: string }
                image:    { type: string }
                replicas: { type: integer, default: 1 }
                tls:      { type: boolean, default: false }
            status:
              type: object
              properties:
                readyReplicas: { type: integer }
                url:           { type: string }
  scope: Namespaced
  names:
    plural: staticwebsites
    singular: staticwebsite
    kind: StaticWebSite
    shortNames: [sw]

$ kubectl apply -f staticwebsite-crd.yaml

Writing a CRD class

Create a Perl class using IO::K8s::APIObject with import parameters for api_version and resource_plural:

package My::StaticWebSite;
use IO::K8s::APIObject
    api_version     => 'homelab.example.com/v1',
    resource_plural => 'staticwebsites';
with 'IO::K8s::Role::Namespaced';

k8s spec   => { Str => 1 };
k8s status => { Str => 1 };
1;

Key points:

  • api_version - the CRD's group/version (e.g. homelab.example.com/v1)

  • resource_plural - must match the CRD's spec.names.plural

  • kind is derived automatically from the class name (last :: segment)

  • Apply IO::K8s::Role::Namespaced for namespace-scoped CRDs

  • spec and status are opaque hashrefs ({ Str => 1 })

Registering the CRD class

Register your class with the + 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',
    },
);

CRUD on Custom Resources

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

use JSON::PP ();

# Create
my $site = $api->create($api->new_object(StaticWebSite =>
    metadata => {
        name      => 'my-blog',
        namespace => 'default',
    },
    spec => {
        domain   => 'blog.example.com',
        image    => 'nginx:1.27-alpine',
        replicas => 2,
        tls      => JSON::PP::true,
    },
));

# Get
my $site = $api->get('StaticWebSite', 'my-blog', namespace => 'default');
say $site->spec->{domain};    # blog.example.com

# List
my $list = $api->list('StaticWebSite', namespace => 'default');
for my $s ($list->items->@*) {
    say $s->metadata->name . ": " . $s->spec->{domain};
}

# Update
$site->spec->{replicas} = 3;
$api->update($site);

# Delete
$api->delete('StaticWebSite', 'my-blog', namespace => 'default');

Note: For boolean CRD fields, use JSON::PP::true and JSON::PP::false instead of 1 and 0. The Kubernetes API validates JSON types strictly for custom resources.

Serialization

CRD objects support the same serialization as built-in resources:

say $site->to_yaml;
# ---
# apiVersion: homelab.example.com/v1
# kind: StaticWebSite
# metadata:
#   name: my-blog
#   namespace: default
# spec:
#   domain: blog.example.com
#   ...

$site->save('/tmp/my-blog.yaml');

AutoGen (dynamic class generation)

If you don't want to write Perl classes by hand, you can generate them dynamically using IO::K8s::AutoGen:

use IO::K8s::AutoGen;

my $class = IO::K8s::AutoGen::get_or_generate(
    'com.example.homelab.v1.StaticWebSite',  # definition name
    $schema,                                   # OpenAPI schema
    {},                                        # all definitions
    'MyApp::K8s',                              # namespace
    api_version     => 'homelab.example.com/v1',
    kind            => 'StaticWebSite',
    resource_plural => 'staticwebsites',
    is_namespaced   => 1,
);

$api->resource_map->{StaticWebSite} = "+$class";

# Now use it like any other resource
my $site = $api->k8s->struct_to_object($class, {
    metadata => { name => 'my-blog', namespace => 'default' },
    spec     => { domain => 'blog.example.com', image => 'nginx' },
});
$api->create($site);

More CRD ideas

Custom resources can simplify complex Kubernetes patterns for your homelab:

  • BackupSchedule - wraps CronJob for scheduled backups with retention policies

  • DatabaseCluster - wraps StatefulSet + PVC + Service for database instances

  • HomeService - simplified Deployment + Service + optional Ingress

  • DNSRecord - manages external DNS entries alongside cluster resources

IO::K8s CLASS REFERENCE

The objects returned by Kubernetes::REST are all IO::K8s classes. Here are the most commonly used ones:

IO::K8s::Api::Core::V1::Pod - Pods
IO::K8s::Api::Core::V1::Service - Services
IO::K8s::Api::Core::V1::ConfigMap - ConfigMaps
IO::K8s::Api::Core::V1::Secret - Secrets
IO::K8s::Api::Core::V1::Namespace - Namespaces
IO::K8s::Api::Core::V1::PersistentVolumeClaim - PersistentVolumeClaims
IO::K8s::Api::Core::V1::ResourceQuota - ResourceQuotas
IO::K8s::Api::Core::V1::LimitRange - LimitRanges
IO::K8s::Api::Core::V1::ServiceAccount - ServiceAccounts
IO::K8s::Api::Apps::V1::Deployment - Deployments
IO::K8s::Api::Batch::V1::Job - Jobs
IO::K8s::Api::Batch::V1::CronJob - CronJobs
IO::K8s::Api::Rbac::V1::Role - Roles
IO::K8s::Api::Rbac::V1::RoleBinding - RoleBindings
IO::K8s::Api::Networking::V1::Ingress - Ingress
IO::K8s::Apimachinery::Pkg::Apis::Meta::V1::ObjectMeta - object metadata

All classes share these patterns:

$obj->metadata->name;          # resource name
$obj->metadata->namespace;     # namespace (if namespaced)
$obj->metadata->labels;        # hashref of labels
$obj->metadata->uid;           # unique ID assigned by Kubernetes
$obj->kind;                    # "Pod", "Service", etc.
$obj->api_version;             # "v1", "apps/v1", etc.
$obj->TO_JSON;                 # serialize to hashref
$obj->to_yaml;                 # serialize to YAML string

See IO::K8s for full documentation on the object system, including type safety, YAML loading, and custom resource support.

DEMO SCRIPT

This distribution ships with a comprehensive runnable demo at eg/demo.pl. It exercises every feature documented above in 20 steps against a live cluster:

perl eg/demo.pl

The script connects via kubeconfig, creates a dedicated namespace, then walks through: LimitRange, ResourceQuota, ConfigMap, Secret, ServiceAccount, Role + RoleBinding, PersistentVolumeClaim, Deployment (2 replicas with volumes, probes, ConfigMap/Secret env vars), Service (NodePort), pod inspection, scaling to 3 replicas, rolling image update, Job (run to completion), CronJob, utility Pod (with volume mounts and env vars from ConfigMap and Secret), full namespace inventory, YAML/JSON serialization with round-trip inflate, quota usage inspection, and ordered cleanup.

It works on Minikube, K3s, Kind, or any cluster with a kubeconfig.

SEE ALSO

Kubernetes::REST, Kubernetes::REST::Kubeconfig, IO::K8s, https://minikube.sigs.k8s.io/, https://k3s.io/, https://kind.sigs.k8s.io/, https://kubernetes.io/docs/

SUPPORT

Issues

Please report bugs and feature requests on GitHub at https://github.com/pplu/kubernetes-rest/issues.

IRC

Join #kubernetes on irc.perl.org or message Getty directly.

CONTRIBUTING

Contributions are welcome! Please fork the repository and submit a pull request.

AUTHORS

  • Torsten Raudssus <torsten@raudssus.de>

  • Jose Luis Martinez Torres <jlmartin@cpan.org> (JLMARTIN, original author, inactive)

COPYRIGHT AND LICENSE

This software is Copyright (c) 2019 by Jose Luis Martinez.

This is free software, licensed under:

The Apache License, Version 2.0, January 2004