kubernetes/kubernetes

Migrate 'Good First Migration' Immutable Fields to Declarative Validation

Open

#136 785 ouverte le 5 févr. 2026

Voir sur GitHub
 (21 commentaires) (1 réaction) (6 assignés)Go (43 066 forks)batch import
area/api-validationhelp wantedsig/api-machinerytriage/accepted

Métriques du dépôt

Stars
 (122 268 stars)
Métriques de merge PR
 (Merge moyen 28j 17h) (343 PRs mergées en 30 j)

Description

What would you like to be added?

This issue tracks a number of identified "good first migration" cases from handwritten Go code to Declarative Validation tags (+k8s:immutable or +k8s:update=NoModify) as part of KEP-5073. This issue aims to be a more narrowed down list with specific fields and validation tags than the general tracking issues. The goal is to have users more easily able to aid in the migration by first having a set of "Good First Migration" cases with minimal migration friction and build from there.

For more information on the tasks here, the Declarative Validation work group (guiding this and similar issues) has open discussion in kubernetes Slack channel: #sig-api-machinery-dev-tools

We are targeting fields that meet the "Good First Migration" criteria:

  1. Validation Logic: Simple immutability checks (typically using apivalidation.ValidateImmutableField or direct equality checks between new and old objects).
  2. Versioning: The field exists at the same path across all API versions.
  3. Status: The field has not yet been migrated (does not currently have a +k8s:immutable or +k8s:update tag).

IMPORTANT! - PR Structure

The ideal PR structure for a PR contributing a DV tag migration is:

  • Commit # 1: IF NOT ALREADY WIRED UP FOR DV - Wire up the apigroup, add empty test and empty generated file.
  • Commit # 2: Use a single DV tag on a single field. E.g. Add +k8s:immutable to an immutable field. Includes generated code and robust test cases. Emphasis on test-cases -- we want enough to prove that a field is correct, but we don't want to duplicate all the per-tag tests.
  • Commits # 3...N: Use DV tags on the same field, one per commit; repeat until that field is done or all that is left is not handled by DV yet.
  • Commits N+1...M: Repeat the above for more fields.

The emphasis is on lots of small commits. Maintaining commit discipline is hard but worthwhile.

Resources

Understanding +k8s:update Options

When migrating immutable fields, you will primarily use +k8s:immutable or +k8s:update=NoModify. Here is when to use which:

  • +k8s:immutable: Use this for fields that, once set (during creation or update), can never be changed. It is the most common tag for "immutable" fields. It is equivalent to +k8s:update=NoModify but conveys the intent of "immutability" more clearly for the entire lifecycle.
  • +k8s:update=NoModify: Use this if you specifically want to enforce that the value cannot change during updates.
  • +k8s:update=NoSet: Use this if a field cannot be set (transition from nil to non-nil) during an update if it was previously unset.
  • +k8s:update=NoUnset: Use this if a field cannot be unset (transition from non-nil to nil) during an update if it was previously set.

For the purpose of this "Good First Migration" issue, most fields listed are strictly immutable, so +k8s:immutable is the preferred choice unless the existing validation logic explicitly allows transitions (e.g., allow setting once but not modifying afterwards).

Task List

The following fields have been identified as good candidates for the initial migration.

Apps (apps)

  • ControllerRevision.Data
    • Field: Data
    • Location:
      • staging/src/k8s.io/api/apps/v1/types.go
      • staging/src/k8s.io/api/apps/v1beta1/types.go
      • staging/src/k8s.io/api/apps/v1beta2/types.go
  • Deployment.Spec.Selector
    • Field: Spec.Selector
    • Location:
      • staging/src/k8s.io/api/apps/v1/types.go
      • staging/src/k8s.io/api/apps/v1beta1/types.go
      • staging/src/k8s.io/api/apps/v1beta2/types.go
  • ReplicaSet.Spec.Selector
    • Field: Spec.Selector
    • Location:
      • staging/src/k8s.io/api/apps/v1/types.go
      • staging/src/k8s.io/api/apps/v1beta2/types.go
  • StatefulSet.Spec.Selector
    • Field: Spec.Selector
    • Location:
      • staging/src/k8s.io/api/apps/v1/types.go
      • staging/src/k8s.io/api/apps/v1beta1/types.go
      • staging/src/k8s.io/api/apps/v1beta2/types.go

Batch (batch)

  • Job.Spec.Selector
    • Field: Spec.Selector
    • Location:
      • staging/src/k8s.io/api/batch/v1/types.go
      • staging/src/k8s.io/api/batch/v1beta1/types.go

Certificates (certificates.k8s.io)

  • CertificateSigningRequest.Spec (Multiple Fields)
    • Fields: Request, SignerName, ExpirationSeconds, Usages
    • Location:
      • staging/src/k8s.io/api/certificates/v1/types.go
      • staging/src/k8s.io/api/certificates/v1beta1/types.go
  • ClusterTrustBundle.Spec.SignerName
    • Field: Spec.SignerName
    • Location: staging/src/k8s.io/api/certificates/v1alpha1/types.go

