NAME

Net::Async::Kubernetes::Controller - Minimal controller runtime for Net::Async::Kubernetes

VERSION

version 0.009

SYNOPSIS

use IO::Async::Loop;
use Net::Async::Kubernetes;

my $loop = IO::Async::Loop->new;
my $kube = Net::Async::Kubernetes->new(
    kubeconfig => "$ENV{HOME}/.kube/config",
);
$loop->add($kube);

my $controller = $kube->controller(
    on_reconcile => sub {
        my ($ctx) = @_;

        return $ctx->{controller}->patch_status($ctx->{object},
            status => { phase => 'Running' },
        );
    },
);

$controller->watch_resource('Pod', namespace => 'default');
$loop->run;

DESCRIPTION

Net::Async::Kubernetes::Controller is a minimal controller runtime built on top of Net::Async::Kubernetes and its watcher support.

It is intentionally small. The module focuses on the operational pieces most controllers need:

  • watch registration

  • keyed queueing with deduplication

  • serialized reconcile dispatch

  • retry hooks with configurable delay policy

  • status subresource helpers

It does not attempt to provide higher-level controller DSL sugar.

configure

Internal IO::Async::Notifier configuration method.

Accepted parameters:

kube

Existing Net::Async::Kubernetes instance to bind to. The reference is weak, as in Net::Async::Kubernetes::Watcher, so the caller has to keep the client alive for as long as the controller is used.

kubeconfig, context, server, credentials, resource_map, resource_map_from_cluster

Client construction parameters used when kube is not supplied. A client built this way is owned by the controller.

on_reconcile

Required reconcile callback. Receives a hashref with controller, kube, resource, event_type, object, key, and attempt. controller and kube are weak references. Both are valid for the whole reconcile, including anything chained onto the Future it returns, but a context kept past that keeps neither object alive.

on_watch_error

Optional callback for errors of a registered watch: ERROR events, for example a 403 arriving mid-stream, and failed watch attempts -- a transport error, the API server rejecting the request, or a stream that closes at once without an event or right after an ERROR event -- which the watcher retries with backoff. Receives ($error, $ctx), where $error is the Status hashref the watcher reports (see "on_error" in Net::Async::Kubernetes::Watcher; a failed attempt has reason WatchFailed) and $ctx carries controller, kube, and resource. Errors are not reconcile objects, so they never enter the workqueue. An on_error passed to watch_resource takes precedence for that watch. Without either, the watcher warns about failed attempts.

The watcher's reconnect settings (reconnect_delay, max_reconnect_delay, reconnect_jitter, max_retries, min_watch_duration) are passed to watch_resource with the other watcher arguments. A watch that gave up on max_retries stays stopped; stop and start on the controller build it anew.

retry_delay

Optional retry policy. Accepts a fixed scalar delay, an arrayref of delays, or a coderef receiving ($attempt, $ctx, $error).

kube

Returns the bound Net::Async::Kubernetes client.

on_reconcile

Returns the reconcile callback.

on_watch_error

Returns the watch error callback, if one is configured.

retry_delay

Returns the configured retry delay policy. Defaults to 1.

start

Starts all registered resource watches. Called automatically when the controller is added to an event loop.

stop

Stops registered watches and prevents further queue processing. Each stopped watch is also detached from the client it was registered on, so a watcher handle a caller kept from watch_resource does not survive its controller's stop: it is no longer attached to anything, calling start on it directly drives a notifier with no loop, and the controller's own start builds a fresh watcher for the spec regardless. Do not hold on to a watcher handle past a stop.

Pending work is dropped with them: the workqueue is cleared and any retry timer is cancelled, because a restart re-lists through fresh watches and delivers everything still present again. A key's failure count survives, so a key that was retrying picks up at its next attempt number rather than at attempt 1.

watch_resource

$controller->watch_resource('Pod',
    namespace => 'default',
    key_for   => sub {
        my ($object, $spec) = @_;
        return $object->metadata->name;
    },
);

Registers a watched resource and returns the watcher instance once started. An odd list of parameters croaks, as Invalid arguments to watch_resource(), before anything is registered. Repeated events for the same reconcile key are coalesced into a single queued entry. A key's entry is dropped once it reconciles cleanly, so the queue does not grow with the number of objects seen; a key that is still queued, dirty or retrying keeps its entry, and with it its attempt count.

get_object

Thin wrapper around $controller->kube->get(...). Returns its Future, resolving to the inflated IO::K8s object.

list_objects

Thin wrapper around $controller->kube->list(...). Returns its Future, resolving to an IO::K8s::List.

patch_status

$controller->patch_status('Pod', 'my-pod',
    namespace => 'default',
    status    => { phase => 'Running' },
)->get;

$controller->patch_status($object,
    status => { phase => 'Running' },
)->get;

Patch the /status subresource for an object. Accepts either a class/name pair (the name positional or as name => ...) or an object instance, plus a status payload; in the object form, status defaults to the object's own status when its class has one. The helper sends { status => $status } through "patch_status" in Net::Async::Kubernetes, with the patch type from type (merge by default, strategic or json). Returns a Future that resolves to the patched object.

Bad arguments, a missing status (including an object whose class has no status to fall back on), an unknown resource or patch type, and server errors fail the returned Future instead of dying. So does any option other than name, namespace, status and type -- in the object form only status and type, the object gives name and namespace -- before a request is sent: Unknown argument 'typ' to patch_status() (allowed: name, namespace, status, type). Such an option would otherwise be dropped when the patch is built. An odd list of options after the object or the name fails it as Invalid arguments to patch_status() too, before a request is sent.

update_status

$controller->update_status($object)->get;

Update the /status subresource for a full object instance, through "update_status" in Net::Async::Kubernetes. Returns a Future that resolves to the updated object. An object without metadata or metadata.name, or one that is no Kubernetes resource (an IO::K8s::List, a nested PodSpec), fails the Future rather than croaking as the client method does.

SEE ALSO

SUPPORT

Issues

Please report bugs and feature requests on GitHub at https://github.com/Getty/p5-net-async-kubernetes/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.

AUTHOR

Torsten Raudssus <torsten@raudssus.de>

COPYRIGHT AND LICENSE

This software is copyright (c) 2026 by Torsten Raudssus <torsten@raudssus.de> https://raudssus.de/.

This is free software; you can redistribute it and/or modify it under the same terms as the Perl 5 programming language system itself.