NAME

IO::K8s::Resource - Base class for all Kubernetes resources

VERSION

version 1.110

SYNOPSIS

package IO::K8s::Api::Core::V1::Pod;
use IO::K8s::Resource;

k8s apiVersion => 'Str';
k8s kind => 'Str';
k8s metadata => 'Meta::V1::ObjectMeta';
k8s spec => 'Core::V1::PodSpec';

1;

DESCRIPTION

Base class that sets up Moo, inheritance, and provides the k8s DSL. Just use IO::K8s::Resource; - no need for use Moo or extends.

NAME

IO::K8s::Resource - Base class for Kubernetes resources

EXPORTED FUNCTIONS

k8s

k8s name => 'Str';
k8s replicas => 'Int';
k8s ratio => 'Num';                        # JSON number, unquoted on the wire
k8s suspend => 'Bool';
k8s spec => 'Core::V1::PodSpec';           # Short class name
k8s containers => ['Core::V1::Container']; # Array of objects
k8s labels => { Str => 1 };                # String map (lenient legacy spelling)
k8s data => HashRef[Str];                  # String map, strict
k8s raw => Opaque;                         # Free map, values untyped (also bare HashRef)
k8s limits => { Quantity => 1 };           # Typed value map (also Int/Num/Bool/Time/IntOrStr)
k8s ports => HashRef[Int];                 # The same, as HashRef[X]
k8s rows => [ {} ];                         # Array of opaque hashes
k8s matrix => [ [] ];                       # Array of opaque arrays
k8s spec => {                              # Inline struct
    replicas => Int,
    selector => Str,
    template => { Str => 1 },
};

{ Str => 1 } and HashRef[Str] declare a string map -- labels, annotations, ConfigMap data, a map[string]string upstream: TO_JSON puts every scalar value out as a JSON string, so labels => { v => 5 } goes out as {"v":"5"}. HashRef[Str] refuses a reference value at construction. { Str => 1 } keeps accepting any value, since it was the opaque map before 1.109: a reference value passes through unchanged and warns once per class and field (category deprecated). Opaque, or a bare HashRef, is the free map for genuinely free-form values such as fieldsV1 or a RawExtension: any value, copied through as it is. Opaque takes no parameters -- Opaque[...] dies at declaration. Declare a free field as Opaque: { Str => 1 } was the opaque map up to 1.108, but is a string map now, so to_crd emits additionalProperties: string for it instead of x-kubernetes-preserve-unknown-fields, and a reference value warns. { Quantity => 1 } (and Int, Num, Bool, Time, IntOrStr) instead validates every value against that scalar type, so a map upstream declares as map[X]Quantity rejects a bad value at construction rather than at the API server; HashRef[Quantity] and the other HashRef[X], including HashRef[InstanceOf['Some::Class']] for a map of objects, are the same declarations spelled the other way.

Inline structs auto-generate an inner class (e.g. MyClass::_Spec) with the declared fields. Hashrefs are auto-coerced to the inner class on construction, through the very same coercion a named nested class gets -- so a plain container inside the hashref is copied one level rather than stored by reference. Before 1.108, ->new was the one route into an inline-struct field that aliased the caller's structure while inflate, struct_to_object and FROM_HASH already copied it; the four now agree.

A named nested class coerces the same way (since 1.108): a plain hashref passed to ->new or to the setter of an is_object, is_array_of_objects or is_hash_of_objects field is built into that class -- element-wise for an array, value-wise for a map -- so

IO::K8s::Traefik::V1alpha1::Middleware->new(
    spec => { rateLimit => { average => 100 } });

no longer has to pre-build MiddlewareSpec and RateLimit by hand. It goes through the same inflation "FROM_HASH" in IO::K8s::Role::Resource uses, so boolean spellings and the unknown-field policy behave identically on both routes. A value that is already an object is passed through untouched, and anything that is neither a hashref nor an object is left to the type constraint to reject.

The one exception is a class that inflates through FROM_STRUCT -- the apiextensions union classes V1::JSON and JSONSchemaPropsOr*, which serialize as the bare value they hold. Such a field takes every defined value, not only a hashref, exactly as inflation does:

