mirror of
https://github.com/k3s-io/kubernetes.git
synced 2026-07-17 20:00:07 +00:00
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:
committed by
yongruilin
parent
4d61ba787d
commit
331ea38769
160
staging/src/k8s.io/apimachinery/pkg/api/validate/update.go
Normal file
160
staging/src/k8s.io/apimachinery/pkg/api/validate/update.go
Normal 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
|
||||
}
|
||||
430
staging/src/k8s.io/apimachinery/pkg/api/validate/update_test.go
Normal file
430
staging/src/k8s.io/apimachinery/pkg/api/validate/update_test.go
Normal 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)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
@@ -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"),
|
||||
})
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
Reference in New Issue
Block a user