Revision history for IO-K8s

1.109     2026-10-01 02:33:28Z

  - Behaviour change: { Str => 1 } is now a map of strings. Every scalar
    value goes out as a JSON string in TO_JSON, to_json and to_yaml, so a
    label, annotation, ConfigMap data value or any other map[string]string
    value built from a numeric Perl value (labels => { v => $n + 0 }) no
    longer reaches the API server as a JSON number and gets a 400 back. A
    reference value in a { Str => 1 } map is still passed through, but
    warns once per class and field in the 'deprecated' category: declare a
    free-form map as Opaque (or a bare HashRef), which copies its values
    through unchanged. to_crd exports a string map as additionalProperties:
    {type: string} and an Opaque map as x-kubernetes-preserve-unknown-fields.
    Opaque takes no parameters.

  - New map forms HashRef[Str] (strict: a reference value is refused at
    construction), HashRef[Int], HashRef[Num], HashRef[Bool],
    HashRef[IntOrStr], HashRef[Quantity], HashRef[Time] and
    HashRef[InstanceOf[Class]], equivalent to the { X => 1 } sugar, which
    stays. { Bool => 1 } and HashRef[Bool] now accept the JSON booleans a
    cluster sends back. AutoGen types an additionalProperties map as
    HashRef[X] and a schemaless or preserve-unknown object as Opaque.

  - 77 shipped fields whose upstream schema is not a map of strings are now
    declared by what they hold: RawExtension, FieldsV1 and
    map[string][]string fields (ControllerRevision data, WatchEvent object,
    ManagedFieldsEntry fieldsV1, OpaqueDeviceConfiguration parameters,
    UserInfo extra, PrometheusOperator proxyConnectHeader and params, ...)
    are Opaque; resource lists (PodSpec overhead, allocatedResources,
    consumedCapacity, ...) are HashRef[Quantity] and reject a value that is
    not a quantity at construction; PodDisruptionBudgetStatus disruptedPods
    is HashRef[Time]; ExternalSecrets OnePasswordProvider vaults and Traefik
    ErrorPage statusRewrites are HashRef[Int]; Resource V1
    DeviceCounterConsumption counters is a map of Counter objects.
    AgentSandbox v1alpha1 volumeClaimTemplates, conditions and env are
    arrays, as upstream declares them, instead of maps that refused every
    real value.

  - to_yaml, TO_YAML and save now quote every string that a JSON, YAML 1.2
    core or YAML 1.1 reader would take for a boolean, null or number (True,
    False, yes, No, on, ~, Null, 012, 0x1F, +1, 1_000, .inf, ...). Before,
    only strings JSON would misread were quoted, so kubectl and the API
    server, which resolve YAML 1.1, read such label, annotation and env
    values and condition statuses as booleans, nulls or numbers, and a
    generated CRD's enum [True, False, Unknown] arrived as
    [true, false, "Unknown"]. Real booleans and numbers are still emitted
    bare, and the output is unchanged for every other value. Every object's
    to_yaml now goes through the one TO_YAML emitter.

  - AgentSandbox and PrometheusOperator now reuse the shipped core classes
    for their embedded pod and volume-claim templates: Sandbox and
    SandboxTemplate podTemplate is Core::V1::PodTemplateSpec, and every
    volumeClaimTemplates entry (plus Prometheus/PrometheusAgent's ephemeral
    volumeClaimTemplate) is Core::V1::PersistentVolumeClaimTemplate. The
    reuse heuristic now filters candidates by recursive shape compatibility
    before its tie-break, which resolves these previously-ambiguous
    {metadata,spec} shapes to the single matching core class.

  - A colliding k8s declaration now fails at class load instead of silently
    corrupting the class: two JSON keys that sanitize to one Perl accessor
    (x-value and x_value, also across inheritance), a field -- or an AutoGen
    CRD/OpenAPI property -- named like a role method that is not an
    attribute (label, save, spec_get, ...), or a field its own class already
    declared with a different type, options or required-ness (an identical
    repeat is a no-op). A subclass redeclaring an inherited field under the
    same JSON key now really replaces its type, coercion, required flag and
    init_arg, not only its registry entry.

  - ComponentStatus keeps its top-level conditions: they were dropped on
    inflate and new and serialized as an empty list. The condition helpers
    (get_condition, is_condition_true, is_ready, condition_message) read a
    top-level conditions field wherever a class declares one.

  - Serialize numeric Perl values in Str fields and [Str] elements as JSON
    strings: EnvVar value => 8080 now emits "8080" instead of a bare number
    the API server rejects. Opaque { Str => 1 } maps are unchanged. AutoGen
    types an array of type: number as [Num], whose elements always go out
    as JSON numbers, and arrays of int-or-string and date-time items as
    [IntOrStr] and [Time] instead of [Str], so to_crd and the emitter keep
    them. [IntOrStr] elements serialize like an IntOrStr field.

  - new_object, inflate, struct_to_object, json_to_object and FROM_HASH
    refuse a non-hash value (array, string, code or scalar reference) at an
    object position instead of silently building an empty object, with or
    without strict. The error names the target class and the field.

  - SpecBuilder element writes (spec_push, indexed spec_set, map keys and
    spec_merge) go through the field's own coercion and type check, like a
    whole-field assignment: 'false' pushed into a [Bool] stays false, and a
    bad element croaks at the write instead of later in to_json. A failed
    multi-value spec_push changes nothing.

  - AutoGen fails closed after a failed generation: every class the failed
    run began stays failed, and later requests die with the original error
    behind an explanatory prefix instead of returning a half-built class,
    even after clear_cache. This includes add_crd; load a repaired schema
    into a fresh IO::K8s instance.

  - AutoGen's reuse_core no longer types a nested schema as a shipped core
    class whose array, map or nested field holds a different element type
    (an array of objects as LabelSelectorRequirement's [Str] values), which
    made valid data fail to inflate, nor an int-or-string field as a Str or
    [Str] one, which wrote 8080 back as "8080". Element types, map values
    and nested fields are now compared recursively; reuse decisions for the
    bundled providers are unchanged.

  - AutoGen's reuse_core no longer types a plain type: string field as a
    shipped core class whose matching field is an IntOrStr, Quantity or Time
    -- the mirror of the int-or-string fix above. {host, port: string} no
    longer reuses TCPSocketAction, whose IntOrStr port sent "8080" out as
    8080, and {medium, mode, sizeLimit: string} no longer reuses
    EmptyDirVolumeSource, whose strict Quantity rejected sizeLimit "big"; such
    a field is typed Str and keeps its string value. A format: date-time
    string still reuses a Time field and a $ref to resource.Quantity a
    Quantity field.

  - Cilium's CiliumNetworkPolicy and CiliumClusterwideNetworkPolicy
    toPorts.ports is now a dedicated PortProtocol class whose port is a
    string, not the reused Networking::V1::NetworkPolicyPort whose IntOrStr
    port sent a Cilium port "80" out as the number 80 that the Cilium CRD
    rejects.

  - Declare Sub::Util as a direct runtime prerequisite.

  - A blessed value at an object position has to come out of its TO_JSON
    as a hash: the JSON boolean that "metadata": true decodes to used to
    build an empty ObjectMeta and now dies naming its class. An IO::K8s
    object of another class is still converted through its TO_JSON.

  - An array or hash field given the wrong container (containers => 'x',
    labels => []), a List whose items is no array or holds a non-hash
    item or whose item_class is a reference or empty, and an inflate
    document that is no hash (inflate('[]')) die naming the class, the
    field or element and the expected and received shape instead of with
    a bare Perl dereference, Module::Runtime, type-constraint or JSON
    parse error.

  - load_yaml reads a file as UTF-8 instead of handing its bytes to
    YAML::PP, which mangled or died on non-ASCII text. A one-line argument
    that is neither an existing file nor YAML text, typically a mistyped
    path, now dies instead of returning an empty list, in load_yaml as in
    IO::K8s::CRD->load and add_crd; so does a directory.

  - load no longer leaves a loader package behind on every call (about
    850 KB each, unbounded in a process that reloads manifests), and reads
    .pk8s files as UTF-8, so a non-ASCII literal gives the same characters
    as in YAML. An unknown option to load now dies.

  - load($file, vars => { ... }) hands values to a .pk8s manifest, which
    reads them with var($name) or var($name, $default).

  - Document that json_to_object and struct_to_object resolve their class
    name again through expand_class, so a bare one-word name is read as a
    Kind; an already resolved class goes in as '+Class'.

  - AutoGen types a $ref to the apiextensions JSON, JSONSchemaPropsOrArray,
    JSONSchemaPropsOrBool and JSONSchemaPropsOrStringArray definitions as
    the shipped classes, so a value such as default: "foo", an array or a
    boolean round-trips instead of failing to inflate. The v1beta1 names
    are carried unchanged by the v1 JSON class.

  - Errors in a .pk8s manifest (die, warnings, syntax errors, var() and
    the inflation errors of a Kind call) name its file and line instead of
    "(eval 273) line 1848", and the manifest no longer sees the loader's
    own variables ($file, $m, ...), which now fail under use strict.

  - Errors name the calling line instead of one inside the distribution:
    a missing required field or a wrongly typed value met while new_object,
    inflate, struct_to_object, json_to_object, load_yaml or a .pk8s Kind
    call builds an object (a Type::Tiny exception stays an object), and
    errors from add_crd, IO::K8s::CRD->generate (AutoGen's included) and to_crd,
    FROM_HASH and from_json (the unknown_kinds => 'unstructured' fallback
    included), spec_set and spec_merge, the JSONSchemaPropsOr* classes,
    classes AutoGen builds from an openapi_spec, load of a missing .pk8s
    file, inflate of a document without kind, and List errors through
    inflate or IO::K8s::List->FROM_STRUCT; so does a CRD file that exists
    but cannot be read, through add_crd and IO::K8s::CRD->load alike.

  - An apiextensions JSON value (a CRD schema's default, example or enum
    entry, a K3s HelmChart's values) is copied one level on the way in and
    out: inflating copies a hash or array instead of keeping the caller's
    own, and TO_JSON returns a copy, so editing the source structure or the
    serialized output no longer changes the object -- the rule every other
    array or hash field already follows.

  - A field declared nullable => 1 keeps an explicit JSON null: new, its
    setter and every inflate path (inflate, new_object, struct_to_object,
    json_to_object, FROM_HASH, nested coercion) keep it, TO_JSON writes it
    back as null, and an absent field stays absent. Such a field gets
    has_<accessor> and clear_<accessor>, and spec_delete removes it.
    Setting a nullable field to undef therefore sends null; the
    ExternalSecrets refreshTime now round-trips a server's null.

  - use IO::K8s::APIObject takes a subresources parameter (status, scale)
    that to_crd and IO::K8s::CRD->new write into spec.versions[]; it is
    checked at use time. add_crd carries a CRD version's subresources into
    the generated class and IO::K8s::CRD::Emitter renders the parameter,
    so a CRD read in and written out again keeps them. Any other import
    parameter -- a typo such as subresource or resource_plurals -- dies at
    the use line, naming the class and the known ones, instead of being
    ignored; so does an odd number of import arguments, and an api_version
    or resource_plural that is empty, undef or a reference.

  - SpecBuilder paths walk through apiextensions union fields into the
    value they hold: spec_set('values.replicaCount', 3) on a K3s HelmChart
    now serializes values: {"replicaCount":3} instead of values: null, and
    spec_get, spec_hash, spec_array, spec_push and spec_delete reach the
    value as well; the same for a schema's items, additionalProperties,
    additionalItems, dependencies, default and example.

  - Fix POD where a > inside a C<> code cut it short and garbled the text:
    IO::K8s, IO::K8s::Resource's field options, IO::K8s::CRD->new,
    IO::K8s::AutoGen, IO::K8s::CRD::Emitter, IO::K8s::Cilium,
    IO::K8s::Types::Net, the IO::K8s::Role::* builder roles and several
    Resource API classes.

  - A field typed as an apiextensions union class takes on new, its setter
    and spec_set/spec_push every value inflate takes, not only a hash:
    HelmChartSpec->new(values => [1, 2]) and JSONSchemaProps->new(enum =>
    ['a']) used to die on the type check. Other object fields still refuse
    a non-hash value.

  - AutoGen types a $ref to Quantity, IntOrString, Time or MicroTime in an
    array's items or a map's additionalProperties as that scalar, as it
    already did for a property: [Quantity], { Quantity => 1 } and so on.
    It used to generate an empty class that real values failed to inflate
    into, or refuse a spec that did not ship the definition.

  - Quantity and Time values always serialize as JSON strings, the form
    Kubernetes writes them in, in scalar fields, maps and arrays alike:
    limits => { cpu => 1 } now emits "1" instead of a bare 1. The API
    server accepts both, but the output for numeric Perl values changes.

  - Bundled provider CRD Kind classes (Cilium, GatewayAPI, AgentSandbox,
    CertManager, PrometheusOperator, VolumeSnapshot, ExternalSecrets) now
    declare their upstream subresources, so to_crd writes
    spec.versions[].subresources -- status, plus scale where upstream defines
    it (AgentSandbox SandboxWarmPool, PrometheusOperator Alertmanager and
    Prometheus).

1.108     2026-09-22 00:53:53Z

  - Track Kubernetes v1.37.0: 11 new Kinds, 103 supporting types and 48 added
    fields, with upstream-derived resource plurals. Ten fields newly required
    upstream now fail construction when omitted.

  - Add full-depth PrometheusOperator (v0.93.1), VolumeSnapshot (external-
    snapshotter v8.6.0) and ExternalSecrets (v2.10.0) providers, including all
    19 ExternalSecrets generator Kinds.

  - Model existing CRD providers to full depth and retain compatible older API
    tracks. Update Cilium to v1.20.1, Traefik to v3.7.12, K3s to
    v1.36.4+k3s1 and AgentSandbox to v1.0.0.

  - Fix CertManager status conditions silently dropping live objects: Issuer,
    ClusterIssuer, Certificate and CertificateRequest now type their conditions
    with cert-manager's own condition classes, which require only type and
    status per the CRD, so real objects that omit message no longer fail to
    inflate.

  - Constrain cert-manager IssuerCondition, CertificateCondition and
    CertificateRequestCondition status to the CRD's True/False/Unknown enum,
    enforced client-side at construction like the other schema enums; a status
    outside that set now fails to construct, matching upstream.

  - Fix PrometheusOperator status conditions dropping live objects the same
    way: Alertmanager and Prometheus now use a PrometheusOperator condition
    class (lastTransitionTime, status and type required per the CRD, message
    and reason optional), so real objects that omit message no longer fail to
    inflate. WorkloadBinding, whose upstream condition is a distinct type with
    its type constrained to the Accepted enum, uses its own
    WorkloadBindingCondition class.

  - Fix AutoGen's core-class reuse (D5, reuse_core) picking a shipped class
    that requires a field the schema's own required list leaves optional.
    A nested object generated from a caller-supplied openapi_spec, or by
    CRD::Emitter, no longer reuses a class whose extra required field would
    reject or silently drop a real object that omits it. When that filter
    removes the only shared apimachinery or core type a nested object matched,
    reuse no longer falls back to an unrelated class of the same shape from
    another API group; the object's own class is generated instead, so a
    provider's condition is modelled by that provider's class.

  - Model Cilium and ExternalSecrets status conditions with each provider's own
    condition class instead of reusing Core::V1::NamespaceCondition for a look-
    alike shape that survives the required filter: Cilium's CiliumNetworkPolicy
    conditions are now NetworkPolicyCondition, and ExternalSecrets' SecretStore,
    ExternalSecret, PushSecret and GeneratorState status conditions each their
    own class. The field shape is unchanged, but a condition now blesses as the
    provider's class, and ExternalSecret's condition type is constrained to the
    CRD's Ready/Deleted enum, enforced client-side at construction.

  - Add the three VolumeGroupSnapshot Kinds in v1, v1beta1 and v1beta2, with
    typed version-specific status fields. Bare names select the pinned
    upstream's storage v1beta2; exact GVKs select any served track.

  - Add CRD loading/add_crd for objects, hashes, YAML/JSON and files. Generate
    every served version with exact GVKs and storage-version short names;
    retain the first bare-name binding when groups share a Kind.

  - Generate nested OpenAPI objects at full depth, expose their source metadata
    and safely shorten deep class names. Reject unresolved references and use
    process-unique namespaces so later instances cannot inherit old schemas.

  - Let generation keep a provider's own named type where its shape matches a
    core class (AutoGen reuse_core_except), and let the class emitter point a
    field at an absolute cross-version or core class without emitting a copy.

  - Add single- and multi-version CRD schema export through to_crd and
    IO::K8s::CRD, including field options, JSON Bool defaults and supported
    ECMA262 patterns. Reject duplicate versions and unsupported Perl patterns.

  - CRD schema round trips are not lossless: Quantity becomes Str, typed
    scalar maps become opaque maps, and arrays of Num, Quantity, Time or
    IntOrStr re-import as arrays of Str.

  - Add CRD::Emitter rendering with name/role overlays and canonical Quantity
    recognition. Shared target paths require identical functional declarations;
    contextual POD may differ, but conflicting aliases fail.

  - Extend the k8s DSL with Num, typed scalar maps, nested containers and
    schema/validation field options. Enforce scalar enum/range/pattern rules;
    defaults and schema-requiredness remain metadata, not client-side defaults.

  - Preserve unknown fields at every modeled depth by default; strict => 1
    rejects them. Opt-in unknown_kinds => 'unstructured' also preserves
    unresolved resource documents instead of failing GVK resolution.

  - Make GVK resolution consistently exact and fail closed, including version
    conflicts, qualified map keys and bare names shadowed by Perl packages.
    kind, api_version and resource_plural reject setter arguments.

  - Coerce nested hashrefs through direct constructors and FROM_HASH, preserve
    prebuilt objects and shallow-copy input/output containers. Inner references
    below opaque hashes remain shared.

  - Make class-level JSON round trips recursive, handle fields named json and
    require UTF-8 bytes for from_json. Already-decoded non-ASCII character
    strings now fail instead of producing mojibake.

  - Normalize Bool values consistently, including scalar references and JSON
    booleans. Scalar undef is omitted; undef in Bool arrays fails at the wire
    boundary. Num stays numeric; Quantity accepts the n and u suffixes.

  - Extend generic List inflation to Kubernetes *List payloads, unknown
    envelope fields, strict mode and JSON round trips. item_class requires a
    fully qualified class; unverifiable list identity is not emitted.

  - Make SpecBuilder object-aware on all API objects: typed paths, container
    vivification, hashref coercion and non-destructive errors. Builder roles
    declare their requirements; calling spec_* without a spec gives an error.

  - Support Ingress path backends via add_path_match(service => ..., port =>
    ...), including named ports; add_backend remains the default backend.
    Gateway/Traefik routing is unchanged, with Gateway path-type translation.

  - Limit core NetworkPolicy DNS egress to CoreDNS in kube-system. Policies
    generated by allow_egress_to_dns no longer permit port 53 everywhere;
    other DNS destinations require an explicit rule. Cilium is unchanged.

  - Give MiddlewareTCP a TCP-only builder. Its former HTTP-only builder
    methods now fail as missing methods; HTTP Middleware is unchanged.

  - Require owner UIDs and reject a second controller in set_owner. Adding
    the same UID is an idempotent no-op; controller => 0 creates a
    non-controller reference without the controller flag.

  - Remove unsupported status accessors from Traefik, K3s Addon and
    HelmChartConfig, and volumeClaimTemplatesPolicy from AgentSandbox v1alpha1
    SandboxTemplate. Undeclared wire fields remain preserved outside strict mode.

  - Embedded Pod/Job/PVC template structs and ResourceClaimTemplateSpec lose
    APIObject identity/convenience methods (missing-method errors); metadata
    remains, without the incorrect apiVersion/kind output.

  - Remove imported helper functions accidentally exposed as public methods;
    genuine role methods and composition relationships remain unchanged.

  - Cache successful class loads and precompute core-shape lookup. Removing
    a cached module from %INC no longer forces a reload through load_class;
    failed loads are still retried, and reuse_core can be disabled.

  - Add pinned-source CRD drift/schema auditing and render/check/suggest tools,
    with data-driven exceptions and UTF-8-safe manifests. Correct API/role POD,
    declare Path::Tiny for tests and fix the distribution authority metadata.

1.107     2026-08-15 15:37:52Z

  - Resolve resource_map short-name keys on the explicit-apiVersion path
    (k31). expand_class('Kind', $api_version) and the qualified-string
    form now fall back to the resource_map's bare short-name key when no
    '$api_version/Kind' key exists, but only if the mapped class itself
    confirms the requested version via api_version() — a CRD registered as
    'StaticWebSite => "+My::StaticWebSite"' inflates again from a manifest
    hashref instead of dying with "Cannot resolve Kubernetes GVK". The
    fail-closed semantics from k17 are preserved: a class that cannot
    verify the requested version is never substituted — whether it reports
    a different version, has no api_version() method, returns undef from it,
    or implements api_version as an instance attribute so that the class
    method call dies. The fallback runs before openapi_spec AutoGen, since
    the user explicitly registered the class.

  - Stop add() from dying on a provider class whose api_version is an
    instance attribute (k32). Merging a resource_map — via
    IO::K8s->new(with => [...]) or ->add(...) — asks each mapped class for
    its api_version() to build the domain-qualified key. A class that
    implements api_version as a Moo attribute rather than a constant made
    that class-method call die, taking the whole merge with it. Such a
    class is now registered under its short name and simply skipped for
    the qualified key, which is the only correct answer: no qualified key
    can be derived for it.

1.106     2026-08-13 02:59:43Z

  - Derive api_version through @ISA for class_namespaces subclasses.
    IO::K8s::Role::APIObject::api_version now falls back to walking the
    inheritance chain when the class name itself is not in a known
    namespace, so a consumer subclass (e.g. `use parent
    'IO::K8s::Api::Core::V1::Pod'`) serializes apiVersion instead of
    silently omitting it.

  - Make compare_to_schema inheritance-aware. The schema comparison in
    IO::K8s::Role::Resource now reads the merged @ISA attribute view
    (_k8s_attr_info) instead of the raw registry entry, so a
    class_namespaces-style subclass compares its inherited attributes
    instead of an empty or partial set.

  - Make the k8s attribute lookups class_namespaces-aware via @ISA.
    _k8s_attr_info and _k8s_attributes in IO::K8s::Role::Resource now walk
    the inheritance chain, so a consumer subclass registered through
    class_namespaces (e.g. `use parent 'IO::K8s::Api::Core::V1::Pod'`)
    inflates with typed fields and serializes its parents' attributes
    instead of seeing an empty registry. Attribute info merges nearest-wins
    (a class's own declaration overrides an inherited one, in deterministic
    @ISA order); the attribute list is a deduplicated union of own and
    inherited declarations. Both merged views are cached per class and
    invalidated when a new k8s attribute is registered.

  - Made AutoGen dispatch apiVersion-aware and deterministic.
    expand_class()/inflate() with an explicit apiVersion now fall through to
    the openapi_spec AutoGen lookup when the resource map has no qualified
    key, but only for an exact group/version match in the spec's
    x-kubernetes-group-version-kind metadata; unknown or non-matching
    apiVersions fail closed (expand_class returns undef, inflate dies)
    instead of silently selecting a different version. Definition lookup no
    longer depends on hash order: versionless lookups sort candidate
    definitions lexicographically, exact lookups filter by group/version
    and croak on ambiguity. AutoGen picks a multi-GVK definition's entry by
    exact api_version (croak on ambiguity or no match) and deterministically
    sorted first otherwise, and a definition serving several versions now
    gets a GVK-specific package identity so two apiVersions of one
    definition yield distinct classes with the correct api_version method
    and wire serialization.

  - Fix IO::K8s::List::api_version for empty lists with an item_class.
    The local class-name regex derived shortened, invalid wire versions
    for groups with a ".k8s.io" suffix (rbac/v1, storage/v1, events/v1)
    and serialised them into manifests. The wire version is now derived
    from the item_class's own api_version class method, which knows the
    full group (rbac.authorization.k8s.io/v1, storage.k8s.io/v1,
    events.k8s.io/v1); unloadable or non-API item_classes yield undef and
    serialised empty lists omit apiVersion again.

  - Document that .pk8s manifests are Perl code executed in-process via
    eval and therefore must only be loaded from trusted sources, with
    load_yaml documented as the data-only path without code execution.

  - Declared Module::Runtime as a direct runtime prerequisite. IO::K8s has
    always loaded it in IO::K8s (for require_module) but cpanfile and the
    built META omitted it, so dependency installers that resolve strictly
    from the declared prereqs could miss it. The built META.json now lists
    Module::Runtime under runtime requires.

  - Made built-in Kubernetes resource dispatch exact for every addressable
    apiVersion/kind pair in the pinned v1.36.3 OpenAPI spec. This adds the
    missing events.k8s.io/v1 Event and autoscaling/v1
    HorizontalPodAutoscaler routes while preserving the historical bare-name
    defaults (Core v1 Event and Autoscaling v2 HPA). Explicit unknown,
    malformed, empty, or mismatched apiVersions now fail closed:
    expand_class() returns undef and inflate()/new_object() report the
    requested Kind and apiVersion instead of silently selecting a different
    bare-name schema. Newly exposed compatibility aliases are
    ClusterTrustBundle, DeviceTaintRule, LeaseCandidate, PodGroup,
    ResourcePoolStatusRequest, StorageVersion, StorageVersionMigration and
    Workload; all existing aliases and targets remain pinned. An offline,
    SHA-256-pinned v1.36.3 fixture now exhaustively checks 98 addressable GVKs,
    all 14 multi-version Kind collisions, namespace scope, maintained
    non-dispatchable exceptions, real inflation and provider first-wins
    behavior.

  - Fixed inflate()/expand_class() to dispatch by apiVersion to the correct
    multi-version Kind. Pre-fix, %DEFAULT_RESOURCE_MAP mapped each short
    Kind name (DeviceClass, ResourceClaim, ResourceClaimTemplate,
    ResourceSlice, ...) to exactly one class path (always the GA v1), and
    only entries added externally via add() ever got domain-qualified
    ('$apiVersion/$Kind') entries. Two visible consequences:
    inflate({apiVersion=>'resource.k8s.io/v1beta1', kind=>'DeviceClass',
    ...}) silently returned IO::K8s::Api::Resource::V1::DeviceClass (the
    GA class, wrong schema), and inflate of a short-name-less Kind
    (DeviceTaintRule, ResourcePoolStatusRequest) died with 'Cant locate
    IO/K8s/DeviceTaintRule.pm in @INC' because the bare IO::K8s::$Kind
    fallback does not exist. %DEFAULT_RESOURCE_MAP now carries literal
    qualified entries for every shipped version of every addressable Kind,
    including the short-name-less ones. Class-method and instance dispatch
    therefore share the same complete static map without BUILD eagerly
    loading every target class; add() still derives qualified keys for
    externally merged providers. See k11.
  - Fixed api_version() in IO::K8s::Role::APIObject for the
    Storagemigration and Apiserverinternal groups. The fallback
    `lc($group) . '/' . $version` only produces the correct wire
    apiVersion for groups whose CamelCase lc-form equals the upstream
    group name (apps, batch, autoscaling, policy) -- for groups whose
    upstream name has a `.k8s.io` suffix, it produced a syntactically
    plausible but rejected-by-the-API-server string. In particular,
    every serialised StorageVersionMigration manifested as
    `apiVersion: storagemigration/v1beta1` (upstream:
    `storagemigration.k8s.io/v1beta1`) and every StorageVersion as
    `apiVersion: apiserverinternal/v1alpha1` (upstream:
    `internal.apiserver.k8s.io/v1alpha1`). Both groups are now in
    %API_GROUP_MAP, alongside the 13 already-mapped groups. The other
    18 groups covered by %_class_prefix were audited and verified to
    either already map correctly or to fall through to the lc-fallback
    correctly (the upstream group name equals the lc form).
  - Fixed _expand_class in IO::K8s::Resource to handle CamelCase prefixes
    (KubeAggregator, AdmissionRegistration, ...). The single-word
    `[A-Z][a-z]+` regex could not match a prefix that has an internal
    capital, so any declared class name whose first segment was CamelCase
    silently fell through to the IO::K8s::Api default. In practice this
    meant KubeAggregator's APIService, APIServiceSpec, APIServiceStatus,
    APIServiceCondition and ServiceReference could not be inflated --
    spec/status resolved to nonexistent IO::K8s::Api::KubeAggregator::V1
    classes instead of the shipped IO::K8s::KubeAggregator::Pkg::Apis::
    Apiregistration::V1 ones. The lookup now walks %_class_prefix in
    longest-key-first order so CamelCase wins over a hypothetical shorter
    substring, and uses \Q ... \E so the prefix is matched as a literal
    rather than as a regex. Backwards-compatible: unknown short names still
    fall through to the IO::K8s::Api default.
  - Shipped IO::K8s::Api::Apiserverinternal::V1alpha1::StorageVersionSpec.
    Upstream declares it as an empty struct ("StorageVersionSpec is an
    empty spec"), but the class was never authored, so every StorageVersion
    inflate died with "Can't locate .../StorageVersionSpec.pm in @INC".
    There was no working path through the class. This is the empty class
    upstream asked for; the failure mode that mattered was on the
    serialisation side, where an empty class round-tripped through TO_JSON
    could collapse to an empty hash or disappear altogether.
  - Added t/34_registry_guard.t: after every shipped class is loaded,
    walks the global attribute registry and asserts every referenced
    target class is loadable. This is the regression net that would have
    caught the apiextensions.k8s.io/v1 union types, the KubeAggregator
    prefix mismatch and the missing StorageVersionSpec together, rather
    than one at a time on the consumer side. Inline-generated structs
    (packages with no .pm file) are recognised via the `k8s` symbol in
    the target's stash.
  - Added t/32_kubeaggregator_apiservice.t and t/33_apiserverinternal_
    storage_version.t covering the two regression cases above: that
    APIService and StorageVersion inflate, that every spec/status/
    condition class resolves to the namespace shipped (not the wrong
    IO::K8s::Api fallback), and that the full object round-trips
    byte-for-byte through inflate -> TO_JSON -> inflate -> TO_JSON.
  - Added t/35_expand_class.t and t/36_storage_version_spec_unit.t for
    direct unit coverage of the bugfix targets: every branch of
    Resource::_expand_class (+FullClassName, already-qualified,
    longest-key-first prefix walk, default fallback) and the full
    prefix map; and StorageVersionSpec on its own (DOES Resource,
    ->new, TO_JSON as an empty hash, no api_version/kind since the
    class intentionally does not consume IO::K8s::APIObject).

  - Added the four apiextensions.k8s.io/v1 types the distribution declared
    but never shipped: JSON, JSONSchemaPropsOrArray, JSONSchemaPropsOrBool
    and JSONSchemaPropsOrStringArray. Without them, inflating any
    CustomResourceDefinition whose schema used `items`, `additionalItems`,
    `additionalProperties`, `dependencies`, `default`, `example` or `enum`
    died with "Can't locate IO/K8s/.../JSONSchemaPropsOrArray.pm in @INC" --
    in practice nearly every real CRD, since a single array field is enough
    to hit it. All four are Kubernetes union types that serialize as the
    bare alternative rather than as a tagged wrapper, so which arm was used
    now survives a round trip: `additionalProperties: false` stays false
    instead of collapsing into an empty schema object, and a single `items`
    schema does not turn into a one-element array.
  - Added a FROM_STRUCT inflation hook to struct_to_object. A class that
    provides this class method takes over its own inflation completely and
    owns its TO_JSON in return, instead of being built field by field from
    a hashref. This is what makes the union types above representable; it
    is the general mechanism for any type that serializes as a bare value.

  - Shipped resource.k8s.io/v1beta1 (39 classes, entirely missing --
    lib/IO/K8s/Api/Resource/ had no V1beta1/ directory at all) and filled
    in resource.k8s.io/v1beta2 (39 more classes; only the DeviceTaintRule/
    DeviceTaintRuleSpec/DeviceTaintRuleStatus/DeviceTaintSelector quartet
    from an earlier fix was shipped there). Both versions now carry the
    full DRA surface: DeviceClass, ResourceClaim, ResourceClaimTemplate and
    ResourceSlice (DeviceClass/ResourceSlice cluster scoped, ResourceClaim/
    ResourceClaimTemplate namespaced -- verified against the real swagger
    paths, not just pattern-matched from V1) plus the whole Device/
    AllocationResult/DeviceRequest/Counter/CapacityRequestPolicy family.
    v1beta1 is the most widely deployed DRA server version; any consumer
    talking to a cluster that had not migrated to v1 got "Can't locate
    .../DeviceClass.pm in @INC" for every one of these Kinds.
  - Added the 9 v1.36 DRA additions to resource.k8s.io/v1alpha3 that had
    not been backported to the legacy "classic DRA" version also serving
    them: DeviceTaint, DeviceTaintRule (+ Spec/Status), DeviceTaintSelector,
    PoolStatus, and ResourcePoolStatusRequest (+ Spec/Status) -- the latter
    three were already present from a previous pass; only the
    DeviceTaint(Rule) family was actually missing. The 24 already-shipped
    "classic DRA" structural types (DeviceClass, ResourceClaim, Device,
    AllocationResult, etc.) are untouched, per the 1.100/1.105 decision to
    keep them as legacy backward compatibility.
  - Fixed ArrayRef[Bool] fields (DeviceAttribute.bools, present in the V1,
    V1beta1 and V1beta2 DRA APIs) to serialize as JSON booleans instead of
    plain 0/1, and to accept real decoded JSON booleans on the way back in.
    Previously TO_JSON emitted [1,0,1] instead of [true,false,true], and
    FROM_HASH on a real cluster response died with a Moo type constraint
    violation because JSON::PP::Boolean objects don't satisfy Types::
    Standard's Bool -- this affected the already-shipped
    IO::K8s::Api::Resource::V1::DeviceAttribute too, not just the new
    v1beta1/v1beta2 copies. IO::K8s::Resource now tracks an
    is_array_of_bool flag with element-wise coercion, mirroring the
    existing scalar Bool handling.
  - Fixed ResourceClaimTemplateSpec (v1beta1 and v1beta2) to be built on
    IO::K8s::APIObject instead of IO::K8s::Resource, matching the already-
    shipped V1 sibling. Despite not being a Kind, it carries a real
    upstream `metadata: ObjectMeta` field; without APIObject the metadata
    attribute was never registered and silently dropped on serialization.
  - Added maint/spec-drift-check.pl, a repeatable coverage checker that
    diffs a real upstream swagger.json against what lib/IO/K8s/ actually
    ships and reports missing Kinds/types/fields (the tool behind
    k4-k8's discovery), plus a --from/--to mode that diffs two upstream
    releases directly to gauge whether a version bump is worth doing.
    Settled non-gaps (dropped *List kinds, old back-compat API tracks,
    apimachinery scalar/opaque types) are filtered via the maintained
    maint/spec-drift-exceptions.yaml. Report-only: never edits lib/ or
    the karr board.
  - Shipped storagemigration.k8s.io/v1beta1 (IO::K8s::Api::Storagemigration::
    V1beta1::StorageVersionMigration, ::StorageVersionMigrationSpec and
    ::StorageVersionMigrationStatus), the version v1.36 clusters actually
    serve now that v1alpha1 has been dropped from the upstream spec. The
    existing V1alpha1 classes are untouched and stay shipped for old-cluster
    back-compat. Also added IO::K8s::Apimachinery::Pkg::Apis::Meta::V1::
    GroupResource, which StorageVersionMigrationSpec.resource needs and
    which did not exist under any group.
  - Added five Core::V1 classes that existing structs gained a new $ref
    field for in v1.36, but whose target type was never shipped, silently
    swallowing the field on inflate: FileKeySelector (EnvVarSource.
    fileKeyRef -- read an env var's value from a file in the container),
    NodeSwapStatus (NodeSystemInfo.swap), PodCertificateProjection
    (VolumeProjection.podCertificate), and VolumeStatus + ImageVolumeStatus
    (VolumeMountStatus.volumeStatus).
  - Added t/38_storagemigration_v1beta1_and_core_v136_fields.t covering the
    two fixes above: a full StorageVersionMigration inflate -> TO_JSON ->
    inflate round-trip through GroupResource, plus round-trips for
    EnvVarSource.fileKeyRef, VolumeMountStatus.volumeStatus, NodeSystemInfo.
    swap and VolumeProjection.podCertificate.
  - Added 14 fields the v1.36 sync had missed on otherwise-shipped classes:
    Core::V1::ContainerStatus.stopSignal, Core::V1::Lifecycle.stopSignal,
    Core::V1::PodCondition.observedGeneration, Core::V1::
    PodSecurityContext.seLinuxChangePolicy, Core::V1::ResourceHealth.message,
    Storage::V1::VolumeError.errorCode, ApiextensionsApiserver::...::V1::
    CustomResourceDefinitionCondition.observedGeneration and ::
    CustomResourceDefinitionStatus.observedGeneration, Apimachinery::...::
    Meta::V1::DeleteOptions.ignoreStoreReadErrorWithClusterBreakingPotential,
    and Apimachinery::Pkg::Version::Info.emulationMajor/emulationMinor/
    minCompatibilityMajor/minCompatibilityMinor. Also shipped the new
    IO::K8s::Apimachinery::Pkg::Apis::Meta::V1::ShardInfo struct and wired it
    up as ListMeta.shardInfo, which previously had no target type at all.
  - Shipped coordination.k8s.io/v1alpha2 (IO::K8s::Api::Coordination::
    V1alpha2::LeaseCandidate and ::LeaseCandidateSpec), which had no
    directory at all despite V1, V1alpha1 and V1beta1 all being shipped.
    Also added IO::K8s::Api::Scheduling::V1alpha2::TypedLocalObjectReference,
    a distinct upstream schema that WorkloadSpec.controllerRef previously
    pointed at Core::V1::TypedLocalObjectReference for instead -- the two
    happen to share the same three fields today, but they are different
    schemas and were drifting apart silently.
  - Added the meta.v1 discovery Kinds APIGroupList and APIResourceList
    (IO::K8s::Apimachinery::Pkg::Apis::Meta::V1::), used by the /apis and
    /apis/<group> discovery endpoints. Modelled like their already-shipped
    siblings APIGroup/APIVersions/Status/DeleteOptions: explicit apiVersion/
    kind Str fields, not IO::K8s::APIObject.
  - Added t/39_v1_36_field_and_kind_gaps.t covering all of the above: field
    round-trips for a representative sample of the 14 additions, a full
    LeaseCandidate inflate -> TO_JSON -> inflate round-trip, the corrected
    WorkloadSpec.controllerRef target class, and APIGroupList/
    APIResourceList loading with their required fields passed through.

1.105     2026-08-08 18:51:44Z

  - Removed the 76 individual *List classes (PodList, ServiceList,
    DeploymentList, etc.) that have emitted "use IO::K8s::List instead"
    deprecation warnings since the 1.00 Moose-to-Moo rewrite. They were
    never real classes in the 1.x series and this release drops them from
    the distribution entirely. Code that still references one of these
    names (e.g. via cpanfile or `use`) will now fail to install instead of
    silently warning; install IO::K8s::Deprecated for a clear redirect
    message pointing at IO::K8s::List. This also removes four namespaces
    that held nothing but a List stub and are now gone entirely --
    IO::K8s::ApiExtensionsApiServer, IO::K8s::Api::Auditregistration,
    IO::K8s::Api::Extensions, IO::K8s::Api::Settings -- plus several
    now-empty single-version subdirectories under Apps, Autoscaling, Batch,
    Certificates, Coordination, Events, KubeAggregator, Policy, Rbac, and
    Scheduling. Full old-name list: IO::K8s::Deprecated's POD.
  - Restored the 8 Cilium v2alpha1/v2 classes dropped in 1.100's Cilium
    v1.19.2 upgrade (CiliumBGPAdvertisement, CiliumBGPClusterConfig,
    CiliumBGPNodeConfig, CiliumBGPNodeConfigOverride, CiliumBGPPeerConfig,
    CiliumCIDRGroup under Cilium::V2alpha1; CiliumBGPPeeringPolicy under
    Cilium::V2alpha1; CiliumExternalWorkload under Cilium::V2). These now
    ship alongside their current-API-version siblings for backward
    compatibility with clusters still running older Cilium releases,
    matching this dist's existing convention of keeping multiple API
    versions of a resource side by side (e.g. Apps V1beta1/V1beta2/V1).
  - Synced all API surfaces to their current upstream releases (previous
    target: Kubernetes v1.31 and CRD providers pinned months/years back).
    Kubernetes core moved v1.31 -> v1.36; CRD providers moved
    Cilium v1.19.2 -> v1.20.0, K3s v1.35.1+k3s1 -> v1.36.3+k3s1,
    Traefik v3.0.0 -> v3.7.10 (CRD set unchanged across v3.x),
    cert-manager v1.19.3 -> v1.21.1 (no CRD/field changes),
    Gateway API v1.1.0 -> v1.6.1, Agent Sandbox v0.2.1 -> v0.5.4.
    * Cilium: added CiliumDatapathPlugin (cilium.io/v2alpha1, new in
      v1.20.0's Extensible Datapath feature).
    * Gateway API: added BackendTLSPolicy, ListenerSet, TLSRoute,
      TCPRoute, UDPRoute (all gateway.networking.k8s.io/v1), and a
      gateway.networking.k8s.io/v1 ReferenceGrant alongside the existing
      v1beta1 (still the short-name/storage version); TLSRoute joins
      HTTPRoute/GRPCRoute in consuming IO::K8s::Role::Routable.
    * Agent Sandbox: all four CRDs gained a v1beta1 track (now the
      storage version) with real schema changes -- Sandbox drops
      spec/status.replicas for spec.operatingMode + spec.service +
      status.nodeName/podIPs; SandboxClaim drops spec.sandboxTemplateRef
      for a required spec.warmPoolRef and gains
      spec.additionalPodMetadata, spec.env, spec.lifecycle,
      a typed spec.volumeClaimTemplates list, and
      status.sandbox.name (was status.sandbox.Name); SandboxTemplate
      gains spec.envVarsInjectionPolicy, spec.service,
      spec.volumeClaimTemplates, spec.volumeClaimTemplatesPolicy;
      SandboxWarmPool gains spec.updateStrategy.type. v1alpha1 survives
      as served-but-deprecated on all four.
    * K3s and cert-manager: version bump only, no CRD/field-level changes
      upstream.
    * Traefik: version bump only; CRD set confirmed unchanged across the
      whole v3.x line.
    * Core Kubernetes -- workloads: added container restart rules
      (Container/EphemeralContainer restartPolicyRules), pod-level
      resources (PodSpec.resources, PodStatus.allocatedResources/
      resources), PodSpec.hostnameOverride and .schedulingGroup (gang
      scheduling), PodStatus.observedGeneration and DRA-related extended
      resource claim status fields, HorizontalPodAutoscaler per-direction
      scaling tolerance, DeploymentStatus/ReplicaSetStatus
      .terminatingReplicas, NodeStatus.declaredFeatures.
    * Core Kubernetes -- networking: added networking.k8s.io/v1
      ServiceCIDR and IPAddress (stable since v1.33; deprecated v1beta1
      kept alongside), made NetworkPolicySpec.podSelector optional
      (upstream now defaults it), added discovery.k8s.io/v1 EndpointHints
      .forNodes / ForNode for PreferSameNode topology routing.
    * Core Kubernetes -- security/admission: added
      MutatingAdmissionPolicy/MutatingAdmissionPolicyBinding (GA in v1),
      PodCertificateRequest (certificates.k8s.io/v1beta1), and
      ClusterTrustBundle at v1beta1 alongside the existing v1alpha1.
      The older v1alpha1/v1beta1 ValidatingAdmissionPolicy and
      authentication.k8s.io/{v1alpha1,v1beta1} SelfSubjectReview tracks,
      and flowcontrol.apiserver.k8s.io/v1beta3, were evaluated for
      removal (no longer served by a v1.36 apiserver) but kept in this
      release rather than deleted, matching this dist's Cilium-precedent
      of preferring backward compatibility over deletion for classes that
      already shipped in a prior CPAN release -- deleting a
      previously-released module name without a tombstone would orphan
      it on PAUSE. Left as a maintainer decision for a future release.
    * Core Kubernetes -- storage/DRA: added resource.k8s.io/v1 (GA)
      Dynamic Resource Allocation -- DeviceClass, ResourceClaim,
      ResourceClaimTemplate, ResourceSlice and their full nested type
      graph -- replacing the old v1alpha3-only control-plane-controller
      allocation mode (DRA graduated to GA in Kubernetes v1.34).
      Also added resource.k8s.io/v1beta2 DeviceTaintRule,
      resource.k8s.io/v1alpha3 ResourcePoolStatusRequest,
      scheduling.k8s.io/v1alpha2 PodGroup/Workload (gang scheduling),
      storage.k8s.io/v1 VolumeAttributesClass (GA),
      coordination.k8s.io/v1beta1 LeaseCandidate, and new CSIDriverSpec
      fields (nodeAllocatableUpdatePeriodSeconds,
      preventPodSchedulingIfMissing, serviceAccountTokenInSecrets).
      Fixed a pre-existing bug found while verifying the new DRA classes:
      the api_version group map in IO::K8s::Role::APIObject was missing
      an entry for "resource", so every Api::Resource::* class (both the
      pre-existing v1alpha3 ones and the new ones added here) rendered
      apiVersion as "resource/vX" instead of the correct
      "resource.k8s.io/vX" -- would have produced manifests rejected by
      any real cluster. Fixed by adding resource => 'resource.k8s.io' to
      the map.
      Removed four resource.k8s.io/v1alpha3 classes --
      PodSchedulingContext, PodSchedulingContextSpec,
      PodSchedulingContextStatus, ResourceClaimSchedulingStatus --
      confirmed removed from the live v1.36 API (the "classic DRA"
      control-plane-controller allocation flow they supported was fully
      superseded by the new v1 structured-parameters model). Unlike the
      admission/auth/flowcontrol classes noted above, these had no
      lingering old-cluster back-compat rationale to keep them (DRA
      itself was alpha-only when these shipped, never GA, so there is no
      "still-supported old cluster" using them the way there is for a
      long-GA API's deprecated beta track) -- maintainer confirmed
      removal. Since these names already shipped in IO-K8s 1.100, PAUSE
      redirect tombstones for all four were added to the companion
      IO::K8s::Deprecated distribution rather than left to silently
      orphan.

1.100     2026-03-25 19:17:28Z
  - Added IO::K8s::AgentSandbox with 4 CRD classes for agent-sandbox (v0.2.1):
    Sandbox (agents.x-k8s.io/v1alpha1), SandboxClaim, SandboxTemplate,
    SandboxWarmPool (extensions.agents.x-k8s.io/v1alpha1)
  - Added inline struct support to k8s DSL: hash specs with multiple keys now
    auto-generate typed inner classes with hashref coercion, supporting nested
    structs and proper TO_JSON serialization
  - Upgraded IO::K8s::Cilium from ~v1.17 to v1.19.2: promoted 6 BGP CRDs and
    CiliumCIDRGroup from v2alpha1 to v2, removed CiliumExternalWorkload and
    CiliumBGPPeeringPolicy, fixed scope on CiliumEgressGatewayPolicy and
    CiliumGatewayClassConfig (23 → 21 CRDs)
  - Upgraded IO::K8s::K3s to v1.35.1+k3s1: added ETCDSnapshotFile
    (cluster-scoped, k3s.cattle.io/v1) (3 → 4 CRDs)
  - Added upstream_version() method to all CRD providers for tracking which
    upstream release each provider is based on

1.010     2026-03-24 13:17:59Z
  - Added missing Test::Deep test dependency (CPAN Testers failure)

1.009     2026-03-19 21:29:24Z
  - Fix: coerce \0/\1 refs to plain 0/1 for Bool attributes — Perl's \0 is a
    truthy reference, so users passing \0 expecting JSON false got wrong results.
    Bool attributes now dereference scalar refs before coercing.
  - Updated dist.ini authors and copyright holder

1.008     2026-03-08 23:36:05Z
  - Fix to_yaml handling for JSON booleans in resource hashes — TO_YAML now uses
    YAML::PP with JSON schema and JSON::PP boolean mode, so JSON::PP::true/false
    serialize as true/false instead of 1/0 or refs.
  - Added test for CRD to_yaml with JSON booleans in passthrough spec hash.

1.007     2026-03-08 15:01:36Z
  - Fix import leak: using an IO::K8s class (e.g. use IO::K8s::Api::Core::V1::Secret)
    no longer contaminates the caller's namespace with the k8s DSL function or
    IO::K8s::Role::Resource. Previously, _setup_class used extends (inheritance),
    causing every IO::K8s subclass to inherit Resource's import method and re-run
    _setup_class on any package that loaded it. Now uses Moo::Role composition
    (apply_roles_to_package) instead, so the import method stays in IO::K8s::Resource.
  - IO::K8s::Resource no longer composes IO::K8s::Role::Resource itself (it is
    NOT a resource, it is the DSL/setup class).
  - _k8s_attr_info is now called as a method on the target class (via the composed
    Role::Resource) instead of as a direct function call on IO::K8s::Resource.
  - Added regression test t/27_no_import_leak.t.

1.006     2026-03-01 02:55:20Z
  - Fix struct_to_object destroying pre-built objects: when a typed IO::K8s
    object was passed into a parent constructor (e.g. PersistentVolumeClaim in
    StatefulSet.volumeClaimTemplates), _inflate_struct treated the blessed
    object as a non-HASH ref and returned {}, producing an empty object.
    Now passes through objects that already match the expected class.
  - Harden _inflate_struct against blessed objects: if a blessed object
    reaches _inflate_struct despite the struct_to_object guard, it now
    extracts data via TO_JSON instead of silently returning {}.
  - Add explicit Scalar::Util import to IO::K8s (was used unimported).
  - Added comprehensive test suite: passthrough tests (t/23), combination
    tests with 35 subtests (t/24), real-world YAML manifest round-trip tests
    from popular projects like Kubernetes Dashboard, CoreDNS, Prometheus,
    Argo CD, NGINX Ingress, Grafana (t/25), and build-and-verify tests
    constructing manifests via Perl API and checking output (t/26).
    Total: 1047 assertions across 29 test files.

1.005     2026-02-28 06:43:29Z
  - Fixed _inflate_struct not recognizing sanitized field names (x-kubernetes-*,
    $ref, etc.) during JSON deserialization — Bool coercion and type inflation
    were skipped for these fields, causing JSON::PP::Boolean type constraint
    failures on e.g. x-kubernetes-preserve-unknown-fields

1.004     2026-02-28 05:42:53Z
  - Fixed JSONSchemaProps compilation: field names with special characters
    ($ref, $schema, x-kubernetes-*) are now automatically sanitized to valid
    Perl identifiers (e.g. _ref, _schema, x_kubernetes_embedded_resource)
    with init_arg mapping so constructors still accept the original JSON keys
  - Added comprehensive compile test (t/02_compile_all.t) loading all 697 modules

1.003     2026-02-28 00:35:50Z

1.002     2026-02-23 02:37:32Z
  - Added Authorization API types to default resource map
    (SelfSubjectRulesReview, SelfSubjectAccessReview, SubjectAccessReview,
    LocalSubjectAccessReview)
  - Added Authentication API types to default resource map
    (TokenReview, TokenRequest, SelfSubjectReview)

1.001     2026-02-14 19:07:12Z
  - Added IO::K8s::Types::Net with Net::IP-backed IP/CIDR type constraints
  - Added convenience methods to IO::K8s::Role::APIObject: labels, annotations,
    status conditions, and owner references for all API objects
  - Added IO::K8s::Role::SpecBuilder for deep-path spec manipulation on CRD classes
  - Added IO::K8s::Role::NetworkPolicy for building network policies (core K8s + Cilium)
  - Added IO::K8s::Role::Routable for HTTP/gRPC routing (Ingress, HTTPRoute, IngressRoute)
  - Added IO::K8s::Role::CertManaged for cert-manager Certificate/Issuer building
  - Added IO::K8s::Role::HelmManaged for K3s HelmChart/HelmChartConfig building
  - Added IO::K8s::Role::Loadbalanced for TraefikService traffic distribution
  - Added IO::K8s::Role::MiddlewareBuilder for Traefik middleware configuration
  - SpecBuilder is auto-applied to all CRD classes via IO::K8s::APIObject import
  - Added Net::IP as dependency
  - Added IO::K8s::Traefik with 10 Traefik CRD classes (traefik.io/v1alpha1)
  - Added IO::K8s::K3s with 3 K3s CRD classes (helm.cattle.io/v1 and k3s.cattle.io/v1)
  - Added IO::K8s::CertManager with 6 cert-manager CRD classes (cert-manager.io/v1 and acme.cert-manager.io/v1)
  - Added IO::K8s::GatewayAPI with 5 Gateway API CRD classes (gateway.networking.k8s.io/v1 and v1beta1)
  - Added IO::K8s::Cilium with 23 Cilium CRD classes (cilium.io/v2 and cilium.io/v2alpha1)
  - Added IO::K8s::Role::ResourceMap for external resource map providers
  - Added add() method to merge external resource maps (e.g. CRD packages)
  - Added 'with' constructor parameter for merging resource maps at construction
  - Added domain-qualified resource names (api_version/Kind) for collision handling
  - expand_class(), new_object(), inflate() now support api_version disambiguation
  - pk8s DSL functions accept optional api_version second argument
  - resource_map default now returns a copy per instance (safe for mutation)
  - Updated original author email and copyright holder

1.000     2026-02-13 01:52:24Z
  - Major refactoring: Migrated from Moose to Moo for lighter dependencies
  - Replaced YAML::XS with YAML::PP for pure Perl implementation
  - Updated to Kubernetes v1.31 API objects
  - New IO::K8s::List class for unified list handling
  - Improved attribute metadata handling with IO::K8s::Resource
  - Added IO::K8s::APIObject and IO::K8s::Role::APIObject for better API object support
  - Added support for namespaced resources via IO::K8s::Role::Namespaced
  - Added resource_plural support to IO::K8s::Role::APIObject for CRD classes
  - IO::K8s::APIObject accepts import parameters (api_version, resource_plural) for CRDs
  - IO::K8s::AutoGen supports CRD metadata (api_version, kind, resource_plural, is_namespaced)
  - expand_class recognizes already-loaded classes (needed for CRD classes)
  - Improved JSON serialization with canonical output
  - Better handling of opaque fields (fieldsV1, rawExtension, raw)
  - Deprecation stubs for removed List classes (backwards compatibility)
  - Added IO::K8s::Types for Type::Tiny type library integration

0.04     2018-12-11 00:00:00Z
  - Update to Kubernetes v1.14 API objects

0.03     2018-05-15 00:00:00Z
  - add convenience to_json method to all objects (meis)
  - add Test::Exception as dependency (manwar)

0.02     2018-05-08 00:00:00Z
  - Kubernetes API sometimes returns key => undef. We handle this not initializing the attribute to undef,
    since it's functionally the same to initialize it to undef or not initialize it

0.01     2018-05-04 00:00:00Z
  - Initial release to an unsuspecting world