IO::K8s::K3s::V1::HelmChartSpec->new(values => [ 1, 2 ]);   # values: [1,2]
$schema->enum([ 'small', 'large' ]);                         # enum: ["small","large"]

The value goes to the class's FROM_STRUCT through the same call inflation makes, in the constructor, the setter and every spec_* write, element by element for an array or map of such objects. An object already of the class is passed through, undef is still no value, and a value the union itself refuses (a plain scalar for the schema arm of items) fails with the same inflation error "inflate" in IO::K8s gives for it.

Short class names are auto-expanded:

Core::V1::Pod      -> IO::K8s::Api::Core::V1::Pod
Meta::V1::ObjectMeta -> IO::K8s::Apimachinery::Pkg::Apis::Meta::V1::ObjectMeta

Field names that are not valid Perl identifiers are automatically sanitized: $ref becomes _ref, $schema becomes _schema, and hyphens are replaced with underscores (x-kubernetes-foo becomes x_kubernetes_foo). The original JSON key is preserved via init_arg so constructors and FROM_HASH still accept the original names, and TO_JSON outputs the original keys.

k8s '$ref' => Str;                          # Moo attr: _ref
k8s 'x-kubernetes-list-type' => Str;        # Moo attr: x_kubernetes_list_type

Field options

k8s replicas => Int, { minimum => 0, maximum => 10, default => 1 };
k8s policy   => Str, { enum => [qw(Retain Delete)], required => 1 };
k8s name     => Str, { pattern => qr/\A[a-z0-9-]+\z/, description => '...' };
k8s spec     => {
    mode  => [ Str, { enum => [qw(fast safe)] } ],   # inside an inline struct
    hosts => [ [Str], { pattern => '^[a-z.]+$' } ],
};

A field declaration takes an optional third argument: a hashref of options, directly after the type spec (k8s name => Type, { ... }), or as the second element of a two-element arrayref in place of the type spec (name => [ Type, { ... } ]) for a field inside an inline struct, which has no third-argument slot of its own. The legacy 'required' string marker and the Type! suffix (k8s x => 'Str!') still work and are equivalent to { required => 1 }.

The nine recognised option keys are required, default, enum, minimum, maximum, pattern, description, nullable and preserve_unknown. All nine are recorded in the attribute registry for the CRD schema a to_crd emitter builds from it.

required itself takes two meaningful values. required => 1 (like the legacy marker and the ! suffix) both makes the field a Moo-required constructor argument and records required => 1 in the registry. required => 'schema' records the same registry fact without the Moo enforcement, leaving the field optional at construction -- this is what IO::K8s::AutoGen uses for an OpenAPI required list, since a document a real cluster returns can still omit such a field (a server-side default, or a status object not yet populated), and inflate must not fail on data the cluster actually sent. Any other true value is treated the same as 1.

enum, minimum, maximum and pattern are additionally enforced as Type::Tiny constraints at construction, the same way { Quantity => 1 } validates a typed value map -- a bad value fails here instead of at the API server. They apply to a scalar field, to each element of an array of scalars (k8s tags => [Str], { enum => [...] }), and to each value of a typed value map (k8s weights => { Int => 1 }, { maximum => 100 }). Declaring one of them on an object, inline-struct or container field (Opaque, a bare HashRef, { Str => 1 }, [ {} ], [ [] ], a nested class) is a class-load error, since there is no scalar value to check. HashRef[Str] takes them, per value. A failing value dies with one of:

Value "x" is not one of: a, b
Value "-1" is below the minimum 0
Value "11" is above the maximum 10
Value "x" does not match the pattern ...

On an optional field the message is prefixed with Type::Tiny's own generic "did not pass type constraint" line; the rule text above follows in the explanation.

default, description and preserve_unknown are schema-only: they are recorded for to_crd and never change anything at construction or serialization. In particular, default is not applied client-side -- defaulting is the API server's job, and a client-side default would change the wire output, so a field with no value given still serializes as absent.

nullable is recorded for to_crd as well, and since 1.109 it also makes an explicit JSON null a value of its own:

k8s upstream => { Str => 1 }, { nullable => 1 };

my $spec = My::Spec->new(upstream => undef);
$spec->has_upstream;      # true: the key exists
$spec->to_json;           # {"upstream":null}
$spec->clear_upstream;    # absent again
$spec->to_json;           # {}

