feat: add +k8s:update tag and associated tests

This commit introduces the +k8s:update tag to define validation rules that apply only on resource updates. This enables declarative immutability checks (e.g., NoModify), moving validation logic into the API types.

As part of this change, the existing +k8s:immutable tag was also refined for more consistent er
reporting. Unit tests are included to verify the new functionality.
This commit is contained in:
Aaron Prindle
2025-09-14 00:24:49 +00:00
committed by yongruilin
parent 4d61ba787d
commit 331ea38769
6 changed files with 1855 additions and 0 deletions

View File

@@ -0,0 +1,160 @@
/*
Copyright 2025 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package validate
import (
"context"
"k8s.io/apimachinery/pkg/api/equality"
"k8s.io/apimachinery/pkg/api/operation"
"k8s.io/apimachinery/pkg/util/validation/field"
)
// UpdateConstraint represents a constraint on update operations
type UpdateConstraint int
const (
// NoSet prevents unset->set transitions
NoSet UpdateConstraint = iota
// NoUnset prevents set->unset transitions
NoUnset
// NoModify prevents value changes but allows set/unset transitions
NoModify
)
// UpdateValueByCompare verifies update constraints for comparable value types.
func UpdateValueByCompare[T comparable](_ context.Context, op operation.Operation, fldPath *field.Path, value, oldValue *T, constraints ...UpdateConstraint) field.ErrorList {
if op.Type != operation.Update {
return nil
}
var errs field.ErrorList
var zero T
for _, constraint := range constraints {
switch constraint {
case NoSet:
if *oldValue == zero && *value != zero {
errs = append(errs, field.Invalid(fldPath, nil, "field cannot be set once created").WithOrigin("update"))
}
case NoUnset:
if *oldValue != zero && *value == zero {
errs = append(errs, field.Invalid(fldPath, nil, "field cannot be cleared once set").WithOrigin("update"))
}
case NoModify:
// Rely on validation ratcheting to detect that the value has changed.
// This check only verifies that the field was set in both the old and
// new objects, confirming it was a modification, not a set/unset.
if *oldValue != zero && *value != zero {
errs = append(errs, field.Invalid(fldPath, nil, "field cannot be modified once set").WithOrigin("update"))
}
}
}
return errs
}
// UpdatePointer verifies update constraints for pointer types.
func UpdatePointer[T any](_ context.Context, op operation.Operation, fldPath *field.Path, value, oldValue *T, constraints ...UpdateConstraint) field.ErrorList {
if op.Type != operation.Update {
return nil
}
var errs field.ErrorList
for _, constraint := range constraints {
switch constraint {
case NoSet:
if oldValue == nil && value != nil {
errs = append(errs, field.Invalid(fldPath, nil, "field cannot be set once created").WithOrigin("update"))
}
case NoUnset:
if oldValue != nil && value == nil {
errs = append(errs, field.Invalid(fldPath, nil, "field cannot be cleared once set").WithOrigin("update"))
}
case NoModify:
// Rely on validation ratcheting to detect that the value has changed.
// This check only verifies that the field was non-nil in both the old
// and new objects, confirming it was a modification, not a set/unset.
if oldValue != nil && value != nil {
errs = append(errs, field.Invalid(fldPath, nil, "field cannot be modified once set").WithOrigin("update"))
}
}
}
return errs
}
// UpdateValueByReflect verifies update constraints for non-comparable value types using reflection.
func UpdateValueByReflect[T any](_ context.Context, op operation.Operation, fldPath *field.Path, value, oldValue *T, constraints ...UpdateConstraint) field.ErrorList {
if op.Type != operation.Update {
return nil
}
var errs field.ErrorList
var zero T
valueIsZero := equality.Semantic.DeepEqual(*value, zero)
oldValueIsZero := equality.Semantic.DeepEqual(*oldValue, zero)
for _, constraint := range constraints {
switch constraint {
case NoSet:
if oldValueIsZero && !valueIsZero {
errs = append(errs, field.Invalid(fldPath, nil, "field cannot be set once created").WithOrigin("update"))
}
case NoUnset:
if !oldValueIsZero && valueIsZero {
errs = append(errs, field.Invalid(fldPath, nil, "field cannot be cleared once set").WithOrigin("update"))
}
case NoModify:
// Rely on validation ratcheting to detect that the value has changed.
// This check only verifies that the field was set in both the old and
// new objects, confirming it was a modification, not a set/unset.
if !oldValueIsZero && !valueIsZero {
errs = append(errs, field.Invalid(fldPath, nil, "field cannot be modified once set").WithOrigin("update"))
}
}
}
return errs
}
// UpdateStruct verifies update constraints for non-pointer struct types.
// Non-pointer structs are always considered "set" and never "unset".
func UpdateStruct[T any](_ context.Context, op operation.Operation, fldPath *field.Path, value, oldValue *T, constraints ...UpdateConstraint) field.ErrorList {
if op.Type != operation.Update {
return nil
}
var errs field.ErrorList
for _, constraint := range constraints {
switch constraint {
case NoSet, NoUnset:
// These constraints don't apply to non-pointer structs
// as they can't be unset. This should be caught at generation time.
continue
case NoModify:
// Non-pointer structs are always considered "set". Therefore, any
// change detected by validation ratcheting is a modification.
// The deep equality check is redundant and has been removed.
errs = append(errs, field.Invalid(fldPath, nil, "field cannot be modified once set").WithOrigin("update"))
}
}
return errs
}

View File

@@ -0,0 +1,430 @@
/*
Copyright 2025 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package validate
import (
"context"
"testing"
"k8s.io/apimachinery/pkg/api/operation"
"k8s.io/apimachinery/pkg/util/validation/field"
)
func TestUpdateValue(t *testing.T) {
tests := []struct {
name string
op operation.Type
value string
oldValue string
constraints []UpdateConstraint
wantErrs int
wantMsgs []string
}{
{
name: "create operation - no validation",
op: operation.Create,
value: "value",
oldValue: "",
constraints: []UpdateConstraint{NoSet, NoUnset, NoModify},
wantErrs: 0,
},
{
name: "NoSet - unset to set transition (forbidden)",
op: operation.Update,
value: "value",
oldValue: "",
constraints: []UpdateConstraint{NoSet},
wantErrs: 1,
wantMsgs: []string{"field cannot be set once created"},
},
{
name: "NoSet - set to set transition (allowed)",
op: operation.Update,
value: "value2",
oldValue: "value1",
constraints: []UpdateConstraint{NoSet},
wantErrs: 0,
},
{
name: "NoUnset - set to unset transition (forbidden)",
op: operation.Update,
value: "",
oldValue: "value",
constraints: []UpdateConstraint{NoUnset},
wantErrs: 1,
wantMsgs: []string{"field cannot be cleared once set"},
},
{
name: "NoUnset - unset to set transition (allowed)",
op: operation.Update,
value: "value",
oldValue: "",
constraints: []UpdateConstraint{NoUnset},
wantErrs: 0,
},
{
name: "NoModify - set to different value (forbidden)",
op: operation.Update,
value: "value2",
oldValue: "value1",
constraints: []UpdateConstraint{NoModify},
wantErrs: 1,
wantMsgs: []string{"field cannot be modified once set"},
},
{
name: "NoModify - unset to set transition (allowed)",
op: operation.Update,
value: "value",
oldValue: "",
constraints: []UpdateConstraint{NoModify},
wantErrs: 0,
},
{
name: "NoModify - set to unset transition (allowed)",
op: operation.Update,
value: "",
oldValue: "value",
constraints: []UpdateConstraint{NoModify},
wantErrs: 0,
},
{
name: "Multiple constraints - NoSet and NoUnset",
op: operation.Update,
value: "value",
oldValue: "",
constraints: []UpdateConstraint{NoSet, NoUnset},
wantErrs: 1,
wantMsgs: []string{"field cannot be set once created"},
},
{
name: "Multiple constraints - NoUnset and NoModify",
op: operation.Update,
value: "",
oldValue: "value",
constraints: []UpdateConstraint{NoUnset, NoModify},
wantErrs: 1,
wantMsgs: []string{"field cannot be cleared once set"},
},
{
name: "Multiple constraints - NoSet, NoUnset, NoModify - modify attempt",
op: operation.Update,
value: "value2",
oldValue: "value1",
constraints: []UpdateConstraint{NoSet, NoUnset, NoModify},
wantErrs: 1,
wantMsgs: []string{"field cannot be modified once set"},
},
{
name: "No constraints",
op: operation.Update,
value: "value2",
oldValue: "value1",
constraints: []UpdateConstraint{},
wantErrs: 0,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
op := operation.Operation{Type: tt.op}
errs := UpdateValueByCompare(context.TODO(), op, field.NewPath("test"), &tt.value, &tt.oldValue, tt.constraints...)
if len(errs) != tt.wantErrs {
t.Errorf("UpdateValue() returned %d errors, want %d: %v", len(errs), tt.wantErrs, errs)
}
for i, msg := range tt.wantMsgs {
if i >= len(errs) {
t.Errorf("Expected error message %q not found", msg)
continue
}
if errs[i].Detail != msg {
t.Errorf("UpdateValue() error message = %q, want %q", errs[i].Detail, msg)
}
}
})
}
}
func TestUpdatePointer(t *testing.T) {
stringPtr := func(s string) *string { return &s }
tests := []struct {
name string
op operation.Type
value *string
oldValue *string
constraints []UpdateConstraint
wantErrs int
wantMsgs []string
}{
{
name: "create operation - no validation",
op: operation.Create,
value: stringPtr("value"),
oldValue: nil,
constraints: []UpdateConstraint{NoSet, NoUnset, NoModify},
wantErrs: 0,
},
{
name: "NoSet - nil to non-nil transition (forbidden)",
op: operation.Update,
value: stringPtr("value"),
oldValue: nil,
constraints: []UpdateConstraint{NoSet},
wantErrs: 1,
wantMsgs: []string{"field cannot be set once created"},
},
{
name: "NoSet - non-nil to non-nil transition (allowed)",
op: operation.Update,
value: stringPtr("value2"),
oldValue: stringPtr("value1"),
constraints: []UpdateConstraint{NoSet},
wantErrs: 0,
},
{
name: "NoUnset - non-nil to nil transition (forbidden)",
op: operation.Update,
value: nil,
oldValue: stringPtr("value"),
constraints: []UpdateConstraint{NoUnset},
wantErrs: 1,
wantMsgs: []string{"field cannot be cleared once set"},
},
{
name: "NoUnset - nil to non-nil transition (allowed)",
op: operation.Update,
value: stringPtr("value"),
oldValue: nil,
constraints: []UpdateConstraint{NoUnset},
wantErrs: 0,
},
{
name: "NoModify - different values (forbidden)",
op: operation.Update,
value: stringPtr("value2"),
oldValue: stringPtr("value1"),
constraints: []UpdateConstraint{NoModify},
wantErrs: 1,
wantMsgs: []string{"field cannot be modified once set"},
},
{
name: "NoModify - nil to non-nil transition (allowed)",
op: operation.Update,
value: stringPtr("value"),
oldValue: nil,
constraints: []UpdateConstraint{NoModify},
wantErrs: 0,
},
{
name: "NoModify - non-nil to nil transition (allowed)",
op: operation.Update,
value: nil,
oldValue: stringPtr("value"),
constraints: []UpdateConstraint{NoModify},
wantErrs: 0,
},
{
name: "Multiple constraints - all three",
op: operation.Update,
value: stringPtr("value2"),
oldValue: stringPtr("value1"),
constraints: []UpdateConstraint{NoSet, NoUnset, NoModify},
wantErrs: 1,
wantMsgs: []string{"field cannot be modified once set"},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
op := operation.Operation{Type: tt.op}
errs := UpdatePointer(context.TODO(), op, field.NewPath("test"), tt.value, tt.oldValue, tt.constraints...)
if len(errs) != tt.wantErrs {
t.Errorf("UpdatePointer() returned %d errors, want %d: %v", len(errs), tt.wantErrs, errs)
}
for i, msg := range tt.wantMsgs {
if i >= len(errs) {
t.Errorf("Expected error message %q not found", msg)
continue
}
if errs[i].Detail != msg {
t.Errorf("UpdatePointer() error message = %q, want %q", errs[i].Detail, msg)
}
}
})
}
}
func TestUpdateValueByReflect(t *testing.T) {
type CustomStruct struct {
Field1 string
Field2 int
}
tests := []struct {
name string
op operation.Type
value CustomStruct
oldValue CustomStruct
constraints []UpdateConstraint
wantErrs int
wantMsgs []string
}{
{
name: "NoModify - zero to non-zero transition (allowed)",
op: operation.Update,
value: CustomStruct{Field1: "test", Field2: 42},
oldValue: CustomStruct{},
constraints: []UpdateConstraint{NoModify},
wantErrs: 0,
},
{
name: "NoModify - non-zero to zero transition (allowed)",
op: operation.Update,
value: CustomStruct{},
oldValue: CustomStruct{Field1: "test", Field2: 42},
constraints: []UpdateConstraint{NoModify},
wantErrs: 0,
},
{
name: "NoModify - different values (forbidden)",
op: operation.Update,
value: CustomStruct{Field1: "test2", Field2: 100},
oldValue: CustomStruct{Field1: "test1", Field2: 42},
constraints: []UpdateConstraint{NoModify},
wantErrs: 1,
wantMsgs: []string{"field cannot be modified once set"},
},
{
name: "NoSet - zero to non-zero (forbidden)",
op: operation.Update,
value: CustomStruct{Field1: "test", Field2: 42},
oldValue: CustomStruct{},
constraints: []UpdateConstraint{NoSet},
wantErrs: 1,
wantMsgs: []string{"field cannot be set once created"},
},
{
name: "NoUnset - non-zero to zero (forbidden)",
op: operation.Update,
value: CustomStruct{},
oldValue: CustomStruct{Field1: "test", Field2: 42},
constraints: []UpdateConstraint{NoUnset},
wantErrs: 1,
wantMsgs: []string{"field cannot be cleared once set"},
},
{
name: "Multiple constraints",
op: operation.Update,
value: CustomStruct{Field1: "test", Field2: 42},
oldValue: CustomStruct{},
constraints: []UpdateConstraint{NoSet, NoModify},
wantErrs: 1,
wantMsgs: []string{"field cannot be set once created"},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
op := operation.Operation{Type: tt.op}
errs := UpdateValueByReflect(context.TODO(), op, field.NewPath("test"), &tt.value, &tt.oldValue, tt.constraints...)
if len(errs) != tt.wantErrs {
t.Errorf("UpdateValueByReflect() returned %d errors, want %d: %v", len(errs), tt.wantErrs, errs)
}
for i, msg := range tt.wantMsgs {
if i >= len(errs) {
t.Errorf("Expected error message %q not found", msg)
continue
}
if errs[i].Detail != msg {
t.Errorf("UpdateValueByReflect() error message = %q, want %q", errs[i].Detail, msg)
}
}
})
}
}
func TestUpdateStruct(t *testing.T) {
type TestStruct struct {
Field1 string
Field2 int
}
tests := []struct {
name string
op operation.Type
value TestStruct
oldValue TestStruct
constraints []UpdateConstraint
wantErrs int
wantMsgs []string
}{
{
name: "create operation - no validation",
op: operation.Create,
value: TestStruct{Field1: "test", Field2: 42},
oldValue: TestStruct{},
constraints: []UpdateConstraint{NoModify},
wantErrs: 0,
},
{
name: "NoModify - different values (forbidden)",
op: operation.Update,
value: TestStruct{Field1: "test2", Field2: 100},
oldValue: TestStruct{Field1: "test1", Field2: 42},
constraints: []UpdateConstraint{NoModify},
wantErrs: 1,
wantMsgs: []string{"field cannot be modified once set"},
},
{
name: "NoModify - zero value to non-zero (forbidden)",
op: operation.Update,
value: TestStruct{Field1: "test", Field2: 42},
oldValue: TestStruct{},
constraints: []UpdateConstraint{NoModify},
wantErrs: 1,
wantMsgs: []string{"field cannot be modified once set"},
},
{
name: "NoSet and NoUnset with modification - only NoModify triggers",
op: operation.Update,
value: TestStruct{Field1: "test2", Field2: 100},
oldValue: TestStruct{Field1: "test1", Field2: 42},
constraints: []UpdateConstraint{NoSet, NoUnset, NoModify},
wantErrs: 1,
wantMsgs: []string{"field cannot be modified once set"},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
op := operation.Operation{Type: tt.op}
errs := UpdateStruct(context.TODO(), op, field.NewPath("test"), &tt.value, &tt.oldValue, tt.constraints...)
if len(errs) != tt.wantErrs {
t.Errorf("UpdateStruct() returned %d errors, want %d: %v", len(errs), tt.wantErrs, errs)
}
for i, msg := range tt.wantMsgs {
if i >= len(errs) {
t.Errorf("Expected error message %q not found", msg)
continue
}
if errs[i].Detail != msg {
t.Errorf("UpdateStruct() error message = %q, want %q", errs[i].Detail, msg)
}
}
})
}
}

View File

@@ -0,0 +1,125 @@
/*
Copyright 2025 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
// +k8s:validation-gen=TypeMeta
// +k8s:validation-gen-scheme-registry=k8s.io/code-generator/cmd/validation-gen/testscheme.Scheme
// This is a test package for the +k8s:update tag.
package update
import "k8s.io/code-generator/cmd/validation-gen/testscheme"
var localSchemeBuilder = testscheme.New()
type UpdateTestStruct struct {
TypeMeta int
// +k8s:update=NoSet
StringNoSet string `json:"stringNoSet"`
// +k8s:update=NoUnset
StringNoUnset string `json:"stringNoUnset"`
// +k8s:update=NoModify
StringNoModify string `json:"stringNoModify"`
// +k8s:update=NoSet
// +k8s:update=NoModify
// +k8s:update=NoUnset
StringFullyRestricted string `json:"stringFullyRestricted"`
// +k8s:update=NoModify
// +k8s:update=NoUnset
StringSetOnce string `json:"stringSetOnce"`
// +k8s:update=NoModify
IntNoModify int `json:"intNoModify"`
// +k8s:update=NoModify
Int32NoModify int32 `json:"int32NoModify"`
// +k8s:update=NoModify
Int64NoModify int64 `json:"int64NoModify"`
// +k8s:update=NoModify
UintNoModify uint `json:"uintNoModify"`
// +k8s:update=NoModify
BoolNoModify bool `json:"boolNoModify"`
// +k8s:update=NoModify
Float32NoModify float32 `json:"float32NoModify"`
// +k8s:update=NoModify
Float64NoModify float64 `json:"float64NoModify"`
// +k8s:update=NoModify
ByteNoModify byte `json:"byteNoModify"`
// +k8s:update=NoModify
StructNoModify TestStruct `json:"structNoModify"`
// +k8s:update=NoModify
NonComparableStructNoModify NonComparableStruct `json:"nonComparableStructNoModify"`
// Pointer field tests
// +k8s:update=NoSet
PointerNoSet *string `json:"pointerNoSet"`
// +k8s:update=NoUnset
PointerNoUnset *string `json:"pointerNoUnset"`
// +k8s:update=NoModify
PointerNoModify *string `json:"pointerNoModify"`
// +k8s:update=NoSet
// +k8s:update=NoModify
// +k8s:update=NoUnset
PointerFullyRestricted *string `json:"pointerFullyRestricted"`
// +k8s:update=NoModify
IntPointerNoModify *int `json:"intPointerNoModify"`
// +k8s:update=NoModify
BoolPointerNoModify *bool `json:"boolPointerNoModify"`
// +k8s:update=NoModify
StructPointerNoModify *TestStruct `json:"structPointerNoModify"`
// Type alias tests
// +k8s:update=NoModify
CustomTypeNoModify CustomString `json:"customTypeNoModify"`
// +k8s:update=NoSet
CustomTypeNoSet CustomInt `json:"customTypeNoSet"`
}
type TestStruct struct {
StringField string `json:"stringField"`
IntField int `json:"intField"`
}
// NonComparableStruct contains a slice which makes it non-comparable
type NonComparableStruct struct {
SliceField []string `json:"sliceField"`
IntField int `json:"intField"`
}
// Custom types to test type aliases
type CustomString string
type CustomInt int

View File

@@ -0,0 +1,401 @@
/*
Copyright 2025 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package update
import (
"testing"
"k8s.io/apimachinery/pkg/util/validation/field"
"k8s.io/utils/ptr"
)
func TestUpdateTags(t *testing.T) {
st := localSchemeBuilder.Test(t)
baseStruct := UpdateTestStruct{}
// String NoSet
old := baseStruct
old.StringNoSet = "" // unset
new := baseStruct
new.StringNoSet = "value"
st.Value(&new).OldValue(&old).ExpectMatches(field.ErrorMatcher{}.ByType().ByField().ByDetailSubstring().ByOrigin(), field.ErrorList{
field.Invalid(field.NewPath("stringNoSet"), nil, "field cannot be set once created").WithOrigin("update"),
})
st.Value(&old).OldValue(&old).ExpectValid()
// String NoUnset
oldWithValue := baseStruct
oldWithValue.StringNoUnset = "value"
newUnset := baseStruct
newUnset.StringNoUnset = ""
// Can set initially (empty to non-empty)
st.Value(&oldWithValue).OldValue(&baseStruct).ExpectValid()
// Cannot unset (non-empty to empty)
st.Value(&newUnset).OldValue(&oldWithValue).ExpectMatches(field.ErrorMatcher{}.ByType().ByField().ByDetailSubstring().ByOrigin(), field.ErrorList{
field.Invalid(field.NewPath("stringNoUnset"), nil, "field cannot be cleared once set").WithOrigin("update"),
})
// String NoModify
oldEmpty := baseStruct
withValue := baseStruct
withValue.StringNoModify = "value"
modified := baseStruct
modified.StringNoModify = "different"
// Can set initially (empty to non-empty)
st.Value(&withValue).OldValue(&oldEmpty).ExpectValid()
// Can unset (non-empty to empty)
st.Value(&oldEmpty).OldValue(&withValue).ExpectValid()
// Cannot modify (non-empty to different non-empty)
st.Value(&modified).OldValue(&withValue).ExpectMatches(field.ErrorMatcher{}.ByType().ByField().ByDetailSubstring().ByOrigin(), field.ErrorList{
field.Invalid(field.NewPath("stringNoModify"), nil, "field cannot be modified once set").WithOrigin("update"),
})
// String Fully Restricted
oldEmpty = baseStruct
withValue = baseStruct
withValue.StringFullyRestricted = "value"
modified = baseStruct
modified.StringFullyRestricted = "different"
// Cannot set (NoSet)
st.Value(&withValue).OldValue(&oldEmpty).ExpectMatches(field.ErrorMatcher{}.ByType().ByField().ByDetailSubstring().ByOrigin(), field.ErrorList{
field.Invalid(field.NewPath("stringFullyRestricted"), nil, "field cannot be set once created").WithOrigin("update"),
})
// Cannot unset (NoUnset)
st.Value(&oldEmpty).OldValue(&withValue).ExpectMatches(field.ErrorMatcher{}.ByType().ByField().ByDetailSubstring().ByOrigin(), field.ErrorList{
field.Invalid(field.NewPath("stringFullyRestricted"), nil, "field cannot be cleared once set").WithOrigin("update"),
})
// Cannot modify (NoModify)
st.Value(&modified).OldValue(&withValue).ExpectMatches(field.ErrorMatcher{}.ByType().ByField().ByDetailSubstring().ByOrigin(), field.ErrorList{
field.Invalid(field.NewPath("stringFullyRestricted"), nil, "field cannot be modified once set").WithOrigin("update"),
})
// String Set-Once Pattern
oldEmpty = baseStruct
withValue = baseStruct
withValue.StringSetOnce = "value"
modified = baseStruct
modified.StringSetOnce = "different"
// Can set once (empty to non-empty)
st.Value(&withValue).OldValue(&oldEmpty).ExpectValid()
// Cannot unset (NoUnset)
st.Value(&oldEmpty).OldValue(&withValue).ExpectMatches(field.ErrorMatcher{}.ByType().ByField().ByDetailSubstring().ByOrigin(), field.ErrorList{
field.Invalid(field.NewPath("stringSetOnce"), nil, "field cannot be cleared once set").WithOrigin("update"),
})
// Cannot modify (NoModify)
st.Value(&modified).OldValue(&withValue).ExpectMatches(field.ErrorMatcher{}.ByType().ByField().ByDetailSubstring().ByOrigin(), field.ErrorList{
field.Invalid(field.NewPath("stringSetOnce"), nil, "field cannot be modified once set").WithOrigin("update"),
})
// Int NoModify
oldZero := baseStruct
withValue = baseStruct
withValue.IntNoModify = 10
// Can transition from 0 to 10 (unset to set is allowed)
st.Value(&withValue).OldValue(&oldZero).ExpectValid()
// Cannot modify from one non-zero to another
modified = baseStruct
modified.IntNoModify = 20
st.Value(&modified).OldValue(&withValue).ExpectMatches(field.ErrorMatcher{}.ByType().ByField().ByDetailSubstring().ByOrigin(), field.ErrorList{
field.Invalid(field.NewPath("intNoModify"), nil, "field cannot be modified once set").WithOrigin("update"),
})
// Int32 NoModify
old = baseStruct
withValue = baseStruct
withValue.Int32NoModify = 42
// Can set initially
st.Value(&withValue).OldValue(&old).ExpectValid()
// Cannot modify
modified = baseStruct
modified.Int32NoModify = 100
st.Value(&modified).OldValue(&withValue).ExpectMatches(field.ErrorMatcher{}.ByType().ByField().ByDetailSubstring().ByOrigin(), field.ErrorList{
field.Invalid(field.NewPath("int32NoModify"), nil, "field cannot be modified once set").WithOrigin("update"),
})
// Uint NoModify
old = baseStruct
withValue = baseStruct
withValue.UintNoModify = 42
// Can set initially
st.Value(&withValue).OldValue(&old).ExpectValid()
// Cannot modify
modified = baseStruct
modified.UintNoModify = 100
st.Value(&modified).OldValue(&withValue).ExpectMatches(field.ErrorMatcher{}.ByType().ByField().ByDetailSubstring().ByOrigin(), field.ErrorList{
field.Invalid(field.NewPath("uintNoModify"), nil, "field cannot be modified once set").WithOrigin("update"),
})
// Bool NoModify
oldFalse := baseStruct
withTrue := baseStruct
withTrue.BoolNoModify = true
// Can transition from false to true (unset to set)
st.Value(&withTrue).OldValue(&oldFalse).ExpectValid()
// Cannot modify back to false
st.Value(&oldFalse).OldValue(&withTrue).ExpectValid() // This is allowed as it's set->unset
// Float32 NoModify
old = baseStruct
withValue = baseStruct
withValue.Float32NoModify = 3.14
// Can set initially
st.Value(&withValue).OldValue(&old).ExpectValid()
// Cannot modify
modified = baseStruct
modified.Float32NoModify = 2.71
st.Value(&modified).OldValue(&withValue).ExpectMatches(field.ErrorMatcher{}.ByType().ByField().ByDetailSubstring().ByOrigin(), field.ErrorList{
field.Invalid(field.NewPath("float32NoModify"), nil, "field cannot be modified once set").WithOrigin("update"),
})
// Float64 NoModify
oldZero = baseStruct
withValue = baseStruct
withValue.Float64NoModify = 3.14
// Can transition from 0.0 to 3.14
st.Value(&withValue).OldValue(&oldZero).ExpectValid()
// Cannot modify to different value
modified = baseStruct
modified.Float64NoModify = 2.71
st.Value(&modified).OldValue(&withValue).ExpectMatches(field.ErrorMatcher{}.ByType().ByField().ByDetailSubstring().ByOrigin(), field.ErrorList{
field.Invalid(field.NewPath("float64NoModify"), nil, "field cannot be modified once set").WithOrigin("update"),
})
// Byte NoModify
old = baseStruct
withValue = baseStruct
withValue.ByteNoModify = 255
// Can set initially
st.Value(&withValue).OldValue(&old).ExpectValid()
// Cannot modify
modified = baseStruct
modified.ByteNoModify = 128
st.Value(&modified).OldValue(&withValue).ExpectMatches(field.ErrorMatcher{}.ByType().ByField().ByDetailSubstring().ByOrigin(), field.ErrorList{
field.Invalid(field.NewPath("byteNoModify"), nil, "field cannot be modified once set").WithOrigin("update"),
})
// Struct NoModify
withStruct := baseStruct
withStruct.StructNoModify = TestStruct{StringField: "value", IntField: 42}
modifiedStruct := baseStruct
modifiedStruct.StructNoModify = TestStruct{StringField: "different", IntField: 100}
// Cannot modify (struct fields are always set, never unset)
st.Value(&modifiedStruct).OldValue(&withStruct).ExpectMatches(field.ErrorMatcher{}.ByType().ByField().ByDetailSubstring().ByOrigin(), field.ErrorList{
field.Invalid(field.NewPath("structNoModify"), nil, "field cannot be modified once set").WithOrigin("update"),
})
// NonComparable Struct NoModify - uses reflection
// For non-pointer structs, even zero value is considered "set"
// So any change is a modification
old = baseStruct
old.NonComparableStructNoModify = NonComparableStruct{} // zero value
withValue = baseStruct
withValue.NonComparableStructNoModify = NonComparableStruct{
SliceField: []string{"a", "b"},
IntField: 42,
}
// Cannot change from zero value to non-zero (both are "set")
st.Value(&withValue).OldValue(&old).ExpectMatches(field.ErrorMatcher{}.ByType().ByField().ByDetailSubstring().ByOrigin(), field.ErrorList{
field.Invalid(field.NewPath("nonComparableStructNoModify"), nil, "field cannot be modified once set").WithOrigin("update"),
})
// Also cannot modify between two non-zero values
modified = baseStruct
modified.NonComparableStructNoModify = NonComparableStruct{
SliceField: []string{"c", "d"},
IntField: 100,
}
st.Value(&modified).OldValue(&withValue).ExpectMatches(field.ErrorMatcher{}.ByType().ByField().ByDetailSubstring().ByOrigin(), field.ErrorList{
field.Invalid(field.NewPath("nonComparableStructNoModify"), nil, "field cannot be modified once set").WithOrigin("update"),
})
// Can keep the same value
st.Value(&withValue).OldValue(&withValue).ExpectValid()
// Pointer NoSet
withSet := baseStruct
withSet.PointerNoSet = ptr.To("value")
// Cannot set after creation (nil to non-nil)
st.Value(&withSet).OldValue(&baseStruct).ExpectMatches(field.ErrorMatcher{}.ByType().ByField().ByDetailSubstring().ByOrigin(), field.ErrorList{
field.Invalid(field.NewPath("pointerNoSet"), nil, "field cannot be set once created").WithOrigin("update"),
})
// Pointer NoUnset
withPointer := baseStruct
withPointer.PointerNoUnset = ptr.To("value")
// Can set initially
st.Value(&withPointer).OldValue(&baseStruct).ExpectValid()
// Cannot unset (non-nil to nil)
st.Value(&baseStruct).OldValue(&withPointer).ExpectMatches(field.ErrorMatcher{}.ByType().ByField().ByDetailSubstring().ByOrigin(), field.ErrorList{
field.Invalid(field.NewPath("pointerNoUnset"), nil, "field cannot be cleared once set").WithOrigin("update"),
})
// Pointer NoModify
withPointer = baseStruct
withPointer.PointerNoModify = ptr.To("value")
modifiedPointer := baseStruct
modifiedPointer.PointerNoModify = ptr.To("different")
// Can set initially
st.Value(&withPointer).OldValue(&baseStruct).ExpectValid()
// Can unset (NoModify allows set/unset transitions)
st.Value(&baseStruct).OldValue(&withPointer).ExpectValid()
// Cannot modify content
st.Value(&modifiedPointer).OldValue(&withPointer).ExpectMatches(field.ErrorMatcher{}.ByType().ByField().ByDetailSubstring().ByOrigin(), field.ErrorList{
field.Invalid(field.NewPath("pointerNoModify"), nil, "field cannot be modified once set").WithOrigin("update"),
})
// Pointer Fully Restricted
withPointer = baseStruct
withPointer.PointerFullyRestricted = ptr.To("value")
modifiedPointer = baseStruct
modifiedPointer.PointerFullyRestricted = ptr.To("different")
// Cannot set (NoSet)
st.Value(&withPointer).OldValue(&baseStruct).ExpectMatches(field.ErrorMatcher{}.ByType().ByField().ByDetailSubstring().ByOrigin(), field.ErrorList{
field.Invalid(field.NewPath("pointerFullyRestricted"), nil, "field cannot be set once created").WithOrigin("update"),
})
// Cannot unset (NoUnset)
st.Value(&baseStruct).OldValue(&withPointer).ExpectMatches(field.ErrorMatcher{}.ByType().ByField().ByDetailSubstring().ByOrigin(), field.ErrorList{
field.Invalid(field.NewPath("pointerFullyRestricted"), nil, "field cannot be cleared once set").WithOrigin("update"),
})
// Cannot modify (NoModify)
st.Value(&modifiedPointer).OldValue(&withPointer).ExpectMatches(field.ErrorMatcher{}.ByType().ByField().ByDetailSubstring().ByOrigin(), field.ErrorList{
field.Invalid(field.NewPath("pointerFullyRestricted"), nil, "field cannot be modified once set").WithOrigin("update"),
})
// Int Pointer NoModify
withPointer = baseStruct
withPointer.IntPointerNoModify = ptr.To(42)
modifiedPointer = baseStruct
modifiedPointer.IntPointerNoModify = ptr.To(100)
// Can set initially
st.Value(&withPointer).OldValue(&baseStruct).ExpectValid()
// Can unset
st.Value(&baseStruct).OldValue(&withPointer).ExpectValid()
// Cannot modify content
st.Value(&modifiedPointer).OldValue(&withPointer).ExpectMatches(field.ErrorMatcher{}.ByType().ByField().ByDetailSubstring().ByOrigin(), field.ErrorList{
field.Invalid(field.NewPath("intPointerNoModify"), nil, "field cannot be modified once set").WithOrigin("update"),
})
// Bool Pointer NoModify
falseVal := false
trueVal := true
withFalse := baseStruct
withFalse.BoolPointerNoModify = &falseVal
withTrue = baseStruct
withTrue.BoolPointerNoModify = &trueVal
// Can set initially
st.Value(&withFalse).OldValue(&baseStruct).ExpectValid()
// Cannot modify content
st.Value(&withTrue).OldValue(&withFalse).ExpectMatches(field.ErrorMatcher{}.ByType().ByField().ByDetailSubstring().ByOrigin(), field.ErrorList{
field.Invalid(field.NewPath("boolPointerNoModify"), nil, "field cannot be modified once set").WithOrigin("update"),
})
// Struct Pointer NoModify
withPointer = baseStruct
withPointer.StructPointerNoModify = &TestStruct{StringField: "value", IntField: 42}
modifiedPointer = baseStruct
modifiedPointer.StructPointerNoModify = &TestStruct{StringField: "different", IntField: 100}
// Can set initially
st.Value(&withPointer).OldValue(&baseStruct).ExpectValid()
// Can unset
st.Value(&baseStruct).OldValue(&withPointer).ExpectValid()
// Cannot modify content
st.Value(&modifiedPointer).OldValue(&withPointer).ExpectMatches(field.ErrorMatcher{}.ByType().ByField().ByDetailSubstring().ByOrigin(), field.ErrorList{
field.Invalid(field.NewPath("structPointerNoModify"), nil, "field cannot be modified once set").WithOrigin("update"),
})
// Custom Type NoModify
old = baseStruct
withValue = baseStruct
withValue.CustomTypeNoModify = "custom-value"
// Can set initially
st.Value(&withValue).OldValue(&old).ExpectValid()
// Cannot modify
modified = baseStruct
modified.CustomTypeNoModify = "different-value"
st.Value(&modified).OldValue(&withValue).ExpectMatches(field.ErrorMatcher{}.ByType().ByField().ByDetailSubstring().ByOrigin(), field.ErrorList{
field.Invalid(field.NewPath("customTypeNoModify"), nil, "field cannot be modified once set").WithOrigin("update"),
})
// Custom Type NoSet
old = baseStruct
withValue = baseStruct
withValue.CustomTypeNoSet = 42
// Cannot set
st.Value(&withValue).OldValue(&old).ExpectMatches(field.ErrorMatcher{}.ByType().ByField().ByDetailSubstring().ByOrigin(), field.ErrorList{
field.Invalid(field.NewPath("customTypeNoSet"), nil, "field cannot be set once created").WithOrigin("update"),
})
}

View File

@@ -0,0 +1,514 @@
//go:build !ignore_autogenerated
// +build !ignore_autogenerated
/*
Copyright The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
// Code generated by validation-gen. DO NOT EDIT.
package update
import (
context "context"
fmt "fmt"
equality "k8s.io/apimachinery/pkg/api/equality"
operation "k8s.io/apimachinery/pkg/api/operation"
safe "k8s.io/apimachinery/pkg/api/safe"
validate "k8s.io/apimachinery/pkg/api/validate"
field "k8s.io/apimachinery/pkg/util/validation/field"
testscheme "k8s.io/code-generator/cmd/validation-gen/testscheme"
)
func init() { localSchemeBuilder.Register(RegisterValidations) }
// RegisterValidations adds validation functions to the given scheme.
// Public to allow building arbitrary schemes.
func RegisterValidations(scheme *testscheme.Scheme) error {
// type UpdateTestStruct
scheme.AddValidationFunc((*UpdateTestStruct)(nil), func(ctx context.Context, op operation.Operation, obj, oldObj interface{}) field.ErrorList {
switch op.Request.SubresourcePath() {
case "/":
return Validate_UpdateTestStruct(ctx, op, nil /* fldPath */, obj.(*UpdateTestStruct), safe.Cast[*UpdateTestStruct](oldObj))
}
return field.ErrorList{field.InternalError(nil, fmt.Errorf("no validation found for %T, subresource: %v", obj, op.Request.SubresourcePath()))}
})
return nil
}
// Validate_UpdateTestStruct validates an instance of UpdateTestStruct according
// to declarative validation rules in the API schema.
func Validate_UpdateTestStruct(ctx context.Context, op operation.Operation, fldPath *field.Path, obj, oldObj *UpdateTestStruct) (errs field.ErrorList) {
// field UpdateTestStruct.TypeMeta has no validation
// field UpdateTestStruct.StringNoSet
errs = append(errs,
func(fldPath *field.Path, obj, oldObj *string) (errs field.ErrorList) {
// don't revalidate unchanged data
if op.Type == operation.Update && (obj == oldObj || (obj != nil && oldObj != nil && *obj == *oldObj)) {
return nil
}
// call field-attached validations
earlyReturn := false
if e := validate.UpdateValueByCompare(ctx, op, fldPath, obj, oldObj, validate.NoSet); len(e) != 0 {
errs = append(errs, e...)
earlyReturn = true
}
if earlyReturn {
return // do not proceed
}
return
}(fldPath.Child("stringNoSet"), &obj.StringNoSet, safe.Field(oldObj, func(oldObj *UpdateTestStruct) *string { return &oldObj.StringNoSet }))...)
// field UpdateTestStruct.StringNoUnset
errs = append(errs,
func(fldPath *field.Path, obj, oldObj *string) (errs field.ErrorList) {
// don't revalidate unchanged data
if op.Type == operation.Update && (obj == oldObj || (obj != nil && oldObj != nil && *obj == *oldObj)) {
return nil
}
// call field-attached validations
earlyReturn := false
if e := validate.UpdateValueByCompare(ctx, op, fldPath, obj, oldObj, validate.NoUnset); len(e) != 0 {
errs = append(errs, e...)
earlyReturn = true
}
if earlyReturn {
return // do not proceed
}
return
}(fldPath.Child("stringNoUnset"), &obj.StringNoUnset, safe.Field(oldObj, func(oldObj *UpdateTestStruct) *string { return &oldObj.StringNoUnset }))...)
// field UpdateTestStruct.StringNoModify
errs = append(errs,
func(fldPath *field.Path, obj, oldObj *string) (errs field.ErrorList) {
// don't revalidate unchanged data
if op.Type == operation.Update && (obj == oldObj || (obj != nil && oldObj != nil && *obj == *oldObj)) {
return nil
}
// call field-attached validations
earlyReturn := false
if e := validate.UpdateValueByCompare(ctx, op, fldPath, obj, oldObj, validate.NoModify); len(e) != 0 {
errs = append(errs, e...)
earlyReturn = true
}
if earlyReturn {
return // do not proceed
}
return
}(fldPath.Child("stringNoModify"), &obj.StringNoModify, safe.Field(oldObj, func(oldObj *UpdateTestStruct) *string { return &oldObj.StringNoModify }))...)
// field UpdateTestStruct.StringFullyRestricted
errs = append(errs,
func(fldPath *field.Path, obj, oldObj *string) (errs field.ErrorList) {
// don't revalidate unchanged data
if op.Type == operation.Update && (obj == oldObj || (obj != nil && oldObj != nil && *obj == *oldObj)) {
return nil
}
// call field-attached validations
earlyReturn := false
if e := validate.UpdateValueByCompare(ctx, op, fldPath, obj, oldObj, validate.NoSet, validate.NoUnset, validate.NoModify); len(e) != 0 {
errs = append(errs, e...)
earlyReturn = true
}
if earlyReturn {
return // do not proceed
}
return
}(fldPath.Child("stringFullyRestricted"), &obj.StringFullyRestricted, safe.Field(oldObj, func(oldObj *UpdateTestStruct) *string { return &oldObj.StringFullyRestricted }))...)
// field UpdateTestStruct.StringSetOnce
errs = append(errs,
func(fldPath *field.Path, obj, oldObj *string) (errs field.ErrorList) {
// don't revalidate unchanged data
if op.Type == operation.Update && (obj == oldObj || (obj != nil && oldObj != nil && *obj == *oldObj)) {
return nil
}
// call field-attached validations
earlyReturn := false
if e := validate.UpdateValueByCompare(ctx, op, fldPath, obj, oldObj, validate.NoUnset, validate.NoModify); len(e) != 0 {
errs = append(errs, e...)
earlyReturn = true
}
if earlyReturn {
return // do not proceed
}
return
}(fldPath.Child("stringSetOnce"), &obj.StringSetOnce, safe.Field(oldObj, func(oldObj *UpdateTestStruct) *string { return &oldObj.StringSetOnce }))...)
// field UpdateTestStruct.IntNoModify
errs = append(errs,
func(fldPath *field.Path, obj, oldObj *int) (errs field.ErrorList) {
// don't revalidate unchanged data
if op.Type == operation.Update && (obj == oldObj || (obj != nil && oldObj != nil && *obj == *oldObj)) {
return nil
}
// call field-attached validations
earlyReturn := false
if e := validate.UpdateValueByCompare(ctx, op, fldPath, obj, oldObj, validate.NoModify); len(e) != 0 {
errs = append(errs, e...)
earlyReturn = true
}
if earlyReturn {
return // do not proceed
}
return
}(fldPath.Child("intNoModify"), &obj.IntNoModify, safe.Field(oldObj, func(oldObj *UpdateTestStruct) *int { return &oldObj.IntNoModify }))...)
// field UpdateTestStruct.Int32NoModify
errs = append(errs,
func(fldPath *field.Path, obj, oldObj *int32) (errs field.ErrorList) {
// don't revalidate unchanged data
if op.Type == operation.Update && (obj == oldObj || (obj != nil && oldObj != nil && *obj == *oldObj)) {
return nil
}
// call field-attached validations
earlyReturn := false
if e := validate.UpdateValueByCompare(ctx, op, fldPath, obj, oldObj, validate.NoModify); len(e) != 0 {
errs = append(errs, e...)
earlyReturn = true
}
if earlyReturn {
return // do not proceed
}
return
}(fldPath.Child("int32NoModify"), &obj.Int32NoModify, safe.Field(oldObj, func(oldObj *UpdateTestStruct) *int32 { return &oldObj.Int32NoModify }))...)
// field UpdateTestStruct.Int64NoModify
errs = append(errs,
func(fldPath *field.Path, obj, oldObj *int64) (errs field.ErrorList) {
// don't revalidate unchanged data
if op.Type == operation.Update && (obj == oldObj || (obj != nil && oldObj != nil && *obj == *oldObj)) {
return nil
}
// call field-attached validations
earlyReturn := false
if e := validate.UpdateValueByCompare(ctx, op, fldPath, obj, oldObj, validate.NoModify); len(e) != 0 {
errs = append(errs, e...)
earlyReturn = true
}
if earlyReturn {
return // do not proceed
}
return
}(fldPath.Child("int64NoModify"), &obj.Int64NoModify, safe.Field(oldObj, func(oldObj *UpdateTestStruct) *int64 { return &oldObj.Int64NoModify }))...)
// field UpdateTestStruct.UintNoModify
errs = append(errs,
func(fldPath *field.Path, obj, oldObj *uint) (errs field.ErrorList) {
// don't revalidate unchanged data
if op.Type == operation.Update && (obj == oldObj || (obj != nil && oldObj != nil && *obj == *oldObj)) {
return nil
}
// call field-attached validations
earlyReturn := false
if e := validate.UpdateValueByCompare(ctx, op, fldPath, obj, oldObj, validate.NoModify); len(e) != 0 {
errs = append(errs, e...)
earlyReturn = true
}
if earlyReturn {
return // do not proceed
}
return
}(fldPath.Child("uintNoModify"), &obj.UintNoModify, safe.Field(oldObj, func(oldObj *UpdateTestStruct) *uint { return &oldObj.UintNoModify }))...)
// field UpdateTestStruct.BoolNoModify
errs = append(errs,
func(fldPath *field.Path, obj, oldObj *bool) (errs field.ErrorList) {
// don't revalidate unchanged data
if op.Type == operation.Update && (obj == oldObj || (obj != nil && oldObj != nil && *obj == *oldObj)) {
return nil
}
// call field-attached validations
earlyReturn := false
if e := validate.UpdateValueByCompare(ctx, op, fldPath, obj, oldObj, validate.NoModify); len(e) != 0 {
errs = append(errs, e...)
earlyReturn = true
}
if earlyReturn {
return // do not proceed
}
return
}(fldPath.Child("boolNoModify"), &obj.BoolNoModify, safe.Field(oldObj, func(oldObj *UpdateTestStruct) *bool { return &oldObj.BoolNoModify }))...)
// field UpdateTestStruct.Float32NoModify
errs = append(errs,
func(fldPath *field.Path, obj, oldObj *float32) (errs field.ErrorList) {
// don't revalidate unchanged data
if op.Type == operation.Update && (obj == oldObj || (obj != nil && oldObj != nil && *obj == *oldObj)) {
return nil
}
// call field-attached validations
earlyReturn := false
if e := validate.UpdateValueByCompare(ctx, op, fldPath, obj, oldObj, validate.NoModify); len(e) != 0 {
errs = append(errs, e...)
earlyReturn = true
}
if earlyReturn {
return // do not proceed
}
return
}(fldPath.Child("float32NoModify"), &obj.Float32NoModify, safe.Field(oldObj, func(oldObj *UpdateTestStruct) *float32 { return &oldObj.Float32NoModify }))...)
// field UpdateTestStruct.Float64NoModify
errs = append(errs,
func(fldPath *field.Path, obj, oldObj *float64) (errs field.ErrorList) {
// don't revalidate unchanged data
if op.Type == operation.Update && (obj == oldObj || (obj != nil && oldObj != nil && *obj == *oldObj)) {
return nil
}
// call field-attached validations
earlyReturn := false
if e := validate.UpdateValueByCompare(ctx, op, fldPath, obj, oldObj, validate.NoModify); len(e) != 0 {
errs = append(errs, e...)
earlyReturn = true
}
if earlyReturn {
return // do not proceed
}
return
}(fldPath.Child("float64NoModify"), &obj.Float64NoModify, safe.Field(oldObj, func(oldObj *UpdateTestStruct) *float64 { return &oldObj.Float64NoModify }))...)
// field UpdateTestStruct.ByteNoModify
errs = append(errs,
func(fldPath *field.Path, obj, oldObj *byte) (errs field.ErrorList) {
// don't revalidate unchanged data
if op.Type == operation.Update && (obj == oldObj || (obj != nil && oldObj != nil && *obj == *oldObj)) {
return nil
}
// call field-attached validations
earlyReturn := false
if e := validate.UpdateValueByCompare(ctx, op, fldPath, obj, oldObj, validate.NoModify); len(e) != 0 {
errs = append(errs, e...)
earlyReturn = true
}
if earlyReturn {
return // do not proceed
}
return
}(fldPath.Child("byteNoModify"), &obj.ByteNoModify, safe.Field(oldObj, func(oldObj *UpdateTestStruct) *byte { return &oldObj.ByteNoModify }))...)
// field UpdateTestStruct.StructNoModify
errs = append(errs,
func(fldPath *field.Path, obj, oldObj *TestStruct) (errs field.ErrorList) {
// don't revalidate unchanged data
if op.Type == operation.Update && (obj == oldObj || (obj != nil && oldObj != nil && *obj == *oldObj)) {
return nil
}
// call field-attached validations
earlyReturn := false
if e := validate.UpdateStruct(ctx, op, fldPath, obj, oldObj, validate.NoModify); len(e) != 0 {
errs = append(errs, e...)
earlyReturn = true
}
if earlyReturn {
return // do not proceed
}
return
}(fldPath.Child("structNoModify"), &obj.StructNoModify, safe.Field(oldObj, func(oldObj *UpdateTestStruct) *TestStruct { return &oldObj.StructNoModify }))...)
// field UpdateTestStruct.NonComparableStructNoModify
errs = append(errs,
func(fldPath *field.Path, obj, oldObj *NonComparableStruct) (errs field.ErrorList) {
// don't revalidate unchanged data
if op.Type == operation.Update && equality.Semantic.DeepEqual(obj, oldObj) {
return nil
}
// call field-attached validations
earlyReturn := false
if e := validate.UpdateStruct(ctx, op, fldPath, obj, oldObj, validate.NoModify); len(e) != 0 {
errs = append(errs, e...)
earlyReturn = true
}
if earlyReturn {
return // do not proceed
}
return
}(fldPath.Child("nonComparableStructNoModify"), &obj.NonComparableStructNoModify, safe.Field(oldObj, func(oldObj *UpdateTestStruct) *NonComparableStruct { return &oldObj.NonComparableStructNoModify }))...)
// field UpdateTestStruct.PointerNoSet
errs = append(errs,
func(fldPath *field.Path, obj, oldObj *string) (errs field.ErrorList) {
// don't revalidate unchanged data
if op.Type == operation.Update && (obj == oldObj || (obj != nil && oldObj != nil && *obj == *oldObj)) {
return nil
}
// call field-attached validations
earlyReturn := false
if e := validate.UpdatePointer(ctx, op, fldPath, obj, oldObj, validate.NoSet); len(e) != 0 {
errs = append(errs, e...)
earlyReturn = true
}
if earlyReturn {
return // do not proceed
}
return
}(fldPath.Child("pointerNoSet"), obj.PointerNoSet, safe.Field(oldObj, func(oldObj *UpdateTestStruct) *string { return oldObj.PointerNoSet }))...)
// field UpdateTestStruct.PointerNoUnset
errs = append(errs,
func(fldPath *field.Path, obj, oldObj *string) (errs field.ErrorList) {
// don't revalidate unchanged data
if op.Type == operation.Update && (obj == oldObj || (obj != nil && oldObj != nil && *obj == *oldObj)) {
return nil
}
// call field-attached validations
earlyReturn := false
if e := validate.UpdatePointer(ctx, op, fldPath, obj, oldObj, validate.NoUnset); len(e) != 0 {
errs = append(errs, e...)
earlyReturn = true
}
if earlyReturn {
return // do not proceed
}
return
}(fldPath.Child("pointerNoUnset"), obj.PointerNoUnset, safe.Field(oldObj, func(oldObj *UpdateTestStruct) *string { return oldObj.PointerNoUnset }))...)
// field UpdateTestStruct.PointerNoModify
errs = append(errs,
func(fldPath *field.Path, obj, oldObj *string) (errs field.ErrorList) {
// don't revalidate unchanged data
if op.Type == operation.Update && (obj == oldObj || (obj != nil && oldObj != nil && *obj == *oldObj)) {
return nil
}
// call field-attached validations
earlyReturn := false
if e := validate.UpdatePointer(ctx, op, fldPath, obj, oldObj, validate.NoModify); len(e) != 0 {
errs = append(errs, e...)
earlyReturn = true
}
if earlyReturn {
return // do not proceed
}
return
}(fldPath.Child("pointerNoModify"), obj.PointerNoModify, safe.Field(oldObj, func(oldObj *UpdateTestStruct) *string { return oldObj.PointerNoModify }))...)
// field UpdateTestStruct.PointerFullyRestricted
errs = append(errs,
func(fldPath *field.Path, obj, oldObj *string) (errs field.ErrorList) {
// don't revalidate unchanged data
if op.Type == operation.Update && (obj == oldObj || (obj != nil && oldObj != nil && *obj == *oldObj)) {
return nil
}
// call field-attached validations
earlyReturn := false
if e := validate.UpdatePointer(ctx, op, fldPath, obj, oldObj, validate.NoSet, validate.NoUnset, validate.NoModify); len(e) != 0 {
errs = append(errs, e...)
earlyReturn = true
}
if earlyReturn {
return // do not proceed
}
return
}(fldPath.Child("pointerFullyRestricted"), obj.PointerFullyRestricted, safe.Field(oldObj, func(oldObj *UpdateTestStruct) *string { return oldObj.PointerFullyRestricted }))...)
// field UpdateTestStruct.IntPointerNoModify
errs = append(errs,
func(fldPath *field.Path, obj, oldObj *int) (errs field.ErrorList) {
// don't revalidate unchanged data
if op.Type == operation.Update && (obj == oldObj || (obj != nil && oldObj != nil && *obj == *oldObj)) {
return nil
}
// call field-attached validations
earlyReturn := false
if e := validate.UpdatePointer(ctx, op, fldPath, obj, oldObj, validate.NoModify); len(e) != 0 {
errs = append(errs, e...)
earlyReturn = true
}
if earlyReturn {
return // do not proceed
}
return
}(fldPath.Child("intPointerNoModify"), obj.IntPointerNoModify, safe.Field(oldObj, func(oldObj *UpdateTestStruct) *int { return oldObj.IntPointerNoModify }))...)
// field UpdateTestStruct.BoolPointerNoModify
errs = append(errs,
func(fldPath *field.Path, obj, oldObj *bool) (errs field.ErrorList) {
// don't revalidate unchanged data
if op.Type == operation.Update && (obj == oldObj || (obj != nil && oldObj != nil && *obj == *oldObj)) {
return nil
}
// call field-attached validations
earlyReturn := false
if e := validate.UpdatePointer(ctx, op, fldPath, obj, oldObj, validate.NoModify); len(e) != 0 {
errs = append(errs, e...)
earlyReturn = true
}
if earlyReturn {
return // do not proceed
}
return
}(fldPath.Child("boolPointerNoModify"), obj.BoolPointerNoModify, safe.Field(oldObj, func(oldObj *UpdateTestStruct) *bool { return oldObj.BoolPointerNoModify }))...)
// field UpdateTestStruct.StructPointerNoModify
errs = append(errs,
func(fldPath *field.Path, obj, oldObj *TestStruct) (errs field.ErrorList) {
// don't revalidate unchanged data
if op.Type == operation.Update && (obj == oldObj || (obj != nil && oldObj != nil && *obj == *oldObj)) {
return nil
}
// call field-attached validations
earlyReturn := false
if e := validate.UpdatePointer(ctx, op, fldPath, obj, oldObj, validate.NoModify); len(e) != 0 {
errs = append(errs, e...)
earlyReturn = true
}
if earlyReturn {
return // do not proceed
}
return
}(fldPath.Child("structPointerNoModify"), obj.StructPointerNoModify, safe.Field(oldObj, func(oldObj *UpdateTestStruct) *TestStruct { return oldObj.StructPointerNoModify }))...)
// field UpdateTestStruct.CustomTypeNoModify
errs = append(errs,
func(fldPath *field.Path, obj, oldObj *CustomString) (errs field.ErrorList) {
// don't revalidate unchanged data
if op.Type == operation.Update && (obj == oldObj || (obj != nil && oldObj != nil && *obj == *oldObj)) {
return nil
}
// call field-attached validations
earlyReturn := false
if e := validate.UpdateValueByCompare(ctx, op, fldPath, obj, oldObj, validate.NoModify); len(e) != 0 {
errs = append(errs, e...)
earlyReturn = true
}
if earlyReturn {
return // do not proceed
}
return
}(fldPath.Child("customTypeNoModify"), &obj.CustomTypeNoModify, safe.Field(oldObj, func(oldObj *UpdateTestStruct) *CustomString { return &oldObj.CustomTypeNoModify }))...)
// field UpdateTestStruct.CustomTypeNoSet
errs = append(errs,
func(fldPath *field.Path, obj, oldObj *CustomInt) (errs field.ErrorList) {
// don't revalidate unchanged data
if op.Type == operation.Update && (obj == oldObj || (obj != nil && oldObj != nil && *obj == *oldObj)) {
return nil
}
// call field-attached validations
earlyReturn := false
if e := validate.UpdateValueByCompare(ctx, op, fldPath, obj, oldObj, validate.NoSet); len(e) != 0 {
errs = append(errs, e...)
earlyReturn = true
}
if earlyReturn {
return // do not proceed
}
return
}(fldPath.Child("customTypeNoSet"), &obj.CustomTypeNoSet, safe.Field(oldObj, func(oldObj *UpdateTestStruct) *CustomInt { return &oldObj.CustomTypeNoSet }))...)
return errs
}

