Revision history for IO-K8s

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

  - Resolve resource_map short-name keys on the explicit-apiVersion path
    (karr #31). 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 karr #17 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 (karr #32). 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 karr #11.
  - 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 karr
    #4-#8'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
