NAME
IO::K8s::CRD - Turn CustomResourceDefinition manifests into IO::K8s classes
VERSION
version 1.108
SYNOPSIS
use IO::K8s;
my $k8s = IO::K8s->new;
$k8s->add_crd('crds/knobs.yaml'); # a path, YAML/JSON text, a hashref,
# a CustomResourceDefinition object,
# or an arrayref of those
my $knob = $k8s->new_object('Knob', ...); # storage version
my $old = $k8s->new_object('opts.example.com/v1alpha1/Knob', ...);
# The pieces, for callers that want them separately:
my $crds = IO::K8s::CRD->load($input); # plain hashrefs
my $versions = IO::K8s::CRD->served_versions($crds->[0]);
my $classes = IO::K8s::CRD->generate($crds->[0], 'My::Namespace');
# Class-to-CRD (D9), the other direction:
my $crd = My::StaticWebSite->to_crd; # single-version
my $crd = IO::K8s::CRD->new( # multi-version
classes => [ My::StaticWebSite::V1beta1, My::StaticWebSite::V1 ],
storage => 'v1',
);
DESCRIPTION
The manifest-to-class half of D10 in the CRD design: a CustomResourceDefinition is loaded from whatever form the caller has, every served version of it becomes one IO::K8s::AutoGen class (with nested classes for every object below spec), and "add_crd" in IO::K8s registers them the way a provider's resource map is registered. Nothing here writes files; IO::K8s::CRD::Emitter renders the same classes as source for the checked-in case.
load
my $crds = IO::K8s::CRD->load($input);
Normalizes $input to an arrayref of plain CRD hashrefs. Accepts a CustomResourceDefinition object (anything with TO_JSON), a hashref, YAML or JSON text (multi-document YAML yields several), a path to such a file, or an arrayref of any of those. Dies on a document that is not a CustomResourceDefinition or lacks spec.group, spec.names.kind or spec.versions.
served_versions
my $versions = IO::K8s::CRD->served_versions($crd);
The served versions of one loaded CRD, in manifest order, each as { name, api_version, storage, schema } where schema is the version's openAPIV3Schema (an empty type: object when the manifest has none). Dies when no version is served.
generate
my $classes = IO::K8s::CRD->generate($crd, $namespace);
my $classes = IO::K8s::CRD->generate($crd, $namespace, reuse_core => 0);
Generates one IO::K8s::AutoGen class per served version under $namespace and returns { $api_version => $class, ..., storage => $api_version }. %opts is forwarded to "get_or_generate" in IO::K8s::AutoGen as-is; reuse_core (D5, default 1) is the option most callers touch, controlling whether a nested schema that matches a shipped core class's shape is typed as that class instead of a generated nested one. The storage version is the one the manifest marks; when none is marked (an invalid manifest, but a common one in hand-written fixtures) the last served version is used. Each class carries the CRD's kind, names.plural and scope, and every object with properties below it is a nested class (see IO::K8s::AutoGen).
Classes are generated under $namespace\::_CRD, never $namespace itself. IO::K8s::AutoGen caches by class name, and the class name is derived from the namespace plus the group/version/Kind (see "get_or_generate" in IO::K8s::AutoGen) -- not from the schema, and not differently for a CRD manifest than for an openapi_spec definition of the same GVK. Under a shared namespace the two paths would therefore build the identical class name for the identical GVK and alias in AutoGen's cache: whichever ran first would win the slot, and the CRD's own schema-derived class would silently be discarded (or would silently clobber the openapi_spec one) while $k8s->add_crd still reported success. The ::_CRD sub-namespace rules that out.
Calling generate a second time for the same group/version/Kind under the same $namespace returns the class generated the first time, silently, even when the schema in $crd has since changed -- the sub-namespace does not change that, it only stops the CRD path from colliding with a different one. Iterating on an edited manifest needs a fresh $namespace (in practice: a fresh IO::K8s instance, since "add_crd" in IO::K8s always passes its own _autogen_namespace). Generated classes live for the life of the process regardless -- see "add_crd" in IO::K8s's POD for what that costs a long-running caller that reloads manifests in a loop.
crd_for_class
my $crd = IO::K8s::CRD::crd_for_class($class);
my $crd = $class->to_crd; # installed on every APIObject class, see IO::K8s::Role::APIObject
D9's DSL-to-schema direction. Builds a single-version CustomResourceDefinition object from a top-level $class's own attribute registry: spec.group and the one spec.versions[] entry's name come from splitting $class->api_version on the last /; spec.scope is Namespaced when $class composes IO::K8s::Role::Namespaced, else Cluster; spec.names comes from $class->kind, $class->resource_plural (singular is lc(kind), listKind is "${kind}List"); metadata.name is "$plural.$group". The schema itself is "_schema_for_class".
The schema is generated from the registry, but it is not a lossless DSL-to-schema-to-DSL round-trip through add_crd. Quantity and [Quantity] export only type: string (or string array items), so the Quantity constraint cannot be reconstructed. During the reverse IO::K8s::AutoGen inference, typed maps whose values are Int, Num, Bool, Quantity, Time or IntOrStr re-import as the opaque { Str => 1 } form. Scalar arrays [Num], [Quantity], [Time] and [IntOrStr] re-import as [Str]; [Str], [Int] and [Bool] retain their scalar element type.
The single-version shorthand for "new": IO::K8s::CRD::crd_for_class($class) is exactly IO::K8s::CRD->new(classes => [$class], storage => $version) where $version is $class's own version (split out of api_version the same way). See "new" for the object this returns -- a real, fully typed CustomResourceDefinition, not a bare hashref.
new
my $crd = IO::K8s::CRD->new(
classes => [ $class_v1, $class_v1beta1 ], # one class per version
storage => 'v1', # names one of their versions
);
D9: assembles ONE multi-version CustomResourceDefinition object from one typed class per version -- the assembly layer over "crd_for_class"/ $class->to_crd, which is now exactly IO::K8s::CRD->new(classes => [$class], storage => $version) under the hood (see "crd_for_class").
Every class in classes is one API version of the SAME CRD, so they must agree on spec.group, names.kind (from $class->kind), names.plural (from $class->resource_plural) and spec.scope (IO::K8s::Role::Namespaced or not) -- a mismatch on any of those croaks, naming the field and which class supplied which value. They must also each be a genuinely distinct version: two classes naming the same version (e.g. two whose api_version both end .../v1) would otherwise produce two identically-named spec.versions[] entries -- a shape the apiserver rejects -- so that croaks too, naming the repeated version. Each class becomes one spec.versions[] entry (schema from "_schema_for_class", applied per class): every entry is served = true>, and exactly the one whose name matches storage gets storage = true> (the rest storage = false>). storage is required and must name one of the given classes' own versions, or the call croaks.
Versions land in spec.versions in the order classes was given, not re-sorted by a Kubernetes-style version precedence -- the caller already chose an order (oldest-first is conventional, but not enforced), and silently reordering it would be a surprise, not a service. Pass classes in whatever order the manifest should show.
classes must be a non-empty arrayref, or the call croaks.
_schema_for_class
my $schema = IO::K8s::CRD::_schema_for_class($class);
The openAPIV3Schema for one version of $class (D9): walks $class->_k8s_attr_info and mirrors IO::K8s::AutoGen's schema-to-DSL mapping (_schema_to_type_spec) field by field, in reverse, keyed by each field's json_key.
For a top-level Kind ($class->can('_is_resource')) the registry's own metadata entry is skipped and apiVersion/kind/metadata get the standard envelope stubs instead -- the same three fields IO::K8s::AutoGen's %role_supplied excludes when building attributes FROM a schema (see _generate_class there), lined up here in the opposite direction. A nested class reached through a field is walked exactly the same way, minus the stubs (it is never itself _is_resource).
Recursion guards against cycles by tracking the classes already on the CURRENT path (the second, internal $seen argument -- never pass it from outside): a class that references itself, directly or through a reused core class, becomes an opaque { type => 'object', 'x-kubernetes-preserve-unknown-fields' => true } stub at the repeat instead of recursing forever, the same stub the opaque { Str => 1 } map gets. This is deliberately PATH-scoped, not global: a class that legitimately appears more than once as unrelated siblings (LabelSelector, reused all over a real CRD schema per D5) must not be flattened to that stub on its second, unrelated appearance.
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