View File

@@ -0,0 +1,225 @@
/*
Copyright 2025 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package validators
import (
"fmt"
"sort"
"k8s.io/apimachinery/pkg/api/validate"
"k8s.io/apimachinery/pkg/util/sets"
"k8s.io/code-generator/cmd/validation-gen/util"
"k8s.io/gengo/v2/codetags"
"k8s.io/gengo/v2/types"
)
const (
updateTagName = "k8s:update"
)
func init() {
shared := map[string]sets.Set[validate.UpdateConstraint]{}
RegisterFieldValidator(updateFieldValidator{byFieldPath: shared})
RegisterTagValidator(updateTagCollector{byFieldPath: shared})
}
// updateTagCollector collects +k8s:update tags
type updateTagCollector struct {
byFieldPath map[string]sets.Set[validate.UpdateConstraint]
}
func (updateTagCollector) Init(_ Config) {}
func (updateTagCollector) TagName() string {
return updateTagName
}
var updateTagValidScopes = sets.New(ScopeField)
func (updateTagCollector) ValidScopes() sets.Set[Scope] {
return updateTagValidScopes
}
func (utc updateTagCollector) GetValidations(context Context, tag codetags.Tag) (Validations, error) {
// Parse constraint from this tag
var constraint validate.UpdateConstraint
switch tag.Value {
case "NoSet":
constraint = validate.NoSet
case "NoUnset":
constraint = validate.NoUnset
case "NoModify":
constraint = validate.NoModify
default:
return Validations{}, fmt.Errorf("unknown +k8s:update constraint: %s", tag.Value)
}
// Initialize set if doesn't exist
fieldPath := context.Path.String()
if utc.byFieldPath[fieldPath] == nil {
utc.byFieldPath[fieldPath] = sets.New[validate.UpdateConstraint]()
}
// Add this constraint to the set for this field
utc.byFieldPath[fieldPath].Insert(constraint)
if err := utc.validateConstraintsForType(context, utc.byFieldPath[fieldPath].UnsortedList()); err != nil {
return Validations{}, err
}
// Don't generate validations here, just collect
return Validations{}, nil
}
func (utc updateTagCollector) validateConstraintsForType(context Context, constraints []validate.UpdateConstraint) error {
t := util.NonPointer(util.NativeType(context.Type))
isCompound := t.Kind == types.Slice || t.Kind == types.Map
isPointer := context.Type.Kind == types.Pointer
isStruct := t.Kind == types.Struct
if isCompound {
for _, constraint := range constraints {
return fmt.Errorf("+k8s:update=%s is currently not supported on list or map fields", constraintName(constraint))
}
}
// For non-pointer struct fields, only NoModify is applicable
if isStruct && !isPointer {
for _, constraint := range constraints {
if constraint == validate.NoSet || constraint == validate.NoUnset {
return fmt.Errorf("+k8s:update=%s cannot be used on non-pointer struct fields (they cannot be unset)", constraintName(constraint))
}
}
}
return nil
}
func constraintName(c validate.UpdateConstraint) string {
switch c {
case validate.NoSet:
return "NoSet"
case validate.NoUnset:
return "NoUnset"
case validate.NoModify:
return "NoModify"
default:
return fmt.Sprintf("Unknown(%d)", c)
}
}
func (utc updateTagCollector) Docs() TagDoc {
return TagDoc{
Tag: utc.TagName(),
Scopes: utc.ValidScopes().UnsortedList(),
PayloadsType: codetags.ValueTypeString,
Description: "Provides constraints on the allowed update operations of a field. " +
"Currently supports non-list and non-map fields only. " +
"Constraints: NoSet (prevents unset->set transitions), NoUnset (prevents set->unset transitions), " +
"NoModify (prevents value changes but allows set/unset transitions). " +
"Multiple constraints can be specified using multiple tags. " +
"For non-pointer structs, NoSet and NoUnset have no effect as these fields cannot be unset. " +
"Future support planned for lists/maps with NoAddItem and NoRemoveItem constraints. " +
"Examples: +k8s:update=NoModify +k8s:update=NoUnset for set-once fields; " +
"+k8s:update=NoSet for fields that must be set at creation or never.",
}
}
// updateFieldValidator processes all collected update tags and generates validations
type updateFieldValidator struct {
byFieldPath map[string]sets.Set[validate.UpdateConstraint]
}
func (updateFieldValidator) Init(_ Config) {}
func (updateFieldValidator) Name() string {
return "updateFieldValidator"
}
var (
updateValueValidator = types.Name{Package: libValidationPkg, Name: "UpdateValueByCompare"}
updatePointerValidator = types.Name{Package: libValidationPkg, Name: "UpdatePointer"}
updateValueByReflectValidator = types.Name{Package: libValidationPkg, Name: "UpdateValueByReflect"}
updateStructValidator = types.Name{Package: libValidationPkg, Name: "UpdateStruct"}
// Constraint constants that will be used as arguments
noSetConstraint = types.Name{Package: libValidationPkg, Name: "NoSet"}
noUnsetConstraint = types.Name{Package: libValidationPkg, Name: "NoUnset"}
noModifyConstraint = types.Name{Package: libValidationPkg, Name: "NoModify"}
)
func (ufv updateFieldValidator) GetValidations(context Context) (Validations, error) {
constraintSet, ok := ufv.byFieldPath[context.Path.String()]
if !ok || constraintSet.Len() == 0 {
return Validations{}, nil
}
constraints := constraintSet.UnsortedList()
t := util.NonPointer(util.NativeType(context.Type))
if t.Kind == types.Slice || t.Kind == types.Map {
// TODO: add support for list and map fields
return Validations{}, fmt.Errorf("update constraints are currently not supported on list or map fields")
}
return ufv.generateValidation(context, constraints)
}
func (ufv updateFieldValidator) generateValidation(context Context, constraints []validate.UpdateConstraint) (Validations, error) {
var result Validations
// Determine the appropriate validator function based on field type
t := util.NonPointer(util.NativeType(context.Type))
isPointer := context.Type.Kind == types.Pointer
isStruct := t.Kind == types.Struct
isComparable := util.IsDirectComparable(t)
var validatorFunc types.Name
if isPointer {
validatorFunc = updatePointerValidator
} else if isStruct {
validatorFunc = updateStructValidator
} else if isComparable {
validatorFunc = updateValueValidator
} else {
validatorFunc = updateValueByReflectValidator
}
// Sort constraints to ensure deterministic order
sort.Slice(constraints, func(i, j int) bool {
return constraints[i] < constraints[j]
})
// Build the constraint arguments in deterministic order
var constraintArgs []any
for _, constraint := range constraints {
switch constraint {
case validate.NoSet:
constraintArgs = append(constraintArgs, Identifier(noSetConstraint))
case validate.NoUnset:
constraintArgs = append(constraintArgs, Identifier(noUnsetConstraint))
case validate.NoModify:
constraintArgs = append(constraintArgs, Identifier(noModifyConstraint))
}
}
// Use ShortCircuit flag so these run in the same group as +k8s:optional
fn := Function(updateTagName, ShortCircuit, validatorFunc, constraintArgs...)
result.AddFunction(fn)
return result, nil
}