Core (core)

  • Node.Spec (Multiple Fields)
    • Fields: PodCIDR, PodCIDRs, ProviderID
    • Location: staging/src/k8s.io/api/core/v1/types.go
  • PersistentVolumeClaim.Spec.VolumeName
    • Field: Spec.VolumeName
    • Location: staging/src/k8s.io/api/core/v1/types.go
  • ResourceQuota.Spec.Scopes
    • Field: Spec.Scopes
    • Location: staging/src/k8s.io/api/core/v1/types.go
  • Secret.Type
    • Field: Type
    • Location: staging/src/k8s.io/api/core/v1/types.go
  • Service.Spec.ClusterIP
    • Field: Spec.ClusterIP
    • Location: staging/src/k8s.io/api/core/v1/types.go

Discovery (discovery.k8s.io)

  • EndpointSlice.AddressType
    • Field: AddressType
    • Location:
      • staging/src/k8s.io/api/discovery/v1/types.go
      • staging/src/k8s.io/api/discovery/v1beta1/types.go

Networking (networking.k8s.io)

  • IngressClass.Spec.Controller
    • Field: Spec.Controller
    • Location:
      • staging/src/k8s.io/api/networking/v1/types.go
      • staging/src/k8s.io/api/networking/v1beta1/types.go
  • IPAddress.Spec.ParentRef
    • Field: Spec.ParentRef
    • Location:
      • staging/src/k8s.io/api/networking/v1/types.go
      • staging/src/k8s.io/api/networking/v1beta1/types.go

Node (node.k8s.io)

  • RuntimeClass.Handler
    • Field: Handler
    • Location:
      • staging/src/k8s.io/api/node/v1/types.go
      • staging/src/k8s.io/api/node/v1beta1/types.go
      • staging/src/k8s.io/api/node/v1alpha1/types.go

RBAC (rbac.authorization.k8s.io)

  • RoleBinding.RoleRef
    • Field: RoleRef
    • Location:
      • staging/src/k8s.io/api/rbac/v1/types.go
      • staging/src/k8s.io/api/rbac/v1beta1/types.go
      • staging/src/k8s.io/api/rbac/v1alpha1/types.go
  • ClusterRoleBinding.RoleRef
    • Field: RoleRef
    • Location:
      • staging/src/k8s.io/api/rbac/v1/types.go
      • staging/src/k8s.io/api/rbac/v1beta1/types.go
      • staging/src/k8s.io/api/rbac/v1alpha1/types.go

Scheduling (scheduling.k8s.io)

  • PriorityClass.Value
    • Field: Value
    • Location:
      • staging/src/k8s.io/api/scheduling/v1/types.go
      • staging/src/k8s.io/api/scheduling/v1beta1/types.go
      • staging/src/k8s.io/api/scheduling/v1alpha1/types.go

Storage (storage.k8s.io)

  • CSIDriver.Spec.AttachRequired
    • Field: Spec.AttachRequired
    • Location:
      • staging/src/k8s.io/api/storage/v1/types.go
      • staging/src/k8s.io/api/storage/v1beta1/types.go
  • StorageClass (Multiple Fields)
    • Fields: Provisioner, Parameters, ReclaimPolicy, VolumeBindingMode
    • Location:
      • staging/src/k8s.io/api/storage/v1/types.go
      • staging/src/k8s.io/api/storage/v1beta1/types.go
      • staging/src/k8s.io/api/storage/v1alpha1/types.go
  • VolumeAttachment.Spec (Multiple Fields)
    • Fields: Attacher, Source, NodeName
    • Location:
      • staging/src/k8s.io/api/storage/v1/types.go
      • staging/src/k8s.io/api/storage/v1beta1/types.go
      • staging/src/k8s.io/api/storage/v1alpha1/types.go

How to Migrate a Field

  1. Locate the Versioned Type: Find the types.go file for the versioned API.
  2. Add the Tag: Add // +k8s:immutable (or // +k8s:update=NoModify) above the field definition.
  3. Generate: Run hack/update-codegen.sh validation.
  4. Update Strategy: In the resource's strategy.go (usually in pkg/registry/<group>/<resource>/), update the ValidateUpdate function to use rest.ValidateUpdateDeclarativelyWithMigrationChecks (if available/applicable for update validation).
  5. Verify:
    • Ensure unit tests pass.
    • Run declarative_validation_test.go (add one if missing) to prove equivalence.
  6. Mark Covered: In the original validation.go, find the manual check (typically apivalidation.ValidateImmutableField) and append .MarkCoveredByDeclarative().

Example of marking hand-written validation code:

allErrs = append(allErrs, apivalidation.ValidateImmutableField(newObj.Spec.Selector, oldObj.Spec.Selector, fldPath.Child("spec", "selector")).MarkCoveredByDeclarative()...)

Why is this needed?

This helps onboard the community to DV, improves the APIs touched, and helps in the process of getting Declarative Validation to its stated goal of migrating ~50% of existing hand-written code to DV over 5 releases -> improved API readability and more declarative APIs.

Guide contributeur