diff --git a/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/optional/nonzero_defaults/doc.go b/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/optional/nonzero_defaults/doc.go new file mode 100644 index 00000000000..d5206da9733 --- /dev/null +++ b/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/optional/nonzero_defaults/doc.go @@ -0,0 +1,53 @@ +/* +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. +package nonzerodefaults + +import "k8s.io/code-generator/cmd/validation-gen/testscheme" + +var localSchemeBuilder = testscheme.New() + +type Struct struct { + TypeMeta int + + // +k8s:optional + // +default="foobar" + StringField string `json:"stringField"` + + // +k8s:optional + // +default="foobar" + StringPtrField *string `json:"stringPtrField"` + + // +k8s:optional + // +default=123 + IntField int `json:"intField"` + + // +k8s:optional + // +default=123 + IntPtrField *int `json:"intPtrField"` + + // +k8s:optional + // +default=true + BoolField bool `json:"boolField"` + + // +k8s:optional + // +default=true + BoolPtrField *bool `json:"boolPtrField"` +} diff --git a/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/optional/nonzero_defaults/doc_test.go b/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/optional/nonzero_defaults/doc_test.go new file mode 100644 index 00000000000..18841fe4f7f --- /dev/null +++ b/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/optional/nonzero_defaults/doc_test.go @@ -0,0 +1,47 @@ +/* +Copyright 2024 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 nonzerodefaults + +import ( + "testing" + + "k8s.io/utils/ptr" +) + +func Test(t *testing.T) { + st := localSchemeBuilder.Test(t) + + st.Value(&Struct{ + // All zero-values. + }).ExpectRegexpsByPath(map[string][]string{ + "stringField": {"Required value"}, + "stringPtrField": {"Required value"}, + "intField": {"Required value"}, + "intPtrField": {"Required value"}, + "boolField": {"Required value"}, + "boolPtrField": {"Required value"}, + }) + + st.Value(&Struct{ + StringField: "abc", + StringPtrField: ptr.To(""), + IntField: 123, + IntPtrField: ptr.To(0), + BoolField: true, + BoolPtrField: ptr.To(false), + }).ExpectValid() +} diff --git a/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/optional/nonzero_defaults/zz_generated.validations.go b/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/optional/nonzero_defaults/zz_generated.validations.go new file mode 100644 index 00000000000..c710c8deb71 --- /dev/null +++ b/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/optional/nonzero_defaults/zz_generated.validations.go @@ -0,0 +1,119 @@ +//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 nonzerodefaults + +import ( + context "context" + fmt "fmt" + + 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 { + scheme.AddValidationFunc((*Struct)(nil), func(ctx context.Context, op operation.Operation, obj, oldObj interface{}, subresources ...string) field.ErrorList { + if len(subresources) == 0 { + return Validate_Struct(ctx, op, nil /* fldPath */, obj.(*Struct), safe.Cast[*Struct](oldObj)) + } + return field.ErrorList{field.InternalError(nil, fmt.Errorf("no validation found for %T, subresources: %v", obj, subresources))} + }) + return nil +} + +func Validate_Struct(ctx context.Context, op operation.Operation, fldPath *field.Path, obj, oldObj *Struct) (errs field.ErrorList) { + // field Struct.TypeMeta has no validation + + // field Struct.StringField + errs = append(errs, + func(fldPath *field.Path, obj, oldObj *string) (errs field.ErrorList) { + // optional fields with default values are effectively required + if e := validate.RequiredValue(ctx, op, fldPath, obj, oldObj); len(e) != 0 { + errs = append(errs, e...) + return // do not proceed + } + return + }(fldPath.Child("stringField"), &obj.StringField, safe.Field(oldObj, func(oldObj *Struct) *string { return &oldObj.StringField }))...) + + // field Struct.StringPtrField + errs = append(errs, + func(fldPath *field.Path, obj, oldObj *string) (errs field.ErrorList) { + // optional fields with default values are effectively required + if e := validate.RequiredPointer(ctx, op, fldPath, obj, oldObj); len(e) != 0 { + errs = append(errs, e...) + return // do not proceed + } + return + }(fldPath.Child("stringPtrField"), obj.StringPtrField, safe.Field(oldObj, func(oldObj *Struct) *string { return oldObj.StringPtrField }))...) + + // field Struct.IntField + errs = append(errs, + func(fldPath *field.Path, obj, oldObj *int) (errs field.ErrorList) { + // optional fields with default values are effectively required + if e := validate.RequiredValue(ctx, op, fldPath, obj, oldObj); len(e) != 0 { + errs = append(errs, e...) + return // do not proceed + } + return + }(fldPath.Child("intField"), &obj.IntField, safe.Field(oldObj, func(oldObj *Struct) *int { return &oldObj.IntField }))...) + + // field Struct.IntPtrField + errs = append(errs, + func(fldPath *field.Path, obj, oldObj *int) (errs field.ErrorList) { + // optional fields with default values are effectively required + if e := validate.RequiredPointer(ctx, op, fldPath, obj, oldObj); len(e) != 0 { + errs = append(errs, e...) + return // do not proceed + } + return + }(fldPath.Child("intPtrField"), obj.IntPtrField, safe.Field(oldObj, func(oldObj *Struct) *int { return oldObj.IntPtrField }))...) + + // field Struct.BoolField + errs = append(errs, + func(fldPath *field.Path, obj, oldObj *bool) (errs field.ErrorList) { + // optional fields with default values are effectively required + if e := validate.RequiredValue(ctx, op, fldPath, obj, oldObj); len(e) != 0 { + errs = append(errs, e...) + return // do not proceed + } + return + }(fldPath.Child("boolField"), &obj.BoolField, safe.Field(oldObj, func(oldObj *Struct) *bool { return &oldObj.BoolField }))...) + + // field Struct.BoolPtrField + errs = append(errs, + func(fldPath *field.Path, obj, oldObj *bool) (errs field.ErrorList) { + // optional fields with default values are effectively required + if e := validate.RequiredPointer(ctx, op, fldPath, obj, oldObj); len(e) != 0 { + errs = append(errs, e...) + return // do not proceed + } + return + }(fldPath.Child("boolPtrField"), obj.BoolPtrField, safe.Field(oldObj, func(oldObj *Struct) *bool { return oldObj.BoolPtrField }))...) + + return errs +} diff --git a/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/optional/zero_defaults/doc.go b/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/optional/zero_defaults/doc.go new file mode 100644 index 00000000000..71d0a5e7c1d --- /dev/null +++ b/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/optional/zero_defaults/doc.go @@ -0,0 +1,53 @@ +/* +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. +package zerodefaults + +import "k8s.io/code-generator/cmd/validation-gen/testscheme" + +var localSchemeBuilder = testscheme.New() + +type Struct struct { + TypeMeta int + + // +k8s:optional + // +default="" + StringField string `json:"stringField"` + + // +k8s:optional + // +default="" + StringPtrField *string `json:"stringPtrField"` + + // +k8s:optional + // +default=0 + IntField int `json:"intField"` + + // +k8s:optional + // +default=0 + IntPtrField *int `json:"intPtrField"` + + // +k8s:optional + // +default=false + BoolField bool `json:"boolField"` + + // +k8s:optional + // +default=false + BoolPtrField *bool `json:"boolPtrField"` +} diff --git a/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/optional/zero_defaults/doc_test.go b/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/optional/zero_defaults/doc_test.go new file mode 100644 index 00000000000..1a09a9418e0 --- /dev/null +++ b/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/optional/zero_defaults/doc_test.go @@ -0,0 +1,47 @@ +/* +Copyright 2024 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 zerodefaults + +import ( + "testing" + + "k8s.io/utils/ptr" +) + +func Test(t *testing.T) { + st := localSchemeBuilder.Test(t) + + st.Value(&Struct{ + // All zero-values. + }).ExpectRegexpsByPath(map[string][]string{ + // "stringField": optional value fields with zero defaults are just docs + // "intField": optional value fields with zero defaults are just docs + // "boolField": optional value fields with zero defaults are just docs + "stringPtrField": {"Required value"}, + "intPtrField": {"Required value"}, + "boolPtrField": {"Required value"}, + }) + + st.Value(&Struct{ + StringField: "abc", + StringPtrField: ptr.To(""), + IntField: 123, + IntPtrField: ptr.To(0), + BoolField: true, + BoolPtrField: ptr.To(false), + }).ExpectValid() +} diff --git a/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/optional/zero_defaults/zz_generated.validations.go b/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/optional/zero_defaults/zz_generated.validations.go new file mode 100644 index 00000000000..54dbc72560c --- /dev/null +++ b/staging/src/k8s.io/code-generator/cmd/validation-gen/output_tests/tags/optional/zero_defaults/zz_generated.validations.go @@ -0,0 +1,107 @@ +//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 zerodefaults + +import ( + context "context" + fmt "fmt" + + 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 { + scheme.AddValidationFunc((*Struct)(nil), func(ctx context.Context, op operation.Operation, obj, oldObj interface{}, subresources ...string) field.ErrorList { + if len(subresources) == 0 { + return Validate_Struct(ctx, op, nil /* fldPath */, obj.(*Struct), safe.Cast[*Struct](oldObj)) + } + return field.ErrorList{field.InternalError(nil, fmt.Errorf("no validation found for %T, subresources: %v", obj, subresources))} + }) + return nil +} + +func Validate_Struct(ctx context.Context, op operation.Operation, fldPath *field.Path, obj, oldObj *Struct) (errs field.ErrorList) { + // field Struct.TypeMeta has no validation + + // field Struct.StringField + errs = append(errs, + func(fldPath *field.Path, obj, oldObj *string) (errs field.ErrorList) { + // optional value-type fields with zero-value defaults are purely documentation + return + }(fldPath.Child("stringField"), &obj.StringField, safe.Field(oldObj, func(oldObj *Struct) *string { return &oldObj.StringField }))...) + + // field Struct.StringPtrField + errs = append(errs, + func(fldPath *field.Path, obj, oldObj *string) (errs field.ErrorList) { + // optional fields with default values are effectively required + if e := validate.RequiredPointer(ctx, op, fldPath, obj, oldObj); len(e) != 0 { + errs = append(errs, e...) + return // do not proceed + } + return + }(fldPath.Child("stringPtrField"), obj.StringPtrField, safe.Field(oldObj, func(oldObj *Struct) *string { return oldObj.StringPtrField }))...) + + // field Struct.IntField + errs = append(errs, + func(fldPath *field.Path, obj, oldObj *int) (errs field.ErrorList) { + // optional value-type fields with zero-value defaults are purely documentation + return + }(fldPath.Child("intField"), &obj.IntField, safe.Field(oldObj, func(oldObj *Struct) *int { return &oldObj.IntField }))...) + + // field Struct.IntPtrField + errs = append(errs, + func(fldPath *field.Path, obj, oldObj *int) (errs field.ErrorList) { + // optional fields with default values are effectively required + if e := validate.RequiredPointer(ctx, op, fldPath, obj, oldObj); len(e) != 0 { + errs = append(errs, e...) + return // do not proceed + } + return + }(fldPath.Child("intPtrField"), obj.IntPtrField, safe.Field(oldObj, func(oldObj *Struct) *int { return oldObj.IntPtrField }))...) + + // field Struct.BoolField + errs = append(errs, + func(fldPath *field.Path, obj, oldObj *bool) (errs field.ErrorList) { + // optional value-type fields with zero-value defaults are purely documentation + return + }(fldPath.Child("boolField"), &obj.BoolField, safe.Field(oldObj, func(oldObj *Struct) *bool { return &oldObj.BoolField }))...) + + // field Struct.BoolPtrField + errs = append(errs, + func(fldPath *field.Path, obj, oldObj *bool) (errs field.ErrorList) { + // optional fields with default values are effectively required + if e := validate.RequiredPointer(ctx, op, fldPath, obj, oldObj); len(e) != 0 { + errs = append(errs, e...) + return // do not proceed + } + return + }(fldPath.Child("boolPtrField"), obj.BoolPtrField, safe.Field(oldObj, func(oldObj *Struct) *bool { return oldObj.BoolPtrField }))...) + + return errs +} diff --git a/staging/src/k8s.io/code-generator/cmd/validation-gen/validators/required.go b/staging/src/k8s.io/code-generator/cmd/validation-gen/validators/required.go index 866960c0901..54d7fd44a05 100644 --- a/staging/src/k8s.io/code-generator/cmd/validation-gen/validators/required.go +++ b/staging/src/k8s.io/code-generator/cmd/validation-gen/validators/required.go @@ -17,16 +17,21 @@ limitations under the License. package validators import ( + "encoding/json" "fmt" + "reflect" + + "k8s.io/gengo/v2" + "k8s.io/gengo/v2/types" "k8s.io/apimachinery/pkg/util/sets" - "k8s.io/gengo/v2/types" ) const ( requiredTagName = "k8s:required" optionalTagName = "k8s:optional" forbiddenTagName = "k8s:forbidden" + defaultTagName = "default" // TODO: this should evenually be +k8s:default ) func init() { @@ -84,7 +89,7 @@ var ( // TODO: It might be valuable to have a string payload for when requiredness is // conditional (e.g. required when is specified). -func (requirednessTagValidator) doRequired(context Context) (Validations, error) { +func (rtv requirednessTagValidator) doRequired(context Context) (Validations, error) { // Most validators don't care whether the value they are validating was // originally defined as a value-type or a pointer-type in the API. This // one does. Since Go doesn't do partial specialization of templates, we @@ -114,7 +119,48 @@ var ( optionalMapValidator = types.Name{Package: libValidationPkg, Name: "OptionalMap"} ) -func (requirednessTagValidator) doOptional(context Context) (Validations, error) { +func (rtv requirednessTagValidator) doOptional(context Context) (Validations, error) { + // All of our tags are expressed from the perspective of a client of the + // API, but the code we generate is for the server. Optional is tricky. + // + // A field which is marked as optional and does not have a default is + // strictly optional. A client is allowed to not set it and the server will + // not give it a default value. Code which consumes it must handle that it + // might not have any value at all. + // + // A field which is marked as optional but has a default is optional to + // clients, but required to the server. A client is allowed to not set it + // but the server will give it a default value. Code which consumes it can + // assume that it always has a value. + // + // One special case must be handled: optional non-pointer fields with + // default values. If the default is not the zero value for the type, then + // the zero value is used to decide whether to assign the default value, + // and so must be out of bounds; we can proceed as above. + // + // But if the default is the zero value, then the zero value is obviously + // valid, and the fact that the field is optional is meaningless - there is + // no way to tell the difference between a client not setting it (yielding + // the zero value) and a client setting it to the zero value. + // + // TODO: handle default=ref(...) + // TODO: handle manual defaulting + if hasDefault, zeroDefault, err := rtv.hasZeroDefault(context); err != nil { + return Validations{}, err + } else if hasDefault { + if !isNilableType(context.Type) && zeroDefault { + return Validations{Comments: []string{"optional value-type fields with zero-value defaults are purely documentation"}}, nil + } + validations, err := rtv.doRequired(context) + if err != nil { + return Validations{}, err + } + for i, fn := range validations.Functions { + validations.Functions[i] = WithComment(fn, "optional fields with default values are effectively required") + } + return validations, nil + } + // Most validators don't care whether the value they are validating was // originally defined as a value-type or a pointer-type in the API. This // one does. Since Go doesn't do partial specialization of templates, we @@ -137,6 +183,71 @@ func (requirednessTagValidator) doOptional(context Context) (Validations, error) return Validations{Functions: []FunctionGen{Function(optionalTagName, ShortCircuit|NonError, optionalValueValidator)}}, nil } +// hasZeroDefault returns whether the field has a default value and whether +// that default value is the zero value for the field's type. +func (rtv requirednessTagValidator) hasZeroDefault(context Context) (bool, bool, error) { + t := realType(context.Type) + // This validator only applies to fields, so Member must be valid. + tagsByName, err := gengo.ExtractFunctionStyleCommentTags("+", []string{defaultTagName}, context.Member.CommentLines) + if err != nil { + return false, false, fmt.Errorf("failed to read tags: %w", err) + } + + tags, hasDefault := tagsByName[defaultTagName] + if !hasDefault { + return false, false, nil + } + if len(tags) == 0 { + return false, false, fmt.Errorf("+default tag with no value") + } + if len(tags) > 1 { + return false, false, fmt.Errorf("+default tag with multiple values: %q", tags) + } + + payload := tags[0].Value + var defaultValue any + if err := json.Unmarshal([]byte(payload), &defaultValue); err != nil { + return false, false, fmt.Errorf("failed to parse default value %q: %w", payload, err) + } + if defaultValue == nil { + return false, false, fmt.Errorf("failed to parse default value %q: unmarshalled to nil", payload) + } + + zero, found := typeZeroValue[t.String()] + if !found { + return false, false, fmt.Errorf("unknown zero-value for type %s", t.String()) + } + + return true, reflect.DeepEqual(defaultValue, zero), nil +} + +// This is copied from defaulter-gen. +// TODO: move this back to gengo as Type.ZeroValue()? +var typeZeroValue = map[string]any{ + "uint": 0., + "uint8": 0., + "uint16": 0., + "uint32": 0., + "uint64": 0., + "int": 0., + "int8": 0., + "int16": 0., + "int32": 0., + "int64": 0., + "byte": 0., + "float64": 0., + "float32": 0., + "bool": false, + "time.Time": "", + "string": "", + "integer": 0., + "number": 0., + "boolean": false, + "[]byte": "", // base64 encoded characters + "interface{}": interface{}(nil), + "any": interface{}(nil), +} + var ( forbiddenValueValidator = types.Name{Package: libValidationPkg, Name: "ForbiddenValue"} forbiddenPointerValidator = types.Name{Package: libValidationPkg, Name: "ForbiddenPointer"} diff --git a/staging/src/k8s.io/code-generator/cmd/validation-gen/validators/validators.go b/staging/src/k8s.io/code-generator/cmd/validation-gen/validators/validators.go index 81f54f80e7b..5d460ea5f6b 100644 --- a/staging/src/k8s.io/code-generator/cmd/validation-gen/validators/validators.go +++ b/staging/src/k8s.io/code-generator/cmd/validation-gen/validators/validators.go @@ -17,10 +17,11 @@ limitations under the License. package validators import ( - "k8s.io/apimachinery/pkg/util/sets" - "k8s.io/apimachinery/pkg/util/validation/field" "k8s.io/gengo/v2/generator" "k8s.io/gengo/v2/types" + + "k8s.io/apimachinery/pkg/util/sets" + "k8s.io/apimachinery/pkg/util/validation/field" ) // TagValidator describes a single validation tag and how to use it.