The field accepts undef at construction and in its setter -- its type is Maybe-wrapped even when it is required -- and every inflation route ("inflate" in IO::K8s, "new_object" in IO::K8s, "struct_to_object" in IO::K8s, "json_to_object" in IO::K8s, "FROM_HASH" in IO::K8s::Role::Resource, from_json and the nested coercion of a constructor, at any depth) keeps a null for it where every other field drops it. TO_JSON writes a nullable field that is present with undef as null; an absent one stays omitted. Only a nullable field gets the two methods that tell those apart: has_<accessor>, true while the key exists, null included, and clear_<accessor>, which makes the field absent again. required => 1 together with nullable means the key has to exist, and null satisfies it. For every field without nullable, undef and null still mean "absent".

Class load fails, naming the class and field, on: an unrecognised option key (k8s: unknown field option '<key>' for field '<name>' of <class> (known: ...)); any option given an explicit undef value, since that is a declaration error rather than "no option" (k8s: field option '<key>' for ... must not be undef); a third argument that is neither 'required' nor a hashref; an empty or duplicate enum, or enum on a Bool field; minimum/maximum on a non-numeric field, a non-numeric bound, or a minimum exceeding maximum; a pattern on a non-string field or one that does not compile as a Perl regex; and a default that fails the field's own type (k8s: 'default' for ... fails the field's own type: <message>), checked once at class-load time rather than discovered later when to_crd emits it. An object-bearing field -- a referenced class, an inline struct, or an array or map of objects -- is exempt from that last check: no plain hash or array default can ever satisfy an InstanceOf constraint, so there is nothing useful to check, and the default is recorded as given.

Class load also fails, before any field option above is even considered, on a declaration that collides with something already in place:

  • Two different JSON keys that sanitize to the same Perl attribute name (x-value and x_value both become x_value), in this class or, nearest wins, in an ancestor: k8s: field '<name>' of <class> collides with field '<other key>' of <declaring class>: both map to the Perl attribute '<attr>'.

  • A field name that is already a method on the class but not a Moo attribute -- a role helper such as "is_ready" in IO::K8s::Role::APIObject -- unless the role that provides it has declared the helper as yielding to a wire field of the same name, which today only conditions is: k8s: field '<name>' of <class> collides with the method '<attr>' of <class>, which is not an attribute.

  • A field that would take over an attribute the class defines itself outside the k8s DSL (a plain has): k8s: field '<name>' of <class> would take over the attribute '<attr>' that <class> defines outside the k8s DSL.

  • A nullable field whose predicate or clearer name (has_<accessor>, clear_<accessor>) the class already answers to -- a method, or the accessor of another field -- other than as this very field's own, declared before or inherited: k8s: field '<name>' of <class> needs the method '<method>' as a nullable field, but <class> already has a method of that name. The other way round, a field whose accessor would take a nullable field's predicate or clearer name is refused by the method check above.

  • A field the same class has already declared under the same JSON key, declared again with a different type, nested class, option, required (including 1 against 'schema') or inline-struct field set: k8s: field '<name>' of <class> is already declared in <class> with a different type, options or required-ness; declare each field once per class. Moo keeps the first attribute of a class, so a second, different declaration could never take effect; it used to overwrite the _k8s_attr_info entry anyway, leaving serialization and construction to follow two different declarations of one field. An inline struct is compared field by field, and a changed field inside it is reported against the generated inner class (<class>::_<Name>).

A rejected declaration leaves the class exactly as it was -- nothing is installed, registered in _k8s_attr_info, or added to the attribute list. A subclass that redeclares an inherited k8s field under the same JSON key replaces it outright, in the subclass only: nearest wins, so the new declaration's type, coercion, required and init_arg take over there, while the ancestor's own declaration is left completely untouched. Within the very same class, an identical second declaration of a field is tolerated and changes nothing -- not the registry, not the attribute list; that includes declaring metadata as Meta::V1::ObjectMeta in a class whose use IO::K8s::APIObject already adopted it. Declare each field once per class.

The registry (_k8s_attr_info) keeps required as a plain 1 (absent when not required, matching the pre-D3 shape) and every other given option, one level deep, under options.

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