NAME

IO::K8s::AutoGen - Dynamically generate IO::K8s classes from OpenAPI schema

VERSION

version 1.108

SYNOPSIS

use IO::K8s::AutoGen;

# Generate a class from OpenAPI schema
my $class = IO::K8s::AutoGen::get_or_generate(
    'helm.cattle.io.v1.HelmChart',
    $schema_definition,
    $all_definitions,
    'IO::K8s::_AUTOGEN_abc123',  # namespace
);

# The class is now available and works like any IO::K8s class
my $obj = $class->new(metadata => $meta, spec => $spec);
my $json = $obj->TO_JSON;

DESCRIPTION

This module dynamically generates Moo classes for Kubernetes custom resources that don't have pre-generated IO::K8s classes.

Generated classes use IO::K8s::Resource as their base, so they have:

  • The k8s DSL for attribute definitions

  • TO_JSON / to_json serialization

  • _k8s_attr_info for inflate support

  • All standard IO::K8s behavior

Generated classes are placed in a unique namespace per IO::K8s instance to avoid collisions:

IO::K8s::_AUTOGEN_abc123::helm::cattle::io::v1::HelmChart

Each OpenAPI property also carries its field options (D3 of the CRD design) into the generated class, through the same k8s option hash a hand-written class would use (see "k8s" in IO::K8s::Resource): the schema's required list becomes the field's required = 'schema'> option, so a generated class records which fields the schema demands -- available to to_crd and to "_k8s_attr_info" in IO::K8s::Resource -- without enforcing them at construction. An OpenAPI-required field can still be absent from a real cluster document (a server-side default, a status object not yet populated), and rejecting it at inflate would reject valid data; use required = 1> for a field that must always be enforced (see "k8s" in IO::K8s::Resource). enum, minimum, maximum, pattern, default, description, nullable and x-kubernetes-preserve-unknown-fields are carried the same way, so a generated class enforces enum, range and pattern values exactly like a hand-written one that declares the same options. For an array property, enum/minimum/maximum/pattern are read off items and lifted onto the field's own options, since it is each element that is constrained, not the array itself. nullable and x-kubernetes-preserve-unknown-fields are read as JSON booleans, not Perl truthiness, so the wire string "false" is false; a default of JSON null -- common on a nullable: true field -- is treated as no default at all, not as a default of undef, which the DSL's own field-option check would otherwise refuse.

A malformed schema option is dropped rather than failing the whole class: the client-side check is a convenience, the API server validates every value regardless, and dropping it loses no data. This covers an enum that is empty, has duplicate entries, or contains a JSON null; a pattern that does not compile as a Perl regex; a minimum/maximum where either bound is not a number or minimum exceeds maximum; and a default the field cannot hold -- the wrong type, or a value outside its own enum or range.

An inline type: object schema with its own non-empty properties also becomes a typed class now (D10, k94), named after its place in the parent -- <Parent>::<Prop>, with an Item / Value suffix for array items and map values shaped the same way -- so its properties get field options exactly like a class built from a $ref. Only a property-less type: object and an additionalProperties-only map (no properties of their own) stay the existing opaque hash of strings, with nothing underneath to attach options to. Scalar properties carry their options at every level regardless.

A path-derived nested class name that would run past Perl's 251-character limit on a fully qualified identifier (cert-manager's CRDs inline a full PodTemplateSpec several levels into a Challenge's spec, and the namespace prefix an AutoGen instance generates under adds still more) is shortened instead of failing class generation: <root>::_<10 hex chars>, the root being the top-level generated class (the Kind) the nesting started from and the hex digits a Digest::SHA::sha1_hex of the full, unshortened logical name. The logical path is not lost -- "class_path($class)" and "class_root($class)" recover it -- and a name collision between two different schema keys is still detected against that full logical name, never against the (much smaller) space of possibly-shortened names.

A nested object -- or an array's items, or a map's additionalProperties -- whose property set exactly matches a shipped core or apimachinery class's own key set is typed as that class instead of a new nested one (D5, reuse_core, default on): a CRD's inline LabelSelector or HTTPHeader-shaped struct becomes the real IO::K8s class rather than a per-provider copy. get_or_generate's reuse_core => 0 option turns this off and restores the pre-D5 behavior (every inline object becomes its own nested class, per D10 above).

