NAME
IO::K8s - Objects representing things found in the Kubernetes API
VERSION
version 1.108
SYNOPSIS
use IO::K8s;
my $k8s = IO::K8s->new;
# Load .pk8s manifest files (Perl DSL)
my $resources = $k8s->load('myapp.pk8s');
# Load YAML manifests and validate declared field types
my $resources = $k8s->load_yaml('deployment.yaml');
# Also reject fields the current model does not declare
my $strict_k8s = IO::K8s->new(strict => 1);
my $strict_resources = $strict_k8s->load_yaml('deployment.yaml');
# Validate with error collection
my ($objs, $errors) = $k8s->load_yaml($yaml, collect_errors => 1);
# Create objects programmatically
my $pod = $k8s->new_object('Pod',
metadata => { name => 'my-pod', namespace => 'default' },
spec => { containers => [{ name => 'app', image => 'nginx' }] }
);
# Export to YAML and save
print $pod->to_yaml;
$pod->save('pod.yaml');
# Inflate JSON/struct into typed objects
my $svc = $k8s->json_to_object('Service', '{"kind":"Service",...}');
my $obj = $k8s->inflate($json_with_kind); # Auto-detect class from 'kind'
# Serialize back
my $json = $k8s->object_to_json($svc);
my $struct = $k8s->object_to_struct($pod);
# With OpenAPI spec for Custom Resources (CRDs)
my $k8s = IO::K8s->new(openapi_spec => $spec_from_cluster);
my $helmchart = $k8s->inflate($helmchart_json); # Auto-generates class!
# With external resource map providers (e.g. IO::K8s::Cilium)
my $k8s = IO::K8s->new(with => ['IO::K8s::Cilium']);
# Or add at runtime
$k8s->add('IO::K8s::Cilium'); # class name
$k8s->add(IO::K8s::Cilium->new); # instance
$k8s->add({ MyThing => '+My::Thing' }); # raw hashref
# Disambiguate colliding kind names (e.g. both core and Cilium have NetworkPolicy)
$k8s->new_object('NetworkPolicy', { ... }); # core (first-registered)
$k8s->new_object('NetworkPolicy', { ... }, 'cilium.io/v2'); # Cilium
$k8s->new_object('cilium.io/v2/NetworkPolicy', { ... }); # domain-qualified
# inflate() auto-uses apiVersion from the data for disambiguation
$k8s->inflate('{"kind":"NetworkPolicy","apiVersion":"cilium.io/v2",...}');
DESCRIPTION
This module provides objects and serialization / deserialization methods that represent the structures found in the Kubernetes API https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.37/
Kubernetes API is strict about input types. When a value is expected to be an integer, sending it as a string will cause rejection. This module ensures correct value types in JSON that can be sent to Kubernetes.
It also inflates JSON returned by Kubernetes into typed Perl objects.
add_crd
my $registered = $k8s->add_crd('crds/knobs.yaml', $crd_object, \%crd_hash, ...);
Loads each argument through "load" in IO::K8s::CRD, generates one class per served version in this instance's AutoGen namespace, and registers them: every version under its domain-qualified key (group/version/Kind), the storage version under the bare Kind -- through "add", so a class already holding the short name (a provider merged earlier) keeps it and the CRD's class stays reachable by its qualified key. Returns { $Kind => { $api_version => $class, ..., storage => $api_version } }. Classes are cached by group/version/Kind within this instance's AutoGen namespace ("generate" in IO::K8s::CRD), so re-adding an edited manifest on the same $k8s silently returns the class generated the first time -- call add_crd on a fresh instance to pick up schema changes; classes generated by any call live for the life of the process regardless, so a reload loop that builds a fresh $k8s per iteration (rather than calling add_crd again on the same one) accumulates classes rather than freeing the old ones -- add_crd is meant to run once at startup, not inside a polling loop.
Two CRDs can legitimately share a bare Kind across groups (their domain-qualified keys, not the Kind alone, are what actually disambiguate them) -- calling add_crd for both merges the second's version entries into the first's return value under that Kind rather than replacing it outright, and storage stays whichever the FIRST registration reported, mirroring "add"'s own first-registration-wins rule for a short-name collision.
A trailing hashref of options is forwarded to "generate" in IO::K8s::CRD (and from there to "get_or_generate" in IO::K8s::AutoGen) for every CRD in this call:
$k8s->add_crd('crds/knobs.yaml', { reuse_core => 0 });
Distinguished from a CRD document by the absence of a kind key -- every CRD hashref "load" in IO::K8s::CRD accepts has one -- so it must be the LAST argument and only one is read per call.
NAME
IO::K8s - Objects representing things found in the Kubernetes API
CLASS ARCHITECTURE
IO::K8s uses a layered architecture. Understanding these layers helps when working with built-in resources or writing your own CRD classes.
IO::K8s::Resource (setup layer)
Declaring a class with use IO::K8s::Resource imports Moo, installs the k8s DSL, and composes IO::K8s::Role::Resource. It does not make that class isa('IO::K8s::Resource'). It provides:
Moo class setup
The
k8sDSL for declaring attributes with Kubernetes typesTO_JSON/to_jsonserializationType registry for inflation (JSON -> objects)
The k8s DSL supports these type specifications:
k8s name => 'Str'; # string attribute
k8s replicas => 'Int'; # integer attribute
k8s ready => 'Bool'; # boolean attribute
k8s spec => 'Core::V1::PodSpec'; # nested IO::K8s object
k8s ports => ['Core::V1::ServicePort']; # array of objects
k8s labels => { Str => 1 }; # hash of strings
k8s items => ['+Full::Class::Name']; # array with full class (+ prefix)
IO::K8s::APIObject (top-level resources)
IO::K8s::APIObject uses the same setup for top-level API objects (Pod, Deployment, Service, etc.) and additionally composes IO::K8s::Role::APIObject. It adds:
metadataattribute (IO::K8s::Apimachinery::Pkg::Apis::Meta::V1::ObjectMeta)api_version()- derived from class name for built-in types, or set via import parameter for CRDskind()- derived from the last segment of the class nameresource_plural()- the plural Kubernetes addresses the Kind by (pods,networkpolicies,ingresses), from a table generated off the upstream spec for built-in types; CRDs declare their ownto_yaml()- serialize to YAML suitable forkubectl apply -fsave($file)- write YAML to file
IO::K8s::Role::Namespaced (marker role)
IO::K8s::Role::Namespaced is a marker role for namespace-scoped resources. Kubernetes::REST checks this to build the correct URL path (with or without /namespaces/{ns}/).
WRITING CRD CLASSES
To use Custom Resource Definitions with Kubernetes::REST, write a Perl class using IO::K8s::APIObject. It gives a custom class the same Moo/DSL setup and top-level APIObject role composition as built-in Kubernetes types like Pod, Deployment, and Service.
Minimal CRD class
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;
That's it - 6 lines of actual code. This class now supports:
my $site = My::StaticWebSite->new(
metadata => $meta_object,
spec => { domain => 'blog.example.com', image => 'nginx' },
);
$site->kind; # "StaticWebSite"
$site->api_version; # "homelab.example.com/v1"
$site->to_yaml; # full YAML output
$site->TO_JSON; # hashref for JSON encoding
Import parameters
use IO::K8s::APIObject accepts these parameters:
api_version(required for CRDs)-
The CRD's
group/version, e.g.'homelab.example.com/v1'. For built-in types this is derived from the class name (IO::K8s::Api::Core::V1::Podgivesv1), but CRDs must specify it explicitly since their class names don't follow theIO::K8s::Api::*convention. resource_plural(recommended for CRDs)-
The plural resource name for URL building and RBAC
resources:rules, e.g.'staticwebsites'. Must match the CRD'sspec.names.plural. An explicit value always wins over anything IO::K8s knows.Built-in Kinds do not need this: their plurals come from a table generated off the upstream OpenAPI spec's REST paths. CRDs do, because there is no spec to read them from. If omitted,
resource_plural()returnsundefand the caller is left to pluralize the kind name itself (StaticWebSite->staticwebsites) -- that heuristic does not work for all names, which is exactly why declaring it is recommended.
Namespaced vs cluster-scoped
Apply IO::K8s::Role::Namespaced for namespace-scoped CRDs (the common case). Omit it for cluster-scoped CRDs:
# Namespaced CRD (most CRDs):
package My::StaticWebSite;
use IO::K8s::APIObject api_version => 'homelab.example.com/v1', ...;
with 'IO::K8s::Role::Namespaced';
# Cluster-scoped CRD (rare):
package My::ClusterBackupPolicy;
use IO::K8s::APIObject api_version => 'backup.example.com/v1', ...;
# No 'with Namespaced' - this is cluster-wide
Registering with Kubernetes::REST
Register your CRD class in the resource map using the + prefix (which means "use this full class name as-is"):
use Kubernetes::REST::Kubeconfig;
use My::StaticWebSite;
my $api = Kubernetes::REST::Kubeconfig->new->api;
$api->resource_map->{StaticWebSite} = '+My::StaticWebSite';
# Now use it like any built-in resource
my $site = $api->create($api->new_object(StaticWebSite =>
metadata => { name => 'my-blog', namespace => 'default' },
spec => { domain => 'blog.example.com', image => 'nginx' },
));
See Kubernetes::REST::Example for complete CRUD examples with CRDs.
AUTO-GENERATION
IO::K8s can automatically generate classes for Custom Resources and other types not included in the built-in classes. This is an alternative to writing CRD classes by hand.
From cluster OpenAPI spec
Provide the cluster's OpenAPI spec and IO::K8s will auto-generate classes on demand:
use IO::K8s;
use Kubernetes::REST::Kubeconfig;
# Get OpenAPI spec from cluster
my $api = Kubernetes::REST::Kubeconfig->new->api;
my $resp = $api->_request('GET', '/openapi/v2');
my $spec = JSON::MaybeXS->new->decode($resp->content);
# Create IO::K8s with auto-generation enabled
my $k8s = IO::K8s->new(openapi_spec => $spec);
# An unknown Kind with an unambiguous GVK definition in this spec auto-generates a class
my $addon = $k8s->inflate($k3s_addon_json); # k3s.cattle.io/v1 Addon
my $chart = $k8s->inflate($helmchart_json); # helm.cattle.io/v1 HelmChart
Auto-generated classes are placed in a unique namespace per IO::K8s instance (e.g., IO::K8s::_AUTOGEN_abc123::...) to avoid collisions.
From a CustomResourceDefinition manifest
Loading a CRD manifest directly -- see "add_crd" -- generates one class per served version and goes further than the OpenAPI-spec path above: every inline type: object schema below the top level, which is how a CRD schema is written everywhere below its Kind, becomes its own nested class named after its place in the parent (<Kind>::<Prop>, with an Item / Value suffix for array items and map values) instead of an opaque hash of strings, so field options and the unknown-field bag apply at every level. Hash-style access on such a field still works -- a Moo object is a blessed hash keyed by attribute name -- so code that reads $obj->{spec}{mode} does not need to change either way. Only a property-less object and an additionalProperties-only map (nothing underneath to attach options to) stay opaque. IO::K8s::CRD::Emitter renders the same classes as checked-in source.
Explicit generation with IO::K8s::AutoGen
For more control, use IO::K8s::AutoGen directly:
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,
);
# Register with Kubernetes::REST
$api->resource_map->{StaticWebSite} = "+$class";
Custom Class Namespaces
You can provide your own pre-built classes that take precedence over both built-in and auto-generated classes:
my $k8s = IO::K8s->new(
class_namespaces => ['MyApp::K8s'],
openapi_spec => $spec,
);
With this configuration, the class lookup order is:
1. MyApp::K8s::... (your classes)
2. IO::K8s::... (built-in classes)
3. IO::K8s::_AUTOGEN_... (auto-generated)
This lets you create optimized or customized classes for specific resources while falling back to auto-generation for everything else.
ATTRIBUTES
with
Optional. ArrayRef of external resource map providers to merge at construction time. Each entry can be a class name (string) or an object instance. Classes must consume IO::K8s::Role::ResourceMap or otherwise provide a resource_map() method.
my $k8s = IO::K8s->new(with => ['IO::K8s::Cilium']);
When kinds collide (e.g. both core and Cilium have NetworkPolicy), the first-registered entry keeps the short name. All entries are always reachable via domain-qualified names (api_version/Kind).
strict
Optional. Boolean, default 0. Governs what happens when a constructor key matches no declared attribute, at any nesting level. With the default 0 the field is kept and re-emitted by TO_JSON (see "UNKNOWN FIELDS" in IO::K8s::Role::Resource); with 1 it dies instead, with Unknown field '<name>' for <class>.
my $k8s = IO::K8s->new(strict => 1);
$k8s->new_object('Pod', { spec => { bogusField => 1 } });
# dies: Unknown field 'bogusField' for IO::K8s::Api::Core::V1::PodSpec
strict is read by "inflate", "new_object", "json_to_object" and "struct_to_object"; "load" and "load_yaml" inherit it because both build on inflate/new_object. It applies for the duration of that one call, including every nested object it constructs along the way.
Since k99, IO::K8s::List, the envelope a list Kind inflates to, also composes IO::K8s::Role::Resource: its own top-level keys are preserved and checked under strict exactly like any other resource's, alongside the objects inside items, each through its own class.
unknown_kinds
Optional. String, default ''. Governs what "inflate" and "new_object" do when a document's apiVersion/kind (or an explicit api_version argument) amount to a GVK request that resolves to no registered class -- built-in, CRD-registered via add()/add_crd(), or AutoGen'd from openapi_spec. With the default '' this keeps failing closed exactly as without the option, dying with:
Cannot resolve Kubernetes GVK: kind '<kind>', apiVersion '<apiVersion>'
With unknown_kinds => 'unstructured', that failure instead builds an IO::K8s::Unstructured from the document: apiVersion, kind and metadata land on typed attributes, everything else round-trips through the _unknown_fields bag (D1) exactly as an undeclared field on any other class does.
my $k8s = IO::K8s->new(unknown_kinds => 'unstructured');
my $obj = $k8s->inflate($document); # an IO::K8s::Unstructured, not a die
Any value other than the literal string 'unstructured' -- including one left unset -- keeps the fail-closed default; this is strictly opt-in. strict => 1 combined with this option still dies on a registered Kind with an unexpected field the normal way; the Unstructured envelope itself is exempt from strict, since every field beyond apiVersion/kind/metadata is precisely what it exists to preserve.
openapi_spec
Optional. The OpenAPI v2 specification from a Kubernetes cluster. When provided, enables auto-generation of classes for types not found in the built-in classes.
class_namespaces
Optional. ArrayRef of namespace prefixes to search for classes before checking IO::K8s built-ins. Useful for providing your own implementations.
resource_map
HashRef mapping short names (like Pod) and domain-qualified names (like networking.k8s.io/v1/NetworkPolicy) to class paths. Defaults to built-in mappings for standard Kubernetes resources. Each instance gets its own copy, so modifications via add() do not affect other instances.
json
A JSON::MaybeXS encoder/decoder configured with utf8 => 1 and canonical => 1. Used by "object_to_json", "json_to_object" and "inflate" for their default encoding/decoding. Override at construction when the caller needs a different encoder (for example, to disable canonical for tighter output, or to swap in a different backend):
my $k8s = IO::K8s->new(json => JSON::MaybeXS->new(utf8 => 1));
METHODS
add
$k8s->add('IO::K8s::Cilium'); # class name
$k8s->add(IO::K8s::Cilium->new); # instance
$k8s->add({ MyKind => '+My::Class' }); # raw hashref
$k8s->add($provider1, $provider2); # multiple at once
Merge external resource maps into this instance. Accepts class names, object instances with a resource_map() method, or plain hashrefs.
When a kind name already exists in the resource map (collision), the first-registered entry keeps the short name. Both the existing and new entries are registered under domain-qualified names (api_version/Kind) so they remain reachable.
Returns $self for chaining.
load
my $resources = $k8s->load('myapp.pk8s');
Load a .pk8s manifest file and return an ArrayRef of IO::K8s objects.
Trust boundary: A .pk8s manifest is Perl code, not data. The loader evals the file content in-process, so a .pk8s file can execute arbitrary code with the full privileges of the running program. Only load .pk8s files from sources you trust. For data-only manifests (YAML or JSON), use load_yaml, which parses without executing any code.
The .pk8s file format is Perl code with a DSL for defining Kubernetes resources:
# myapp.pk8s
ConfigMap {
name => 'my-config',
namespace => 'default',
data => { key => 'value' }
};
Deployment {
name => 'my-app',
namespace => 'default',
spec => {
replicas => 3,
selector => { matchLabels => { app => 'my-app' } },
template => {
metadata => { labels => { app => 'my-app' } },
spec => {
containers => [{
name => 'app',
image => 'my-app:latest',
}],
},
},
}
};
Inside {} blocks, name, namespace, labels, and annotations are automatically moved to metadata.
With CRDs (requires openapi_spec):
my $k8s = IO::K8s->new(openapi_spec => $spec);
my $resources = $k8s->load('helmchart.pk8s');
load_yaml
my $resources = $k8s->load_yaml('manifest.yaml');
my $resources = $k8s->load_yaml($yaml_string);
Load a YAML manifest file (or YAML string) and return an ArrayRef of IO::K8s objects. Supports multi-document YAML (separated by ---).
This method validates declared fields against the Kubernetes types. A declared field with the wrong type throws an error. By default, an undeclared field is kept for forward-compatible round-tripping; construct IO::K8s with strict => 1 to reject it instead. This is useful for validating manifests before applying them to a cluster.
# Validate a manifest file and reject undeclared fields
my $strict_k8s = IO::K8s->new(strict => 1);
eval {
my $objs = $strict_k8s->load_yaml('deployment.yaml');
say "Valid! Contains " . scalar(@$objs) . " resources";
};
if ($@) {
say "Invalid manifest: $@";
}
Options:
- collect_errors => 1
-
Collect all validation errors instead of stopping at the first one. Returns a list of
(objects, errors)whereobjectscontains successfully parsed resources anderrorsis an ArrayRef of error messages.my ($objs, $errors) = $k8s->load_yaml($yaml, collect_errors => 1); if (@$errors) { say "Found " . scalar(@$errors) . " errors:"; say " - $_" for @$errors; }
new_object
my $pod = $k8s->new_object('Pod', %args);
my $pod = $k8s->new_object('Pod', \%args);
my $np = $k8s->new_object('NetworkPolicy', \%args, 'cilium.io/v2');
my $np = $k8s->new_object('cilium.io/v2/NetworkPolicy', \%args);
Create a new Kubernetes object of the given type. The type can be a short name (like Pod), a domain-qualified name (like cilium.io/v2/NetworkPolicy), or a full class path (like My::StaticWebSite).
A bare one-word name is always read as a Kubernetes Kind, never as a package name, so it resolves through the resource map, class_namespaces, IO::K8s::<Kind> and auto-generation -- a same-named top-level distribution that happens to be installed is not consulted. To name a single-segment class of your own, prefix it: $k8s->new_object('+Widget', \%args).
An optional third argument specifies the api_version to disambiguate when multiple providers register the same kind name.
An apiVersion key inside the params hash is honoured symmetrically with "inflate": it is treated as the exact GVK the caller wants, and the short name resolves against it instead of whichever version the class defaults to. When a positional api_version is also given and the two disagree -- including one being defined and the other undef -- this croaks rather than picking one (k62):
new_object: conflicting apiVersion for kind 'NetworkPolicy' --
params hash says 'cilium.io/v2', positional argument says 'networking.k8s.io/v1'
If the name is domain-qualified (like cilium.io/v2/NetworkPolicy) or an explicit api_version argument is given, it is a GVK (Group/Version/Kind) request. When such a request cannot be resolved to a class, the call dies rather than silently falling back to a different version or a similarly-named class:
Cannot resolve Kubernetes GVK: kind 'UnknownKind', apiVersion 'nonexistent.io/v1'
A bare unqualified name is not a GVK request and is exempt from this check -- as described above, it falls back to IO::K8s::<Kind>, and if that class doesn't exist either, the failure is Perl's own module-loading error (Can't locate ... in @INC), not the GVK error.
This same fail-closed behaviour applies uniformly across new_object, inflate, json_to_object and struct_to_object.
inflate
my $obj = $k8s->inflate($json_string);
my $obj = $k8s->inflate(\%hashref);
Inflate a JSON string or hashref into a typed IO::K8s object. The class is auto-detected from the kind field in the data. When external resource maps have been added via add(), the apiVersion field is used to disambiguate colliding kind names.
If kind/apiVersion amount to a GVK request that cannot be resolved, this dies with the same fail-closed error as "new_object" -- see there for the exact message and the bare-Kind exemption.
json_to_object
my $obj = $k8s->json_to_object($json_with_kind);
my $obj = $k8s->json_to_object('Pod', $json_string);
Convert JSON to an IO::K8s object. With one argument, auto-detects the class from kind. With two arguments, uses the specified class.
When the class argument is a GVK request (domain-qualified, or paired with an api_version) that cannot be resolved, this dies with the same fail-closed error as "new_object" -- see there for the exact message and the bare-Kind exemption.
struct_to_object
my $obj = $k8s->struct_to_object(\%hashref_with_kind);
my $obj = $k8s->struct_to_object('Pod', \%hashref);
Convert a Perl hashref to an IO::K8s object. With one argument, auto-detects the class from kind. With two arguments, uses the specified class.
When the class argument is a GVK request (domain-qualified, or paired with an api_version) that cannot be resolved, this dies with the same fail-closed error as "new_object" -- see there for the exact message and the bare-Kind exemption.
If the target class provides a FROM_STRUCT class method, it is called as $class->FROM_STRUCT($struct, $k8s) and its return value is used as-is, bypassing the generic field-by-field inflation. This is the hook for union types that serialize as a bare alternative rather than as a hashref of attributes -- see IO::K8s::ApiextensionsApiserver::Pkg::Apis::Apiextensions::V1::JSONSchemaPropsOrBool and its siblings, where additionalProperties: false has to stay a boolean instead of collapsing into an empty object. A class implementing FROM_STRUCT is responsible for its own TO_JSON as well, so that the two directions stay symmetric.
object_to_json
my $json = $k8s->object_to_json($obj);
Serialize an IO::K8s object to JSON.
object_to_struct
my $hashref = $k8s->object_to_struct($obj);
Convert an IO::K8s object to a plain Perl hashref.
expand_class
my $class = $k8s->expand_class('Pod');
my $class = $k8s->expand_class('cilium.io/v2/NetworkPolicy');
my $class = $k8s->expand_class('NetworkPolicy', 'cilium.io/v2');
Resolve a name to a Perl class. The name can be a short Kind, a domain-qualified api_version/Kind, or a full class path (prefix with + for a verbatim class name, or write the IO::K8s:: prefix in full). Returns the class name as a string -- this method does not load the class.
An explicit api_version makes the lookup an exact GVK request: when no class can confirm the requested version, returns undef. The qualified api_version/Kind form is checked first against the resource map, then a short-name key whose mapped class itself reports the requested api_version, then the openapi_spec for an auto-generated class. Anything else fails closed rather than substituting a different version (k17).
A bare unqualified name is not a GVK request: it falls through to IO::K8s::<Kind> and then to auto-generation, and a name that resolves to nothing fails with the usual module-loading exception, not the GVK error.
load_class
$k8s->load_class('IO::K8s::Api::Core::V1::Pod');
Load (require) a class by name. Used internally after "expand_class" to make sure the class is in %INC before the caller hands it to $class->new. Dies with the usual Can't locate ... in @INC message when the class is not installable.
Successful loads are remembered process-wide, so the second and every later call for the same name costs a hash lookup instead of a require. Failures are not remembered: a name that did not load is attempted again on the next call, which is what keeps a package that only becomes available later -- one defined at runtime and registered in %INC, or a module installed mid-process -- reachable.
CILIUM CRD SUPPORT
IO::K8s includes IO::K8s::Cilium with 31 resource-map entries: 22 short-name Kinds (17 cilium.io/v2 + 5 cilium.io/v2alpha1) and 9 domain-qualified back-compat tracks for v2alpha1 BGP/CIDR/LoadBalancerIPPool, CiliumBGPPeeringPolicy, and CiliumExternalWorkload. The compatibility tracks remain reachable for older clusters without displacing the current short-name Kind. These are not loaded by default -- opt in at construction:
my $k8s = IO::K8s->new(with => ['IO::K8s::Cilium']);
my $cnp = $k8s->new_object('CiliumNetworkPolicy',
metadata => { name => 'allow-dns', namespace => 'kube-system' },
spec => { endpointSelector => { matchLabels => { app => 'dns' } } },
);
print $cnp->to_yaml;
All Cilium kinds are Cilium-prefixed, so there are no collisions with core Kubernetes kind names.
EXTERNAL RESOURCE MAPS
IO::K8s supports merging resource maps from external packages (like IO::K8s::Cilium for Cilium CRDs). This allows multiple packages to provide typed Kubernetes objects that work together.
Writing a resource map provider
Create a class that consumes IO::K8s::Role::ResourceMap:
package My::CRD::Provider;
use Moo;
with 'IO::K8s::Role::ResourceMap';
sub resource_map {
return {
MyCustomKind => '+My::CRD::V1::MyCustomKind',
};
}
See IO::K8s::Cilium for a real-world provider with 22 current short-name Kinds and nine domain-qualified compatibility tracks.
Collision handling
When two providers register the same kind name, the first-registered entry keeps the short name. Both entries are always reachable via domain-qualified names (api_version/Kind):
my $k8s = IO::K8s->new(with => ['My::Firewall::Provider']);
# Short name -> core (first-registered)
$k8s->expand_class('NetworkPolicy');
# -> IO::K8s::Api::Networking::V1::NetworkPolicy
# Domain-qualified -> specific version
$k8s->expand_class('firewall.example.com/v1/NetworkPolicy');
# -> My::Firewall::V1::NetworkPolicy
# api_version parameter for disambiguation
$k8s->expand_class('NetworkPolicy', 'firewall.example.com/v1');
# -> My::Firewall::V1::NetworkPolicy
Disambiguation in pk8s DSL
In .pk8s manifest files, pass the api_version as a second argument:
# Core NetworkPolicy (default)
NetworkPolicy { name => 'deny-all', spec => { ... } };
# Firewall NetworkPolicy (disambiguated, no comma - like grep/map syntax)
NetworkPolicy { name => 'deny-all', spec => { ... } } 'firewall.example.com/v1';
UPGRADING FROM PREVIOUS VERSIONS
WARNING: Version 1.00 contains breaking changes!
This version has been completely rewritten. Key changes that may affect your code:
Moose to Moo migration
All classes now use Moo instead of Moose. This means faster startup and lighter dependencies, but Moose-specific features (meta introspection, etc.) are no longer available.
List classes removed
Individual
*Listclasses (e.g.,IO::K8s::Api::Core::V1::PodList) have been replaced with the unified IO::K8s::List class. The old class names emitted deprecation warnings for a while; as of this release they have been dropped from this distribution entirely. If you need the old name to fail loudly instead of silently resolving to a stale prior release, install IO::K8s::Deprecated, which ships CPAN redirect stubs for all 76 of them.Updated to Kubernetes v1.37 API
API objects have been updated from v1.14 to v1.37. Some fields may have changed, been added, or removed according to upstream Kubernetes API changes.
New Role for namespaced resources
Resources that are namespaced now consume IO::K8s::Role::Namespaced. Use
$class->does('IO::K8s::Role::Namespaced')to check if a resource is namespace-scoped.
SEE ALSO
Kubernetes::REST - REST client for the Kubernetes API, uses IO::K8s for typed request/response objects
IO::K8s::Deprecated - CPAN redirect stubs for IO::K8s module names that were renamed or removed
Bundled CRD providers: IO::K8s::Cilium, IO::K8s::Traefik, IO::K8s::CertManager, IO::K8s::K3s, IO::K8s::GatewayAPI, IO::K8s::AgentSandbox, IO::K8s::PrometheusOperator, IO::K8s::VolumeSnapshot, and IO::K8s::ExternalSecrets
Kubernetes::REST::Example - Comprehensive examples for using Kubernetes::REST with IO::K8s against a real cluster (Minikube, K3s, etc.)
https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.37/
BUGS and SOURCE
The source code is located here: https://github.com/pplu/io-k8s-p5
Please report bugs to: https://github.com/pplu/io-k8s-p5/issues
COPYRIGHT and LICENSE
Copyright (c) 2018 by Jose Luis Martinez Copyright (c) 2026 by Torsten Raudssus
This code is distributed under the Apache 2 License. The full text of the license can be found in the LICENSE file included with this module.
AUTHORS
Torsten Raudssus <torsten@raudssus.de> (current maintainer)
Jose Luis Martinez <jlmartin@cpan.org> (original author)
SUPPORT
Issues
Please report bugs and feature requests on GitHub at https://github.com/pplu/io-k8s-p5/issues.
CONTRIBUTING
Contributions are welcome! Please fork the repository and submit a pull request.
AUTHORS
Torsten Raudssus <getty@cpan.org>
Jose Luis Martinez Torres <jlmartin@cpan.org>
COPYRIGHT AND LICENSE
This software is Copyright (c) 2018-2026 by Jose Luis Martinez Torres <jlmartin@cpan.org>.
This is free software, licensed under:
The Apache License, Version 2.0, January 2004