A name match alone is not enough to reuse a class -- it only decides which classes "core_class_for_shape(\@json_keys)" lists as candidates. The reuse decision applies three further checks: a shape under two keys is never reused (a single shared key name -- {value}, {name}, ... -- is common enough by accident that this alone rules out most of it); every remaining candidate must be type-compatible with the schema, key by key (a schema string field can't reuse a class that declares the same-named field as an array, for instance -- see "core_class_for_shape(\@json_keys)" below for the full compatibility table); and where several candidates survive that filter, they are reused as the preferred one (apimachinery's LabelSelector family first, then the rest of Meta::V1, then Core::V1, then alphabetically) only when they are wire-identical -- the same type per key (ignoring required-ness: a field being optional on one shipped class and mandatory on another doesn't change what value it holds) and the same referenced class per key, where a key has one. {name,value} matches three unrelated Core::V1 leaf structs -- HTTPHeader, PodDNSConfigOption, Sysctl -- wire-identical despite one of the three having two optional fields where the other two have two required ones, so it reuses the preferred one, HTTPHeader. {key,operator,values} matches LabelSelectorRequirement and FieldSelectorRequirement (Meta::V1) as well as NodeSelectorRequirement (Core::V1) -- also wire-identical despite spanning two API areas -- so it reuses LabelSelectorRequirement. A shape shared by candidates that are NOT wire-identical -- an optional field naming a different referenced class, e.g. {metadata,spec} matching PodTemplateSpec, JobTemplateSpec, ResourceClaimTemplateSpec and others, each with spec typed differently -- stays a nested class rather than guess which one is meant.

NAME

IO::K8s::AutoGen - Dynamically generate IO::K8s classes from OpenAPI schema

FUNCTIONS

get_or_generate($def_name, $schema, $all_defs, $namespace)

Generate (or return cached) class for the given OpenAPI definition.

Extra positional options after $namespace pin the identity of a top-level object (api_version => ..., kind => ..., resource_plural => ..., is_namespaced => ...); the generated class then also composes IO::K8s::Role::APIObject. reuse_core => 0|1 (default 1) controls D5's core-class reuse; see above.

When api_version (or kind / resource_plural) is supplied, the generated class installs fixed-value methods for each. These are fixed identity, not writable fields: passing an argument croaks rather than silently retargeting the object (k67, k70) -- the same contract the hand-written CRD template installs via IO::K8s::APIObject.

This function fails closed on input it cannot generate a faithful class from, rather than dropping fields or inventing a wrong type. It croaks when:

  • a property, an array's items, or an additionalProperties schema carries a $ref to a definition not present in $all_defs. A partial spec that references definitions it does not ship used to generate the class anyway, minus those fields -- losing their data on every round-trip. It now dies naming the $ref and where it appeared (k56).

  • additionalProperties is a reference that is neither a schema object nor a JSON boolean; the message names the class and field (k55).

  • the schema's x-kubernetes-group-version-kind metadata is ambiguous for the requested api_version, or names no entry matching it -- the GVK selection fails closed rather than pick a version.

One partial-spec shape still generates successfully by design: a top-level CRD schema whose metadata $refs the standard ObjectMeta without shipping its definition. metadata is supplied by the role and is skipped before its $ref is looked at (k60), so this common single-schema hand-in does not trip the unresolved-$ref refusal. A side effect of that skip: when $all_defs does carry ObjectMeta and nothing else references it, it no longer appears in "generated_classes()".

def_to_class($def_name, $namespace)

Convert OpenAPI definition name to Perl class name.

class_to_def($class)

Convert Perl class name back to OpenAPI definition name.

is_autogen($class)

Returns true if the class was auto-generated.

clear_cache()

Clear the generated class cache. Classes generated before the call keep working -- their packages already exist and nothing here touches them -- but regenerating the same names into the same namespace afterward is unsupported: Moo cannot rebuild an existing package, and "class_path($class)" / "class_root($class)" forget what they knew about the classes this cleared.

generated_classes()

List all generated class names.

class_description($class)

The schema description a generated class was built from, or undef when the schema carried none. Used by IO::K8s::CRD::Emitter to fill in a rendered class's # ABSTRACT line.

class_root($class)

The top-level generated class (the Kind class) $class's nesting started from, or $class itself when it already is a root -- including when $class is not something AutoGen generated at all. Never undef.

class_path($class)

The ::-joined field path $class sits at below its "class_root($class)" (Spec::Acme::SolversItem::...), recorded even when $class's own Perl name had to be shortened past Perl's identifier limit. undef for a root class or for a class AutoGen did not generate through nested-object handling.

core_class_for_shape(\@json_keys)

my @classes = IO::K8s::AutoGen::core_class_for_shape([qw(key operator values)]);

Every shipped core / apimachinery class (under IO::K8s::Api and IO::K8s::Apimachinery) whose own key set is exactly @json_keys, most preferred first (apimachinery's LabelSelector, then the rest of Meta::V1, then Core::V1, then alphabetically). Empty when no shipped class has that exact shape. A class's own metadata counts as part of its shape only when the class is an embedded type (PodTemplateSpec: {metadata,spec}, a real schema-visible field); a top-level Kind's metadata is supplied by IO::K8s::Role::APIObject outside the schema's own properties and is dropped, so e.g. Pod's indexed shape is {spec,status}. This is a name match only, by key set alone -- it says nothing about whether reusing any listed class is actually safe for a given schema; that is D5's reuse_core reuse decision (see above), which consults this same index but additionally requires a type-compatible candidate (a per-key check against the schema: string -- including x-kubernetes-int-or-string and format: date-time -- matches is_str/is_int_or_string/is_quantity/is_time; integer matches is_int or is_int_or_string; number matches is_num; boolean matches is_bool; array matches any is_array_of_*; object, whether the schema property has properties of its own or is a map, matches is_object, is_inline_struct or any is_hash_of_*), and -- when several type-compatible candidates remain -- requires them to be wire-identical before picking the preferred one.

The index itself is precomputed and shipped as IO::K8s::AutoGen::CoreShapes, regenerated by maint/core-shape-index-gen.pl and checked against the shipped classes by t/85_core_shape_index.t; where that module is missing it is rebuilt on first call by loading every class under the two trees instead, which costs seconds rather than milliseconds but produces the same index. Either way, only the classes a looked-up shape actually names are loaded -- the returned names are always loadable, loaded classes, since the reuse decision reads their attribute registries.

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