mirror of
https://github.com/k3s-io/kubernetes.git
synced 2026-02-22 07:03:28 +00:00
Merge pull request #133768 from jpbetz/dv-options
Add +k8s:ifEnabled, +k8s:ifDisabled and +k8s:enumExclude tags
This commit is contained in:
@@ -25,16 +25,50 @@ import (
|
||||
"k8s.io/apimachinery/pkg/util/validation/field"
|
||||
)
|
||||
|
||||
// Enum verifies that the specified value is one of the valid symbols.
|
||||
// This is for string enums only.
|
||||
func Enum[T ~string](_ context.Context, op operation.Operation, fldPath *field.Path, value, _ *T, symbols sets.Set[T]) field.ErrorList {
|
||||
// Enum verifies that a given value is a member of a set of enum values.
|
||||
// Exclude Rules that apply when options are enabled or disabled are also considered.
|
||||
// If ANY exclude rule matches for a value, that value is excluded from the enum when validating.
|
||||
func Enum[T ~string](_ context.Context, op operation.Operation, fldPath *field.Path, value, _ *T, validValues sets.Set[T], exclusions []EnumExclusion[T]) field.ErrorList {
|
||||
if value == nil {
|
||||
return nil
|
||||
}
|
||||
if !symbols.Has(*value) {
|
||||
symbolList := symbols.UnsortedList()
|
||||
slices.Sort(symbolList)
|
||||
return field.ErrorList{field.NotSupported[T](fldPath, *value, symbolList)}
|
||||
if !validValues.Has(*value) || isExcluded(op, exclusions, *value) {
|
||||
return field.ErrorList{field.NotSupported[T](fldPath, *value, supportedValues(op, validValues, exclusions))}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// supportedValues returns a sorted list of supported values.
|
||||
// Excluded enum values are not included in the list.
|
||||
func supportedValues[T ~string](op operation.Operation, values sets.Set[T], exclusions []EnumExclusion[T]) []T {
|
||||
res := make([]T, 0, len(values))
|
||||
for key := range values {
|
||||
if isExcluded(op, exclusions, key) {
|
||||
continue
|
||||
}
|
||||
res = append(res, key)
|
||||
}
|
||||
slices.Sort(res)
|
||||
return res
|
||||
}
|
||||
|
||||
// EnumExclusion represents a single enum exclusion rule.
|
||||
type EnumExclusion[T ~string] struct {
|
||||
// Value specifies the enum value to be conditionally excluded.
|
||||
Value T
|
||||
// ExcludeWhen determines the condition for exclusion.
|
||||
// If true, the value is excluded if the option is present.
|
||||
// If false, the value is excluded if the option is NOT present.
|
||||
ExcludeWhen bool
|
||||
// Option is the name of the feature option that controls the exclusion.
|
||||
Option string
|
||||
}
|
||||
|
||||
func isExcluded[T ~string](op operation.Operation, exclusions []EnumExclusion[T], value T) bool {
|
||||
for _, rule := range exclusions {
|
||||
if rule.Value == value && rule.ExcludeWhen == op.HasOption(rule.Option) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -27,38 +27,43 @@ import (
|
||||
|
||||
func TestEnum(t *testing.T) {
|
||||
cases := []struct {
|
||||
value string
|
||||
valid sets.Set[string]
|
||||
err bool
|
||||
name string
|
||||
value string
|
||||
valid sets.Set[string]
|
||||
expectErr string
|
||||
}{{
|
||||
value: "a",
|
||||
valid: sets.New("a", "b", "c"),
|
||||
err: false,
|
||||
name: "valid value",
|
||||
value: "a",
|
||||
valid: sets.New("a", "b", "c"),
|
||||
expectErr: "",
|
||||
}, {
|
||||
value: "x",
|
||||
valid: sets.New("c", "a", "b"),
|
||||
err: true,
|
||||
name: "invalid value",
|
||||
value: "x",
|
||||
valid: sets.New("a", "b", "c"),
|
||||
expectErr: `fldpath: Unsupported value: "x": supported values: "a", "b", "c"`,
|
||||
}}
|
||||
|
||||
for i, tc := range cases {
|
||||
result := Enum(context.Background(), operation.Operation{}, field.NewPath("fldpath"), &tc.value, nil, tc.valid)
|
||||
if len(result) > 0 && !tc.err {
|
||||
t.Errorf("case %d: unexpected failure: %v", i, fmtErrs(result))
|
||||
continue
|
||||
}
|
||||
if len(result) == 0 && tc.err {
|
||||
t.Errorf("case %d: unexpected success", i)
|
||||
continue
|
||||
}
|
||||
if len(result) > 0 {
|
||||
if len(result) > 1 {
|
||||
t.Errorf("case %d: unexepected multi-error: %v", i, fmtErrs(result))
|
||||
continue
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
op := operation.Operation{Type: operation.Create}
|
||||
errs := Enum(context.Background(), op, field.NewPath("fldpath"), &tc.value, nil, tc.valid, nil)
|
||||
|
||||
if tc.expectErr == "" {
|
||||
if len(errs) > 0 {
|
||||
t.Fatalf("expected no error, but got: %v", errs)
|
||||
}
|
||||
} else {
|
||||
if len(errs) == 0 {
|
||||
t.Fatal("expected an error, but got none")
|
||||
}
|
||||
if len(errs) > 1 {
|
||||
t.Fatalf("expected a single error, but got: %v", errs)
|
||||
}
|
||||
if errs[0].Error() != tc.expectErr {
|
||||
t.Errorf("expected error %q, but got %q", tc.expectErr, errs[0].Error())
|
||||
}
|
||||
}
|
||||
if want, got := `supported values: "a", "b", "c"`, result[0].Detail; got != want {
|
||||
t.Errorf("case %d: wrong error, expected: %q, got: %q", i, want, got)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -71,37 +76,149 @@ func TestEnumTypedef(t *testing.T) {
|
||||
)
|
||||
|
||||
cases := []struct {
|
||||
value StringType
|
||||
valid sets.Set[StringType]
|
||||
err bool
|
||||
name string
|
||||
value StringType
|
||||
valid sets.Set[StringType]
|
||||
expectErr string
|
||||
}{{
|
||||
value: "foo",
|
||||
valid: sets.New(NotStringFoo, NotStringBar, NotStringQux),
|
||||
err: false,
|
||||
name: "valid value",
|
||||
value: "foo",
|
||||
valid: sets.New(NotStringFoo, NotStringBar, NotStringQux),
|
||||
expectErr: "",
|
||||
}, {
|
||||
value: "x",
|
||||
valid: sets.New(NotStringFoo, NotStringBar, NotStringQux),
|
||||
err: true,
|
||||
name: "invalid value",
|
||||
value: "x",
|
||||
valid: sets.New(NotStringFoo, NotStringBar, NotStringQux),
|
||||
expectErr: `fldpath: Unsupported value: "x": supported values: "bar", "foo", "qux"`,
|
||||
}}
|
||||
|
||||
for i, tc := range cases {
|
||||
result := Enum(context.Background(), operation.Operation{}, field.NewPath("fldpath"), &tc.value, nil, tc.valid)
|
||||
if len(result) > 0 && !tc.err {
|
||||
t.Errorf("case %d: unexpected failure: %v", i, fmtErrs(result))
|
||||
continue
|
||||
}
|
||||
if len(result) == 0 && tc.err {
|
||||
t.Errorf("case %d: unexpected success", i)
|
||||
continue
|
||||
}
|
||||
if len(result) > 0 {
|
||||
if len(result) > 1 {
|
||||
t.Errorf("case %d: unexepected multi-error: %v", i, fmtErrs(result))
|
||||
continue
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
op := operation.Operation{Type: operation.Create}
|
||||
errs := Enum(context.Background(), op, field.NewPath("fldpath"), &tc.value, nil, tc.valid, nil)
|
||||
|
||||
if tc.expectErr == "" {
|
||||
if len(errs) > 0 {
|
||||
t.Fatalf("expected no error, but got: %v", errs)
|
||||
}
|
||||
} else {
|
||||
if len(errs) == 0 {
|
||||
t.Fatal("expected an error, but got none")
|
||||
}
|
||||
if len(errs) > 1 {
|
||||
t.Fatalf("expected a single error, but got: %v", errs)
|
||||
}
|
||||
if errs[0].Error() != tc.expectErr {
|
||||
t.Errorf("expected error %q, but got %q", tc.expectErr, errs[0].Error())
|
||||
}
|
||||
}
|
||||
if want, got := `supported values: "bar", "foo", "qux"`, result[0].Detail; got != want {
|
||||
t.Errorf("case %d: wrong error, expected: %q, got: %q", i, want, got)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestEnumExclude(t *testing.T) {
|
||||
type TestEnum string
|
||||
const (
|
||||
ValueA TestEnum = "A"
|
||||
ValueB TestEnum = "B"
|
||||
ValueC TestEnum = "C"
|
||||
ValueD TestEnum = "D"
|
||||
)
|
||||
|
||||
const (
|
||||
FeatureA = "FeatureA"
|
||||
FeatureB = "FeatureB"
|
||||
)
|
||||
|
||||
testEnumValues := sets.New(ValueA, ValueB, ValueC, ValueD)
|
||||
testEnumExclusions := []EnumExclusion[TestEnum]{
|
||||
{Value: ValueA, Option: FeatureA, ExcludeWhen: true},
|
||||
{Value: ValueB, Option: FeatureB, ExcludeWhen: false},
|
||||
{Value: ValueD, Option: FeatureA, ExcludeWhen: true},
|
||||
{Value: ValueD, Option: FeatureB, ExcludeWhen: false},
|
||||
}
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
value TestEnum
|
||||
opts []string
|
||||
expectErr string
|
||||
}{
|
||||
{
|
||||
name: "no options, A is valid",
|
||||
value: ValueA,
|
||||
},
|
||||
{
|
||||
name: "no options, B is invalid",
|
||||
value: ValueB,
|
||||
expectErr: `fld: Unsupported value: "B": supported values: "A", "C"`,
|
||||
},
|
||||
{
|
||||
name: "no options, D is invalid",
|
||||
value: ValueD,
|
||||
expectErr: `fld: Unsupported value: "D": supported values: "A", "C"`,
|
||||
},
|
||||
{
|
||||
name: "FeatureA enabled, A is invalid",
|
||||
value: ValueA,
|
||||
opts: []string{FeatureA},
|
||||
expectErr: `fld: Unsupported value: "A": supported values: "C"`,
|
||||
},
|
||||
{
|
||||
name: "FeatureA enabled, B is invalid",
|
||||
value: ValueB,
|
||||
opts: []string{FeatureA},
|
||||
expectErr: `fld: Unsupported value: "B": supported values: "C"`,
|
||||
},
|
||||
{
|
||||
name: "FeatureB enabled, A is valid",
|
||||
value: ValueA,
|
||||
opts: []string{FeatureB},
|
||||
},
|
||||
{
|
||||
name: "FeatureB enabled, B is valid",
|
||||
value: ValueB,
|
||||
opts: []string{FeatureB},
|
||||
},
|
||||
{
|
||||
name: "FeatureA and FeatureB enabled, A is invalid",
|
||||
value: ValueA,
|
||||
opts: []string{FeatureA, FeatureB},
|
||||
expectErr: `fld: Unsupported value: "A": supported values: "B", "C"`,
|
||||
},
|
||||
{
|
||||
name: "FeatureA and FeatureB enabled, B is valid",
|
||||
value: ValueB,
|
||||
opts: []string{FeatureA, FeatureB},
|
||||
},
|
||||
{
|
||||
name: "FeatureA and FeatureB enabled, D is invalid",
|
||||
value: ValueD,
|
||||
opts: []string{FeatureA, FeatureB},
|
||||
expectErr: `fld: Unsupported value: "D": supported values: "B", "C"`,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
op := operation.Operation{Type: operation.Create, Options: tc.opts}
|
||||
errs := Enum(context.Background(), op, field.NewPath("fld"), &tc.value, nil, testEnumValues, testEnumExclusions)
|
||||
|
||||
if tc.expectErr == "" {
|
||||
if len(errs) > 0 {
|
||||
t.Fatalf("expected no error, but got: %v", errs)
|
||||
}
|
||||
} else {
|
||||
if len(errs) == 0 {
|
||||
t.Fatal("expected an error, but got none")
|
||||
}
|
||||
if len(errs) > 1 {
|
||||
t.Fatalf("expected a single error, but got: %v", errs)
|
||||
}
|
||||
if errs[0].Error() != tc.expectErr {
|
||||
t.Errorf("expected error %q, but got %q", tc.expectErr, errs[0].Error())
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
35
staging/src/k8s.io/apimachinery/pkg/api/validate/options.go
Normal file
35
staging/src/k8s.io/apimachinery/pkg/api/validate/options.go
Normal file
@@ -0,0 +1,35 @@
|
||||
/*
|
||||
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/operation"
|
||||
"k8s.io/apimachinery/pkg/util/validation/field"
|
||||
)
|
||||
|
||||
// IfOption conditionally evaluates a validation function. If the option and enabled are both true the validator
|
||||
// is called. If the option and enabled are both false the validator is called. Otherwise, the validator is not called.
|
||||
func IfOption[T any](ctx context.Context, op operation.Operation, fldPath *field.Path, value, oldValue *T,
|
||||
optionName string, enabled bool, validator func(context.Context, operation.Operation, *field.Path, *T, *T) field.ErrorList,
|
||||
) field.ErrorList {
|
||||
if op.HasOption(optionName) == enabled {
|
||||
return validator(ctx, op, fldPath, value, oldValue)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -19,6 +19,7 @@ package enum
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"k8s.io/apimachinery/pkg/util/validation/field"
|
||||
"k8s.io/utils/ptr"
|
||||
)
|
||||
|
||||
@@ -27,10 +28,10 @@ func Test(t *testing.T) {
|
||||
|
||||
st.Value(&Struct{
|
||||
// All zero vals
|
||||
}).ExpectRegexpsByPath(map[string][]string{
|
||||
"enum0Field": {"Unsupported value: \"\"$"},
|
||||
"enum1Field": {"Unsupported value: \"\": supported values: \"e1v1\""},
|
||||
"enum2Field": {"Unsupported value: \"\": supported values: \"e2v1\", \"e2v2\""},
|
||||
}).ExpectMatches(field.ErrorMatcher{}.ByType().ByField(), field.ErrorList{
|
||||
field.NotSupported(field.NewPath("enum0Field"), Enum0(""), []Enum0{}),
|
||||
field.NotSupported(field.NewPath("enum1Field"), Enum1(""), []Enum1{E1V1}),
|
||||
field.NotSupported(field.NewPath("enum2Field"), Enum2(""), []Enum2{E2V1, E2V2}),
|
||||
})
|
||||
|
||||
st.Value(&Struct{
|
||||
@@ -42,9 +43,9 @@ func Test(t *testing.T) {
|
||||
Enum2PtrField: ptr.To(E2V1),
|
||||
NotEnumField: "x",
|
||||
NotEnumPtrField: ptr.To(NotEnum("x")),
|
||||
}).ExpectRegexpsByPath(map[string][]string{
|
||||
"enum0Field": {"Unsupported value: \"\"$"},
|
||||
"enum0PtrField": {"Unsupported value: \"\"$"},
|
||||
}).ExpectMatches(field.ErrorMatcher{}.ByType().ByField(), field.ErrorList{
|
||||
field.NotSupported(field.NewPath("enum0Field"), Enum0(""), []Enum0{}),
|
||||
field.NotSupported(field.NewPath("enum0PtrField"), Enum0(""), []Enum0{}),
|
||||
})
|
||||
|
||||
st.Value(&Struct{
|
||||
@@ -56,12 +57,12 @@ func Test(t *testing.T) {
|
||||
Enum2PtrField: ptr.To(Enum2("x")),
|
||||
NotEnumField: "x",
|
||||
NotEnumPtrField: ptr.To(NotEnum("x")),
|
||||
}).ExpectRegexpsByPath(map[string][]string{
|
||||
"enum0Field": {"Unsupported value: \"x\"$"},
|
||||
"enum0PtrField": {"Unsupported value: \"x\"$"},
|
||||
"enum1Field": {"Unsupported value: \"x\": supported values: \"e1v1\""},
|
||||
"enum1PtrField": {"Unsupported value: \"x\": supported values: \"e1v1\""},
|
||||
"enum2Field": {"Unsupported value: \"x\": supported values: \"e2v1\", \"e2v2\""},
|
||||
"enum2PtrField": {"Unsupported value: \"x\": supported values: \"e2v1\", \"e2v2\""},
|
||||
}).ExpectMatches(field.ErrorMatcher{}.ByType().ByField(), field.ErrorList{
|
||||
field.NotSupported(field.NewPath("enum0Field"), Enum0("x"), []Enum0{}),
|
||||
field.NotSupported(field.NewPath("enum0PtrField"), Enum0("x"), []Enum0{}),
|
||||
field.NotSupported(field.NewPath("enum1Field"), Enum1("x"), []Enum1{E1V1}),
|
||||
field.NotSupported(field.NewPath("enum1PtrField"), Enum1("x"), []Enum1{E1V1}),
|
||||
field.NotSupported(field.NewPath("enum2Field"), Enum2("x"), []Enum2{E2V1, E2V2}),
|
||||
field.NotSupported(field.NewPath("enum2PtrField"), Enum2("x"), []Enum2{E2V1, E2V2}),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -0,0 +1,110 @@
|
||||
/*
|
||||
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.
|
||||
*/
|
||||
|
||||
// +k8s:validation-gen=TypeMeta
|
||||
// +k8s:validation-gen-scheme-registry=k8s.io/code-generator/cmd/validation-gen/testscheme.Scheme
|
||||
|
||||
// This is a test package.
|
||||
package options
|
||||
|
||||
import "k8s.io/code-generator/cmd/validation-gen/testscheme"
|
||||
|
||||
var localSchemeBuilder = testscheme.New()
|
||||
|
||||
type Struct struct {
|
||||
TypeMeta int
|
||||
|
||||
Enum0Field Enum0 `json:"enum0Field"`
|
||||
Enum0PtrField *Enum0 `json:"enum0PtrField"`
|
||||
|
||||
Enum1Field Enum1 `json:"enum1Field"`
|
||||
Enum1PtrField *Enum1 `json:"enum1PtrField"`
|
||||
|
||||
Enum2Field Enum2 `json:"enum2Field"`
|
||||
Enum2PtrField *Enum2 `json:"enum2PtrField"`
|
||||
|
||||
NotEnumField NotEnum `json:"notEnumField"`
|
||||
NotEnumPtrField *NotEnum `json:"notEnumPtrField"`
|
||||
|
||||
EnumWithExcludeField EnumWithExclude `json:"enumWithExcludeField"`
|
||||
EnumWithExcludePtrField *EnumWithExclude `json:"enumWithExcludePtrField"`
|
||||
}
|
||||
|
||||
type ConditionalStruct struct {
|
||||
TypeMeta int
|
||||
|
||||
ConditionalEnumField ConditionalEnum `json:"conditionalEnumField"`
|
||||
ConditionalEnumPtrField *ConditionalEnum `json:"conditionalEnumPtrField"`
|
||||
}
|
||||
|
||||
// +k8s:enum
|
||||
type Enum0 string // Note: this enum has no values
|
||||
|
||||
// +k8s:enum
|
||||
type Enum1 string // Note: this enum has 1 value
|
||||
|
||||
const (
|
||||
E1V1 Enum1 = "e1v1"
|
||||
)
|
||||
|
||||
// +k8s:enum
|
||||
type Enum2 string // Note: this enum has 2 values
|
||||
|
||||
const (
|
||||
E2V1 Enum2 = "e2v1"
|
||||
E2V2 Enum2 = "e2v2"
|
||||
)
|
||||
|
||||
// Note: this is not an enum because the const values are of type Enum2, and
|
||||
// because go elides intermediate typedefs (this is modelled as "NotEnum" ->
|
||||
// "string" in the AST).
|
||||
type NotEnum Enum2
|
||||
|
||||
// +k8s:enum
|
||||
type EnumWithExclude string
|
||||
|
||||
const (
|
||||
EnumWithExclude1 EnumWithExclude = "enumWithExclude1"
|
||||
|
||||
// +k8s:enumExclude
|
||||
EnumWithExclude2 EnumWithExclude = "enumWithExclude2"
|
||||
)
|
||||
|
||||
// +k8s:enum
|
||||
type ConditionalEnum string
|
||||
|
||||
const (
|
||||
// +k8s:ifEnabled(FeatureA)=+k8s:enumExclude
|
||||
ConditionalA ConditionalEnum = "A"
|
||||
|
||||
// +k8s:ifDisabled(FeatureB)=+k8s:enumExclude
|
||||
ConditionalB ConditionalEnum = "B"
|
||||
|
||||
// This value is always included.
|
||||
ConditionalC ConditionalEnum = "C"
|
||||
|
||||
// +k8s:ifEnabled(FeatureA)=+k8s:enumExclude
|
||||
// +k8s:ifEnabled(FeatureB)=+k8s:enumExclude
|
||||
ConditionalD ConditionalEnum = "D"
|
||||
|
||||
// +k8s:ifDisabled(FeatureC)=+k8s:enumExclude
|
||||
// +k8s:ifDisabled(FeatureD)=+k8s:enumExclude
|
||||
ConditionalE ConditionalEnum = "E"
|
||||
|
||||
// +k8s:ifDisabled(FeatureC)=+k8s:enumExclude
|
||||
// +k8s:ifEnabled(FeatureD)=+k8s:enumExclude
|
||||
ConditionalF ConditionalEnum = "F"
|
||||
)
|
||||
@@ -0,0 +1,197 @@
|
||||
/*
|
||||
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 options
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"k8s.io/apimachinery/pkg/util/validation/field"
|
||||
)
|
||||
|
||||
func Test(t *testing.T) {
|
||||
st := localSchemeBuilder.Test(t)
|
||||
|
||||
st.Value(&ConditionalStruct{
|
||||
ConditionalEnumField: "",
|
||||
}).ExpectMatches(field.ErrorMatcher{}.ByType().ByField(), field.ErrorList{
|
||||
field.NotSupported(field.NewPath("conditionalEnumField"), ConditionalEnum(""), []ConditionalEnum{ConditionalA, ConditionalC, ConditionalD}),
|
||||
})
|
||||
|
||||
// Scenario 1: No options (default)
|
||||
// Valid values: A, C, D
|
||||
st.Value(&ConditionalStruct{
|
||||
ConditionalEnumField: "B",
|
||||
}).ExpectMatches(field.ErrorMatcher{}.ByType().ByField(), field.ErrorList{
|
||||
field.NotSupported(field.NewPath("conditionalEnumField"), ConditionalEnum("B"), []ConditionalEnum{ConditionalA, ConditionalC, ConditionalD}),
|
||||
})
|
||||
st.Value(&ConditionalStruct{
|
||||
ConditionalEnumField: "E",
|
||||
}).ExpectMatches(field.ErrorMatcher{}.ByType().ByField(), field.ErrorList{
|
||||
field.NotSupported(field.NewPath("conditionalEnumField"), ConditionalEnum("E"), []ConditionalEnum{ConditionalA, ConditionalC, ConditionalD}),
|
||||
})
|
||||
st.Value(&ConditionalStruct{
|
||||
ConditionalEnumField: "F",
|
||||
}).ExpectMatches(field.ErrorMatcher{}.ByType().ByField(), field.ErrorList{
|
||||
field.NotSupported(field.NewPath("conditionalEnumField"), ConditionalEnum("F"), []ConditionalEnum{ConditionalA, ConditionalC, ConditionalD}),
|
||||
})
|
||||
st.Value(&ConditionalStruct{
|
||||
ConditionalEnumField: "A",
|
||||
}).ExpectValid()
|
||||
st.Value(&ConditionalStruct{
|
||||
ConditionalEnumField: "C",
|
||||
}).ExpectValid()
|
||||
st.Value(&ConditionalStruct{
|
||||
ConditionalEnumField: "D",
|
||||
}).ExpectValid()
|
||||
|
||||
// Scenario 2: FeatureA enabled
|
||||
// Valid values: C
|
||||
st.Value(&ConditionalStruct{
|
||||
ConditionalEnumField: "A",
|
||||
}).Opts([]string{"FeatureA"}).ExpectMatches(field.ErrorMatcher{}.ByType().ByField(), field.ErrorList{
|
||||
field.NotSupported(field.NewPath("conditionalEnumField"), ConditionalEnum("A"), []ConditionalEnum{ConditionalC}),
|
||||
})
|
||||
st.Value(&ConditionalStruct{
|
||||
ConditionalEnumField: "B",
|
||||
}).Opts([]string{"FeatureA"}).ExpectMatches(field.ErrorMatcher{}.ByType().ByField(), field.ErrorList{
|
||||
field.NotSupported(field.NewPath("conditionalEnumField"), ConditionalEnum("B"), []ConditionalEnum{ConditionalC}),
|
||||
})
|
||||
st.Value(&ConditionalStruct{
|
||||
ConditionalEnumField: "D",
|
||||
}).Opts([]string{"FeatureA"}).ExpectMatches(field.ErrorMatcher{}.ByType().ByField(), field.ErrorList{
|
||||
field.NotSupported(field.NewPath("conditionalEnumField"), ConditionalEnum("D"), []ConditionalEnum{ConditionalC}),
|
||||
})
|
||||
st.Value(&ConditionalStruct{
|
||||
ConditionalEnumField: "E",
|
||||
}).Opts([]string{"FeatureA"}).ExpectMatches(field.ErrorMatcher{}.ByType().ByField(), field.ErrorList{
|
||||
field.NotSupported(field.NewPath("conditionalEnumField"), ConditionalEnum("E"), []ConditionalEnum{ConditionalC}),
|
||||
})
|
||||
st.Value(&ConditionalStruct{
|
||||
ConditionalEnumField: "F",
|
||||
}).Opts([]string{"FeatureA"}).ExpectMatches(field.ErrorMatcher{}.ByType().ByField(), field.ErrorList{
|
||||
field.NotSupported(field.NewPath("conditionalEnumField"), ConditionalEnum("F"), []ConditionalEnum{ConditionalC}),
|
||||
})
|
||||
st.Value(&ConditionalStruct{
|
||||
ConditionalEnumField: "C",
|
||||
}).Opts([]string{"FeatureA"}).ExpectValid()
|
||||
|
||||
// Scenario 3: FeatureB enabled
|
||||
// Valid values: A, B, C
|
||||
st.Value(&ConditionalStruct{
|
||||
ConditionalEnumField: "D",
|
||||
}).Opts([]string{"FeatureB"}).ExpectMatches(field.ErrorMatcher{}.ByType().ByField(), field.ErrorList{
|
||||
field.NotSupported(field.NewPath("conditionalEnumField"), ConditionalEnum("D"), []ConditionalEnum{ConditionalA, ConditionalB, ConditionalC}),
|
||||
})
|
||||
st.Value(&ConditionalStruct{
|
||||
ConditionalEnumField: "E",
|
||||
}).Opts([]string{"FeatureB"}).ExpectMatches(field.ErrorMatcher{}.ByType().ByField(), field.ErrorList{
|
||||
field.NotSupported(field.NewPath("conditionalEnumField"), ConditionalEnum("E"), []ConditionalEnum{ConditionalA, ConditionalB, ConditionalC}),
|
||||
})
|
||||
st.Value(&ConditionalStruct{
|
||||
ConditionalEnumField: "F",
|
||||
}).Opts([]string{"FeatureB"}).ExpectMatches(field.ErrorMatcher{}.ByType().ByField(), field.ErrorList{
|
||||
field.NotSupported(field.NewPath("conditionalEnumField"), ConditionalEnum("F"), []ConditionalEnum{ConditionalA, ConditionalB, ConditionalC}),
|
||||
})
|
||||
st.Value(&ConditionalStruct{
|
||||
ConditionalEnumField: "A",
|
||||
}).Opts([]string{"FeatureB"}).ExpectValid()
|
||||
st.Value(&ConditionalStruct{
|
||||
ConditionalEnumField: "B",
|
||||
}).Opts([]string{"FeatureB"}).ExpectValid()
|
||||
st.Value(&ConditionalStruct{
|
||||
ConditionalEnumField: "C",
|
||||
}).Opts([]string{"FeatureB"}).ExpectValid()
|
||||
|
||||
// Scenario 4: FeatureA and FeatureB enabled
|
||||
// Valid values: B, C
|
||||
st.Value(&ConditionalStruct{
|
||||
ConditionalEnumField: "A",
|
||||
}).Opts([]string{"FeatureA", "FeatureB"}).ExpectMatches(field.ErrorMatcher{}.ByType().ByField(), field.ErrorList{
|
||||
field.NotSupported(field.NewPath("conditionalEnumField"), ConditionalEnum("A"), []ConditionalEnum{ConditionalB, ConditionalC}),
|
||||
})
|
||||
st.Value(&ConditionalStruct{
|
||||
ConditionalEnumField: "D",
|
||||
}).Opts([]string{"FeatureA", "FeatureB"}).ExpectMatches(field.ErrorMatcher{}.ByType().ByField(), field.ErrorList{
|
||||
field.NotSupported(field.NewPath("conditionalEnumField"), ConditionalEnum("D"), []ConditionalEnum{ConditionalB, ConditionalC}),
|
||||
})
|
||||
st.Value(&ConditionalStruct{
|
||||
ConditionalEnumField: "E",
|
||||
}).Opts([]string{"FeatureA", "FeatureB"}).ExpectMatches(field.ErrorMatcher{}.ByType().ByField(), field.ErrorList{
|
||||
field.NotSupported(field.NewPath("conditionalEnumField"), ConditionalEnum("E"), []ConditionalEnum{ConditionalB, ConditionalC}),
|
||||
})
|
||||
st.Value(&ConditionalStruct{
|
||||
ConditionalEnumField: "F",
|
||||
}).Opts([]string{"FeatureA", "FeatureB"}).ExpectMatches(field.ErrorMatcher{}.ByType().ByField(), field.ErrorList{
|
||||
field.NotSupported(field.NewPath("conditionalEnumField"), ConditionalEnum("F"), []ConditionalEnum{ConditionalB, ConditionalC}),
|
||||
})
|
||||
st.Value(&ConditionalStruct{
|
||||
ConditionalEnumField: "B",
|
||||
}).Opts([]string{"FeatureA", "FeatureB"}).ExpectValid()
|
||||
st.Value(&ConditionalStruct{
|
||||
ConditionalEnumField: "C",
|
||||
}).Opts([]string{"FeatureA", "FeatureB"}).ExpectValid()
|
||||
|
||||
// Scenario 5: FeatureC and FeatureD enabled
|
||||
// Valid values: A, C, D, E
|
||||
st.Value(&ConditionalStruct{
|
||||
ConditionalEnumField: "B",
|
||||
}).Opts([]string{"FeatureC", "FeatureD"}).ExpectMatches(field.ErrorMatcher{}.ByType().ByField(), field.ErrorList{
|
||||
field.NotSupported(field.NewPath("conditionalEnumField"), ConditionalEnum("B"), []ConditionalEnum{ConditionalA, ConditionalC, ConditionalD, ConditionalE}),
|
||||
})
|
||||
st.Value(&ConditionalStruct{
|
||||
ConditionalEnumField: "F",
|
||||
}).Opts([]string{"FeatureC", "FeatureD"}).ExpectMatches(field.ErrorMatcher{}.ByType().ByField(), field.ErrorList{
|
||||
field.NotSupported(field.NewPath("conditionalEnumField"), ConditionalEnum("F"), []ConditionalEnum{ConditionalA, ConditionalC, ConditionalD, ConditionalE}),
|
||||
})
|
||||
st.Value(&ConditionalStruct{
|
||||
ConditionalEnumField: "A",
|
||||
}).Opts([]string{"FeatureC", "FeatureD"}).ExpectValid()
|
||||
st.Value(&ConditionalStruct{
|
||||
ConditionalEnumField: "C",
|
||||
}).Opts([]string{"FeatureC", "FeatureD"}).ExpectValid()
|
||||
st.Value(&ConditionalStruct{
|
||||
ConditionalEnumField: "D",
|
||||
}).Opts([]string{"FeatureC", "FeatureD"}).ExpectValid()
|
||||
st.Value(&ConditionalStruct{
|
||||
ConditionalEnumField: "E",
|
||||
}).Opts([]string{"FeatureC", "FeatureD"}).ExpectValid()
|
||||
|
||||
// Scenario 6: FeatureB and FeatureC enabled
|
||||
// Valid values: A, B, C, F
|
||||
st.Value(&ConditionalStruct{
|
||||
ConditionalEnumField: "D",
|
||||
}).Opts([]string{"FeatureB", "FeatureC"}).ExpectMatches(field.ErrorMatcher{}.ByType().ByField(), field.ErrorList{
|
||||
field.NotSupported(field.NewPath("conditionalEnumField"), ConditionalEnum("D"), []ConditionalEnum{ConditionalA, ConditionalB, ConditionalC, ConditionalF}),
|
||||
})
|
||||
st.Value(&ConditionalStruct{
|
||||
ConditionalEnumField: "E",
|
||||
}).Opts([]string{"FeatureB", "FeatureC"}).ExpectMatches(field.ErrorMatcher{}.ByType().ByField(), field.ErrorList{
|
||||
field.NotSupported(field.NewPath("conditionalEnumField"), ConditionalEnum("E"), []ConditionalEnum{ConditionalA, ConditionalB, ConditionalC, ConditionalF}),
|
||||
})
|
||||
st.Value(&ConditionalStruct{
|
||||
ConditionalEnumField: "A",
|
||||
}).Opts([]string{"FeatureB", "FeatureC"}).ExpectValid()
|
||||
st.Value(&ConditionalStruct{
|
||||
ConditionalEnumField: "B",
|
||||
}).Opts([]string{"FeatureB", "FeatureC"}).ExpectValid()
|
||||
st.Value(&ConditionalStruct{
|
||||
ConditionalEnumField: "C",
|
||||
}).Opts([]string{"FeatureB", "FeatureC"}).ExpectValid()
|
||||
st.Value(&ConditionalStruct{
|
||||
ConditionalEnumField: "F",
|
||||
}).Opts([]string{"FeatureB", "FeatureC"}).ExpectValid()
|
||||
}
|
||||
@@ -0,0 +1,219 @@
|
||||
//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 options
|
||||
|
||||
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"
|
||||
sets "k8s.io/apimachinery/pkg/util/sets"
|
||||
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((*ConditionalStruct)(nil), func(ctx context.Context, op operation.Operation, obj, oldObj interface{}) field.ErrorList {
|
||||
switch op.Request.SubresourcePath() {
|
||||
case "/":
|
||||
return Validate_ConditionalStruct(ctx, op, nil /* fldPath */, obj.(*ConditionalStruct), safe.Cast[*ConditionalStruct](oldObj))
|
||||
}
|
||||
return field.ErrorList{field.InternalError(nil, fmt.Errorf("no validation found for %T, subresource: %v", obj, op.Request.SubresourcePath()))}
|
||||
})
|
||||
scheme.AddValidationFunc((*Struct)(nil), func(ctx context.Context, op operation.Operation, obj, oldObj interface{}) field.ErrorList {
|
||||
switch op.Request.SubresourcePath() {
|
||||
case "/":
|
||||
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, subresource: %v", obj, op.Request.SubresourcePath()))}
|
||||
})
|
||||
return nil
|
||||
}
|
||||
|
||||
var exclusionsForConditionalEnum = []validate.EnumExclusion[ConditionalEnum]{
|
||||
{
|
||||
Value: ConditionalA, Option: "FeatureA", ExcludeWhen: true},
|
||||
{
|
||||
Value: ConditionalB, Option: "FeatureB", ExcludeWhen: false},
|
||||
{
|
||||
Value: ConditionalD, Option: "FeatureA", ExcludeWhen: true},
|
||||
{
|
||||
Value: ConditionalD, Option: "FeatureB", ExcludeWhen: true},
|
||||
{
|
||||
Value: ConditionalE, Option: "FeatureC", ExcludeWhen: false},
|
||||
{
|
||||
Value: ConditionalE, Option: "FeatureD", ExcludeWhen: false},
|
||||
{
|
||||
Value: ConditionalF, Option: "FeatureC", ExcludeWhen: false},
|
||||
{
|
||||
Value: ConditionalF, Option: "FeatureD", ExcludeWhen: true},
|
||||
}
|
||||
var symbolsForConditionalEnum = sets.New(ConditionalA, ConditionalB, ConditionalC, ConditionalD, ConditionalE, ConditionalF)
|
||||
|
||||
func Validate_ConditionalEnum(ctx context.Context, op operation.Operation, fldPath *field.Path, obj, oldObj *ConditionalEnum) (errs field.ErrorList) {
|
||||
// type ConditionalEnum
|
||||
if op.Type == operation.Update && (obj == oldObj || (obj != nil && oldObj != nil && *obj == *oldObj)) {
|
||||
return nil // no changes
|
||||
}
|
||||
errs = append(errs, validate.Enum(ctx, op, fldPath, obj, oldObj, symbolsForConditionalEnum, exclusionsForConditionalEnum)...)
|
||||
|
||||
return errs
|
||||
}
|
||||
|
||||
func Validate_ConditionalStruct(ctx context.Context, op operation.Operation, fldPath *field.Path, obj, oldObj *ConditionalStruct) (errs field.ErrorList) {
|
||||
// field ConditionalStruct.TypeMeta has no validation
|
||||
|
||||
// field ConditionalStruct.ConditionalEnumField
|
||||
errs = append(errs,
|
||||
func(fldPath *field.Path, obj, oldObj *ConditionalEnum) (errs field.ErrorList) {
|
||||
errs = append(errs, Validate_ConditionalEnum(ctx, op, fldPath, obj, oldObj)...)
|
||||
return
|
||||
}(fldPath.Child("conditionalEnumField"), &obj.ConditionalEnumField, safe.Field(oldObj, func(oldObj *ConditionalStruct) *ConditionalEnum { return &oldObj.ConditionalEnumField }))...)
|
||||
|
||||
// field ConditionalStruct.ConditionalEnumPtrField
|
||||
errs = append(errs,
|
||||
func(fldPath *field.Path, obj, oldObj *ConditionalEnum) (errs field.ErrorList) {
|
||||
errs = append(errs, Validate_ConditionalEnum(ctx, op, fldPath, obj, oldObj)...)
|
||||
return
|
||||
}(fldPath.Child("conditionalEnumPtrField"), obj.ConditionalEnumPtrField, safe.Field(oldObj, func(oldObj *ConditionalStruct) *ConditionalEnum { return oldObj.ConditionalEnumPtrField }))...)
|
||||
|
||||
return errs
|
||||
}
|
||||
|
||||
var symbolsForEnum0 = sets.New[Enum0]()
|
||||
|
||||
func Validate_Enum0(ctx context.Context, op operation.Operation, fldPath *field.Path, obj, oldObj *Enum0) (errs field.ErrorList) {
|
||||
// type Enum0
|
||||
if op.Type == operation.Update && (obj == oldObj || (obj != nil && oldObj != nil && *obj == *oldObj)) {
|
||||
return nil // no changes
|
||||
}
|
||||
errs = append(errs, validate.Enum(ctx, op, fldPath, obj, oldObj, symbolsForEnum0, nil)...)
|
||||
|
||||
return errs
|
||||
}
|
||||
|
||||
var symbolsForEnum1 = sets.New(E1V1)
|
||||
|
||||
func Validate_Enum1(ctx context.Context, op operation.Operation, fldPath *field.Path, obj, oldObj *Enum1) (errs field.ErrorList) {
|
||||
// type Enum1
|
||||
if op.Type == operation.Update && (obj == oldObj || (obj != nil && oldObj != nil && *obj == *oldObj)) {
|
||||
return nil // no changes
|
||||
}
|
||||
errs = append(errs, validate.Enum(ctx, op, fldPath, obj, oldObj, symbolsForEnum1, nil)...)
|
||||
|
||||
return errs
|
||||
}
|
||||
|
||||
var symbolsForEnum2 = sets.New(E2V1, E2V2)
|
||||
|
||||
func Validate_Enum2(ctx context.Context, op operation.Operation, fldPath *field.Path, obj, oldObj *Enum2) (errs field.ErrorList) {
|
||||
// type Enum2
|
||||
if op.Type == operation.Update && (obj == oldObj || (obj != nil && oldObj != nil && *obj == *oldObj)) {
|
||||
return nil // no changes
|
||||
}
|
||||
errs = append(errs, validate.Enum(ctx, op, fldPath, obj, oldObj, symbolsForEnum2, nil)...)
|
||||
|
||||
return errs
|
||||
}
|
||||
|
||||
var symbolsForEnumWithExclude = sets.New(EnumWithExclude1)
|
||||
|
||||
func Validate_EnumWithExclude(ctx context.Context, op operation.Operation, fldPath *field.Path, obj, oldObj *EnumWithExclude) (errs field.ErrorList) {
|
||||
// type EnumWithExclude
|
||||
if op.Type == operation.Update && (obj == oldObj || (obj != nil && oldObj != nil && *obj == *oldObj)) {
|
||||
return nil // no changes
|
||||
}
|
||||
errs = append(errs, validate.Enum(ctx, op, fldPath, obj, oldObj, symbolsForEnumWithExclude, nil)...)
|
||||
|
||||
return errs
|
||||
}
|
||||
|
||||
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.Enum0Field
|
||||
errs = append(errs,
|
||||
func(fldPath *field.Path, obj, oldObj *Enum0) (errs field.ErrorList) {
|
||||
errs = append(errs, Validate_Enum0(ctx, op, fldPath, obj, oldObj)...)
|
||||
return
|
||||
}(fldPath.Child("enum0Field"), &obj.Enum0Field, safe.Field(oldObj, func(oldObj *Struct) *Enum0 { return &oldObj.Enum0Field }))...)
|
||||
|
||||
// field Struct.Enum0PtrField
|
||||
errs = append(errs,
|
||||
func(fldPath *field.Path, obj, oldObj *Enum0) (errs field.ErrorList) {
|
||||
errs = append(errs, Validate_Enum0(ctx, op, fldPath, obj, oldObj)...)
|
||||
return
|
||||
}(fldPath.Child("enum0PtrField"), obj.Enum0PtrField, safe.Field(oldObj, func(oldObj *Struct) *Enum0 { return oldObj.Enum0PtrField }))...)
|
||||
|
||||
// field Struct.Enum1Field
|
||||
errs = append(errs,
|
||||
func(fldPath *field.Path, obj, oldObj *Enum1) (errs field.ErrorList) {
|
||||
errs = append(errs, Validate_Enum1(ctx, op, fldPath, obj, oldObj)...)
|
||||
return
|
||||
}(fldPath.Child("enum1Field"), &obj.Enum1Field, safe.Field(oldObj, func(oldObj *Struct) *Enum1 { return &oldObj.Enum1Field }))...)
|
||||
|
||||
// field Struct.Enum1PtrField
|
||||
errs = append(errs,
|
||||
func(fldPath *field.Path, obj, oldObj *Enum1) (errs field.ErrorList) {
|
||||
errs = append(errs, Validate_Enum1(ctx, op, fldPath, obj, oldObj)...)
|
||||
return
|
||||
}(fldPath.Child("enum1PtrField"), obj.Enum1PtrField, safe.Field(oldObj, func(oldObj *Struct) *Enum1 { return oldObj.Enum1PtrField }))...)
|
||||
|
||||
// field Struct.Enum2Field
|
||||
errs = append(errs,
|
||||
func(fldPath *field.Path, obj, oldObj *Enum2) (errs field.ErrorList) {
|
||||
errs = append(errs, Validate_Enum2(ctx, op, fldPath, obj, oldObj)...)
|
||||
return
|
||||
}(fldPath.Child("enum2Field"), &obj.Enum2Field, safe.Field(oldObj, func(oldObj *Struct) *Enum2 { return &oldObj.Enum2Field }))...)
|
||||
|
||||
// field Struct.Enum2PtrField
|
||||
errs = append(errs,
|
||||
func(fldPath *field.Path, obj, oldObj *Enum2) (errs field.ErrorList) {
|
||||
errs = append(errs, Validate_Enum2(ctx, op, fldPath, obj, oldObj)...)
|
||||
return
|
||||
}(fldPath.Child("enum2PtrField"), obj.Enum2PtrField, safe.Field(oldObj, func(oldObj *Struct) *Enum2 { return oldObj.Enum2PtrField }))...)
|
||||
|
||||
// field Struct.NotEnumField has no validation
|
||||
// field Struct.NotEnumPtrField has no validation
|
||||
|
||||
// field Struct.EnumWithExcludeField
|
||||
errs = append(errs,
|
||||
func(fldPath *field.Path, obj, oldObj *EnumWithExclude) (errs field.ErrorList) {
|
||||
errs = append(errs, Validate_EnumWithExclude(ctx, op, fldPath, obj, oldObj)...)
|
||||
return
|
||||
}(fldPath.Child("enumWithExcludeField"), &obj.EnumWithExcludeField, safe.Field(oldObj, func(oldObj *Struct) *EnumWithExclude { return &oldObj.EnumWithExcludeField }))...)
|
||||
|
||||
// field Struct.EnumWithExcludePtrField
|
||||
errs = append(errs,
|
||||
func(fldPath *field.Path, obj, oldObj *EnumWithExclude) (errs field.ErrorList) {
|
||||
errs = append(errs, Validate_EnumWithExclude(ctx, op, fldPath, obj, oldObj)...)
|
||||
return
|
||||
}(fldPath.Child("enumWithExcludePtrField"), obj.EnumWithExcludePtrField, safe.Field(oldObj, func(oldObj *Struct) *EnumWithExclude { return oldObj.EnumWithExcludePtrField }))...)
|
||||
|
||||
return errs
|
||||
}
|
||||
@@ -55,31 +55,31 @@ func Validate_Enum0(ctx context.Context, op operation.Operation, fldPath *field.
|
||||
if op.Type == operation.Update && (obj == oldObj || (obj != nil && oldObj != nil && *obj == *oldObj)) {
|
||||
return nil // no changes
|
||||
}
|
||||
errs = append(errs, validate.Enum(ctx, op, fldPath, obj, oldObj, symbolsForEnum0)...)
|
||||
errs = append(errs, validate.Enum(ctx, op, fldPath, obj, oldObj, symbolsForEnum0, nil)...)
|
||||
|
||||
return errs
|
||||
}
|
||||
|
||||
var symbolsForEnum1 = sets.New[Enum1](E1V1)
|
||||
var symbolsForEnum1 = sets.New(E1V1)
|
||||
|
||||
func Validate_Enum1(ctx context.Context, op operation.Operation, fldPath *field.Path, obj, oldObj *Enum1) (errs field.ErrorList) {
|
||||
// type Enum1
|
||||
if op.Type == operation.Update && (obj == oldObj || (obj != nil && oldObj != nil && *obj == *oldObj)) {
|
||||
return nil // no changes
|
||||
}
|
||||
errs = append(errs, validate.Enum(ctx, op, fldPath, obj, oldObj, symbolsForEnum1)...)
|
||||
errs = append(errs, validate.Enum(ctx, op, fldPath, obj, oldObj, symbolsForEnum1, nil)...)
|
||||
|
||||
return errs
|
||||
}
|
||||
|
||||
var symbolsForEnum2 = sets.New[Enum2](E2V1, E2V2)
|
||||
var symbolsForEnum2 = sets.New(E2V1, E2V2)
|
||||
|
||||
func Validate_Enum2(ctx context.Context, op operation.Operation, fldPath *field.Path, obj, oldObj *Enum2) (errs field.ErrorList) {
|
||||
// type Enum2
|
||||
if op.Type == operation.Update && (obj == oldObj || (obj != nil && oldObj != nil && *obj == *oldObj)) {
|
||||
return nil // no changes
|
||||
}
|
||||
errs = append(errs, validate.Enum(ctx, op, fldPath, obj, oldObj, symbolsForEnum2)...)
|
||||
errs = append(errs, validate.Enum(ctx, op, fldPath, obj, oldObj, symbolsForEnum2, nil)...)
|
||||
|
||||
return errs
|
||||
}
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
/*
|
||||
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.
|
||||
*/
|
||||
|
||||
// +k8s:validation-gen=TypeMeta
|
||||
// +k8s:validation-gen-scheme-registry=k8s.io/code-generator/cmd/validation-gen/testscheme.Scheme
|
||||
|
||||
// This is a test package.
|
||||
package options
|
||||
|
||||
import "k8s.io/code-generator/cmd/validation-gen/testscheme"
|
||||
|
||||
var localSchemeBuilder = testscheme.New()
|
||||
|
||||
type Struct struct {
|
||||
TypeMeta int
|
||||
|
||||
// +k8s:ifEnabled(FeatureX)=+k8s:subfield(xEnabledField)=+k8s:validateFalse="field Struct.ObjectMeta.XEnabledField"
|
||||
ObjectMeta `json:"metadata,omitempty"`
|
||||
|
||||
// +k8s:ifEnabled(FeatureX)=+k8s:validateFalse="field Struct.XEnabledField"
|
||||
XEnabledField string `json:"xEnabledField"`
|
||||
|
||||
// +k8s:ifDisabled(FeatureX)=+k8s:validateFalse="field Struct.XDisabledField"
|
||||
XDisabledField string `json:"xDisabledField"`
|
||||
|
||||
// +k8s:ifEnabled(FeatureY)=+k8s:validateFalse="field Struct.YEnabledField"
|
||||
YEnabledField string `json:"yEnabledField"`
|
||||
|
||||
// +k8s:ifDisabled(FeatureY)=+k8s:validateFalse="field Struct.YDisabledField"
|
||||
YDisabledField string `json:"yDisabledField"`
|
||||
|
||||
// +k8s:ifEnabled(FeatureX)=+k8s:validateFalse="field Struct.XYMixedField/X"
|
||||
// +k8s:ifDisabled(FeatureY)=+k8s:validateFalse="field Struct.XYMixedField/Y"
|
||||
XYMixedField string `json:"xyMixedField"`
|
||||
}
|
||||
|
||||
type ObjectMeta struct {
|
||||
XEnabledField string `json:"xEnabledField"`
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
/*
|
||||
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 options
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func Test(t *testing.T) {
|
||||
st := localSchemeBuilder.Test(t)
|
||||
|
||||
st.Value(&Struct{
|
||||
// All zero values
|
||||
}).ExpectValidateFalseByPath(map[string][]string{
|
||||
// All ifDisabled validations should trigger
|
||||
"xDisabledField": {"field Struct.XDisabledField"},
|
||||
"yDisabledField": {"field Struct.YDisabledField"},
|
||||
"xyMixedField": {"field Struct.XYMixedField/Y"},
|
||||
})
|
||||
|
||||
st.Value(&Struct{
|
||||
// All zero values
|
||||
}).Opts([]string{"FeatureX", "FeatureY"}).ExpectValidateFalseByPath(map[string][]string{
|
||||
// All ifEnabled validations should trigger
|
||||
"metadata.xEnabledField": {"field Struct.ObjectMeta.XEnabledField"},
|
||||
"xEnabledField": {"field Struct.XEnabledField"},
|
||||
"yEnabledField": {"field Struct.YEnabledField"},
|
||||
"xyMixedField": {"field Struct.XYMixedField/X"},
|
||||
})
|
||||
|
||||
st.Value(&Struct{
|
||||
// All zero values
|
||||
}).Opts([]string{"FeatureX"}).ExpectValidateFalseByPath(map[string][]string{
|
||||
// All ifEnabled validations should trigger
|
||||
"metadata.xEnabledField": {"field Struct.ObjectMeta.XEnabledField"},
|
||||
"xEnabledField": {"field Struct.XEnabledField"},
|
||||
"yDisabledField": {"field Struct.YDisabledField"},
|
||||
"xyMixedField": {
|
||||
"field Struct.XYMixedField/X",
|
||||
"field Struct.XYMixedField/Y"},
|
||||
})
|
||||
|
||||
st.Value(&Struct{
|
||||
// All zero values
|
||||
}).Opts([]string{"FeatureY"}).ExpectValidateFalseByPath(map[string][]string{
|
||||
// All ifEnabled validations should trigger
|
||||
"xDisabledField": {"field Struct.XDisabledField"},
|
||||
"yEnabledField": {"field Struct.YEnabledField"},
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,131 @@
|
||||
//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 options
|
||||
|
||||
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{}) field.ErrorList {
|
||||
switch op.Request.SubresourcePath() {
|
||||
case "/":
|
||||
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, subresource: %v", obj, op.Request.SubresourcePath()))}
|
||||
})
|
||||
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.ObjectMeta
|
||||
errs = append(errs,
|
||||
func(fldPath *field.Path, obj, oldObj *ObjectMeta) (errs field.ErrorList) {
|
||||
if op.Type == operation.Update && (obj == oldObj || (obj != nil && oldObj != nil && *obj == *oldObj)) {
|
||||
return nil // no changes
|
||||
}
|
||||
errs = append(errs, validate.IfOption(ctx, op, fldPath, obj, oldObj, "FeatureX", true, func(ctx context.Context, op operation.Operation, fldPath *field.Path, obj, oldObj *ObjectMeta) field.ErrorList {
|
||||
return validate.Subfield(ctx, op, fldPath, obj, oldObj, "xEnabledField", func(o *ObjectMeta) *string { return &o.XEnabledField }, func(ctx context.Context, op operation.Operation, fldPath *field.Path, obj, oldObj *string) field.ErrorList {
|
||||
return validate.FixedResult(ctx, op, fldPath, obj, oldObj, false, "field Struct.ObjectMeta.XEnabledField")
|
||||
})
|
||||
})...)
|
||||
return
|
||||
}(fldPath.Child("metadata"), &obj.ObjectMeta, safe.Field(oldObj, func(oldObj *Struct) *ObjectMeta { return &oldObj.ObjectMeta }))...)
|
||||
|
||||
// field Struct.XEnabledField
|
||||
errs = append(errs,
|
||||
func(fldPath *field.Path, obj, oldObj *string) (errs field.ErrorList) {
|
||||
if op.Type == operation.Update && (obj == oldObj || (obj != nil && oldObj != nil && *obj == *oldObj)) {
|
||||
return nil // no changes
|
||||
}
|
||||
errs = append(errs, validate.IfOption(ctx, op, fldPath, obj, oldObj, "FeatureX", true, func(ctx context.Context, op operation.Operation, fldPath *field.Path, obj, oldObj *string) field.ErrorList {
|
||||
return validate.FixedResult(ctx, op, fldPath, obj, oldObj, false, "field Struct.XEnabledField")
|
||||
})...)
|
||||
return
|
||||
}(fldPath.Child("xEnabledField"), &obj.XEnabledField, safe.Field(oldObj, func(oldObj *Struct) *string { return &oldObj.XEnabledField }))...)
|
||||
|
||||
// field Struct.XDisabledField
|
||||
errs = append(errs,
|
||||
func(fldPath *field.Path, obj, oldObj *string) (errs field.ErrorList) {
|
||||
if op.Type == operation.Update && (obj == oldObj || (obj != nil && oldObj != nil && *obj == *oldObj)) {
|
||||
return nil // no changes
|
||||
}
|
||||
errs = append(errs, validate.IfOption(ctx, op, fldPath, obj, oldObj, "FeatureX", false, func(ctx context.Context, op operation.Operation, fldPath *field.Path, obj, oldObj *string) field.ErrorList {
|
||||
return validate.FixedResult(ctx, op, fldPath, obj, oldObj, false, "field Struct.XDisabledField")
|
||||
})...)
|
||||
return
|
||||
}(fldPath.Child("xDisabledField"), &obj.XDisabledField, safe.Field(oldObj, func(oldObj *Struct) *string { return &oldObj.XDisabledField }))...)
|
||||
|
||||
// field Struct.YEnabledField
|
||||
errs = append(errs,
|
||||
func(fldPath *field.Path, obj, oldObj *string) (errs field.ErrorList) {
|
||||
if op.Type == operation.Update && (obj == oldObj || (obj != nil && oldObj != nil && *obj == *oldObj)) {
|
||||
return nil // no changes
|
||||
}
|
||||
errs = append(errs, validate.IfOption(ctx, op, fldPath, obj, oldObj, "FeatureY", true, func(ctx context.Context, op operation.Operation, fldPath *field.Path, obj, oldObj *string) field.ErrorList {
|
||||
return validate.FixedResult(ctx, op, fldPath, obj, oldObj, false, "field Struct.YEnabledField")
|
||||
})...)
|
||||
return
|
||||
}(fldPath.Child("yEnabledField"), &obj.YEnabledField, safe.Field(oldObj, func(oldObj *Struct) *string { return &oldObj.YEnabledField }))...)
|
||||
|
||||
// field Struct.YDisabledField
|
||||
errs = append(errs,
|
||||
func(fldPath *field.Path, obj, oldObj *string) (errs field.ErrorList) {
|
||||
if op.Type == operation.Update && (obj == oldObj || (obj != nil && oldObj != nil && *obj == *oldObj)) {
|
||||
return nil // no changes
|
||||
}
|
||||
errs = append(errs, validate.IfOption(ctx, op, fldPath, obj, oldObj, "FeatureY", false, func(ctx context.Context, op operation.Operation, fldPath *field.Path, obj, oldObj *string) field.ErrorList {
|
||||
return validate.FixedResult(ctx, op, fldPath, obj, oldObj, false, "field Struct.YDisabledField")
|
||||
})...)
|
||||
return
|
||||
}(fldPath.Child("yDisabledField"), &obj.YDisabledField, safe.Field(oldObj, func(oldObj *Struct) *string { return &oldObj.YDisabledField }))...)
|
||||
|
||||
// field Struct.XYMixedField
|
||||
errs = append(errs,
|
||||
func(fldPath *field.Path, obj, oldObj *string) (errs field.ErrorList) {
|
||||
if op.Type == operation.Update && (obj == oldObj || (obj != nil && oldObj != nil && *obj == *oldObj)) {
|
||||
return nil // no changes
|
||||
}
|
||||
errs = append(errs, validate.IfOption(ctx, op, fldPath, obj, oldObj, "FeatureY", false, func(ctx context.Context, op operation.Operation, fldPath *field.Path, obj, oldObj *string) field.ErrorList {
|
||||
return validate.FixedResult(ctx, op, fldPath, obj, oldObj, false, "field Struct.XYMixedField/Y")
|
||||
})...)
|
||||
errs = append(errs, validate.IfOption(ctx, op, fldPath, obj, oldObj, "FeatureX", true, func(ctx context.Context, op operation.Operation, fldPath *field.Path, obj, oldObj *string) field.ErrorList {
|
||||
return validate.FixedResult(ctx, op, fldPath, obj, oldObj, false, "field Struct.XYMixedField/X")
|
||||
})...)
|
||||
return
|
||||
}(fldPath.Child("xyMixedField"), &obj.XYMixedField, safe.Field(oldObj, func(oldObj *Struct) *string { return &oldObj.XYMixedField }))...)
|
||||
|
||||
return errs
|
||||
}
|
||||
@@ -287,6 +287,9 @@ func GetTargets(context *generator.Context, args *Args) []generator.Target {
|
||||
|
||||
// Create a type discoverer for all types of all inputs.
|
||||
td := NewTypeDiscoverer(validator, inputToPkg)
|
||||
if err := td.Init(context); err != nil {
|
||||
klog.Fatalf("Error discovering constants: %v", err)
|
||||
}
|
||||
|
||||
// Create a linter to collect errors as we go.
|
||||
linter := newLinter()
|
||||
|
||||
@@ -148,23 +148,62 @@ func (g *genValidations) GenerateType(c *generator.Context, t *types.Type, w io.
|
||||
|
||||
// typeDiscoverer contains fields necessary to build graphs of types.
|
||||
type typeDiscoverer struct {
|
||||
validator validators.Validator
|
||||
inputToPkg map[string]string
|
||||
initialized bool
|
||||
validator validators.Validator
|
||||
inputToPkg map[string]string
|
||||
|
||||
// constantsByType holds a map of type to constants of that type.
|
||||
constantsByType map[*types.Type][]*validators.Constant
|
||||
|
||||
// typeNodes holds a map of gengo Type to typeNode for all of the types
|
||||
// encountered during discovery.
|
||||
typeNodes map[*types.Type]*typeNode
|
||||
}
|
||||
|
||||
// NewTypeDiscoverer creates and initializes a NewTypeDiscoverer.
|
||||
// NewTypeDiscoverer creates a NewTypeDiscoverer.
|
||||
// Init must be called before calling DiscoverType.
|
||||
func NewTypeDiscoverer(validator validators.Validator, inputToPkg map[string]string) *typeDiscoverer {
|
||||
return &typeDiscoverer{
|
||||
validator: validator,
|
||||
inputToPkg: inputToPkg,
|
||||
typeNodes: map[*types.Type]*typeNode{},
|
||||
validator: validator,
|
||||
inputToPkg: inputToPkg,
|
||||
constantsByType: map[*types.Type][]*validators.Constant{},
|
||||
typeNodes: map[*types.Type]*typeNode{},
|
||||
}
|
||||
}
|
||||
|
||||
// Init uses the generator context to prepare for type discovery.
|
||||
func (td *typeDiscoverer) Init(c *generator.Context) error {
|
||||
packages := c.Universe
|
||||
for _, pkg := range packages {
|
||||
// We only care about packages we are generating for or are readonly.
|
||||
if _, ok := td.inputToPkg[pkg.Path]; !ok {
|
||||
continue
|
||||
}
|
||||
for _, cnst := range pkg.Constants {
|
||||
context := validators.Context{
|
||||
Scope: validators.ScopeConst,
|
||||
Type: cnst.Underlying,
|
||||
Path: nil, // NA when discovering a constant
|
||||
Member: nil, // NA when discovering a constant
|
||||
ParentPath: nil, // NA when discovering a constant
|
||||
}
|
||||
tgs, err := td.validator.ExtractTags(context, cnst.CommentLines)
|
||||
if err != nil {
|
||||
return fmt.Errorf("constant %s: %w", cnst.Name, err)
|
||||
}
|
||||
if len(tgs) > 0 {
|
||||
// Also check that the tgs are valid.
|
||||
if _, err := td.validator.ExtractValidations(context, tgs...); err != nil {
|
||||
return fmt.Errorf("constant %s: %w", cnst.Name, err)
|
||||
}
|
||||
}
|
||||
td.constantsByType[cnst.Underlying] = append(td.constantsByType[cnst.Underlying], &validators.Constant{Constant: cnst, Tags: tgs})
|
||||
}
|
||||
}
|
||||
td.initialized = true
|
||||
return nil
|
||||
}
|
||||
|
||||
// childNode represents a type which is used in another type (e.g. a struct
|
||||
// field).
|
||||
type childNode struct {
|
||||
@@ -210,6 +249,9 @@ type typeNode struct {
|
||||
// typeDiscoverer. If this is called multiple times for different types, the
|
||||
// graphs will be will be merged.
|
||||
func (td *typeDiscoverer) DiscoverType(t *types.Type) error {
|
||||
if !td.initialized {
|
||||
return fmt.Errorf("typeDiscoverer not initialized")
|
||||
}
|
||||
if t.Kind == types.Pointer {
|
||||
return fmt.Errorf("type %v: pointer root-types are not supported", t)
|
||||
}
|
||||
@@ -349,11 +391,14 @@ func (td *typeDiscoverer) discoverType(t *types.Type, fldPath *field.Path) (*typ
|
||||
if fldPath.String() != t.String() {
|
||||
panic(fmt.Sprintf("path for type != the type name: %s, %s", t.String(), fldPath.String()))
|
||||
}
|
||||
consts := td.constantsByType[t]
|
||||
context := validators.Context{
|
||||
Scope: validators.ScopeType,
|
||||
Type: t,
|
||||
ParentPath: nil,
|
||||
Path: fldPath,
|
||||
Member: nil, // NA when discovering a type
|
||||
ParentPath: nil, // NA when discovering a type
|
||||
Constants: consts,
|
||||
}
|
||||
extractedTags, err := td.validator.ExtractTags(context, t.CommentLines)
|
||||
if err != nil {
|
||||
@@ -1210,7 +1255,7 @@ func emitCallsToValidators(c *generator.Context, validations []validators.Functi
|
||||
sw.Do("(ctx, op, fldPath, obj, oldObj", targs)
|
||||
for _, arg := range v.Args {
|
||||
sw.Do(", ", nil)
|
||||
toGolangSourceDataLiteral(sw, c, arg)
|
||||
toGolangSourceDataLiteral(sw, emitterContext{Context: c}, arg)
|
||||
}
|
||||
sw.Do(")", targs)
|
||||
}
|
||||
@@ -1286,33 +1331,13 @@ func (g *genValidations) emitValidationVariables(c *generator.Context, t *types.
|
||||
return cmp.Compare(a.Variable.Name, b.Variable.Name)
|
||||
})
|
||||
for _, variable := range variables {
|
||||
fn := variable.InitFunc
|
||||
targs := generator.Args{
|
||||
"varName": c.Universe.Type(types.Name(variable.Variable)),
|
||||
"initFn": c.Universe.Type(fn.Function),
|
||||
}
|
||||
for _, comment := range fn.Comments {
|
||||
sw.Do("// $.$\n", comment)
|
||||
}
|
||||
sw.Do("var $.varName|private$ = $.initFn|raw$", targs)
|
||||
if typeArgs := fn.TypeArgs; len(typeArgs) > 0 {
|
||||
sw.Do("[", nil)
|
||||
for i, typeArg := range typeArgs {
|
||||
sw.Do("$.|raw$", c.Universe.Type(typeArg))
|
||||
if i < len(typeArgs)-1 {
|
||||
sw.Do(",", nil)
|
||||
}
|
||||
}
|
||||
sw.Do("]", nil)
|
||||
}
|
||||
sw.Do("(", targs)
|
||||
for i, arg := range fn.Args {
|
||||
if i != 0 {
|
||||
sw.Do(", ", nil)
|
||||
}
|
||||
toGolangSourceDataLiteral(sw, c, arg)
|
||||
}
|
||||
sw.Do(")\n", nil)
|
||||
|
||||
sw.Do("var $.varName|private$ = ", targs)
|
||||
toGolangSourceDataLiteral(sw, emitterContext{Context: c}, variable.Initializer)
|
||||
sw.Do("\n", nil)
|
||||
}
|
||||
}
|
||||
// TODO: Handle potential variable name collisions when multiple validators
|
||||
@@ -1325,7 +1350,13 @@ func (g *genValidations) emitValidationVariables(c *generator.Context, t *types.
|
||||
}
|
||||
}
|
||||
|
||||
func toGolangSourceDataLiteral(sw *generator.SnippetWriter, c *generator.Context, value any) {
|
||||
type emitterContext struct {
|
||||
*generator.Context
|
||||
// True if the literal to be emitted is a slice or array element.
|
||||
isElement bool
|
||||
}
|
||||
|
||||
func toGolangSourceDataLiteral(sw *generator.SnippetWriter, c emitterContext, value any) {
|
||||
// For safety, be strict in what values we output to visited source, and ensure strings
|
||||
// are quoted.
|
||||
|
||||
@@ -1369,9 +1400,9 @@ func toGolangSourceDataLiteral(sw *generator.SnippetWriter, c *generator.Context
|
||||
// a "standard signature" validation function to wrap it.
|
||||
targs := generator.Args{
|
||||
"funcName": c.Universe.Type(v.Function.Function),
|
||||
"field": mkSymbolArgs(c, fieldPkgSymbols),
|
||||
"operation": mkSymbolArgs(c, operationPkgSymbols),
|
||||
"context": mkSymbolArgs(c, contextPkgSymbols),
|
||||
"field": mkSymbolArgs(c.Context, fieldPkgSymbols),
|
||||
"operation": mkSymbolArgs(c.Context, operationPkgSymbols),
|
||||
"context": mkSymbolArgs(c.Context, contextPkgSymbols),
|
||||
"objType": v.ObjType,
|
||||
"objTypePfx": "*",
|
||||
}
|
||||
@@ -1379,37 +1410,23 @@ func toGolangSourceDataLiteral(sw *generator.SnippetWriter, c *generator.Context
|
||||
targs["objTypePfx"] = ""
|
||||
}
|
||||
|
||||
emitCall := func() {
|
||||
sw.Do("return $.funcName|raw$", targs)
|
||||
typeArgs := v.Function.TypeArgs
|
||||
if len(typeArgs) > 0 {
|
||||
sw.Do("[", nil)
|
||||
for i, typeArg := range typeArgs {
|
||||
sw.Do("$.|raw$", c.Universe.Type(typeArg))
|
||||
if i < len(typeArgs)-1 {
|
||||
sw.Do(",", nil)
|
||||
}
|
||||
}
|
||||
sw.Do("]", nil)
|
||||
}
|
||||
sw.Do("(ctx, op, fldPath, obj, oldObj", targs)
|
||||
for _, arg := range extraArgs {
|
||||
sw.Do(", ", nil)
|
||||
toGolangSourceDataLiteral(sw, c, arg)
|
||||
}
|
||||
sw.Do(")", targs)
|
||||
}
|
||||
sw.Do("func(", targs)
|
||||
sw.Do(" ctx $.context.Context|raw$, ", targs)
|
||||
sw.Do(" op $.operation.Operation|raw$, ", targs)
|
||||
sw.Do(" fldPath *$.field.Path|raw$, ", targs)
|
||||
sw.Do(" obj, oldObj $.objTypePfx$$.objType|raw$ ", targs)
|
||||
sw.Do(") $.field.ErrorList|raw$ {\n", targs)
|
||||
emitCall()
|
||||
sw.Do("return ", nil)
|
||||
emitFunctionCall(sw, c, v.Function, "ctx", "op", "fldPath", "obj", "oldObj")
|
||||
sw.Do("\n}", targs)
|
||||
}
|
||||
case validators.Literal:
|
||||
sw.Do("$.$", v)
|
||||
case validators.FunctionGen:
|
||||
for _, comment := range v.Comments {
|
||||
sw.Do("// $.$\\n", comment)
|
||||
}
|
||||
emitFunctionCall(sw, c, v)
|
||||
case validators.FunctionLiteral:
|
||||
sw.Do("func(", nil)
|
||||
for i, param := range v.Parameters {
|
||||
@@ -1440,10 +1457,57 @@ func toGolangSourceDataLiteral(sw *generator.SnippetWriter, c *generator.Context
|
||||
sw.Do(")", nil)
|
||||
}
|
||||
sw.Do(" { $.$ }", v.Body)
|
||||
case validators.StructLiteral:
|
||||
targs := generator.Args{
|
||||
"type": c.Universe.Type(v.Type),
|
||||
}
|
||||
if !c.isElement { // To conform to gofmt, omit type names for array/slice elements.
|
||||
sw.Do("$.type|raw$", targs)
|
||||
if len(v.TypeArgs) > 0 {
|
||||
sw.Do("[", nil)
|
||||
for i, typeArg := range v.TypeArgs {
|
||||
if i > 0 {
|
||||
sw.Do(", ", nil)
|
||||
}
|
||||
sw.Do("$.|raw$", typeArg)
|
||||
}
|
||||
sw.Do("]", nil)
|
||||
}
|
||||
}
|
||||
sw.Do("{\n", nil)
|
||||
for _, f := range v.Fields {
|
||||
sw.Do(f.Name, nil)
|
||||
sw.Do(": ", nil)
|
||||
toGolangSourceDataLiteral(sw, c, f.Value)
|
||||
sw.Do(", ", nil)
|
||||
}
|
||||
sw.Do("}", targs)
|
||||
case validators.SliceLiteral:
|
||||
sw.Do("[]", nil)
|
||||
targs := generator.Args{
|
||||
"type": c.Universe.Type(v.ElementType),
|
||||
}
|
||||
sw.Do("$.type|raw$", targs)
|
||||
if len(v.ElementTypeArgs) > 0 {
|
||||
sw.Do("[", nil)
|
||||
for i, typeArg := range v.ElementTypeArgs {
|
||||
if i > 0 {
|
||||
sw.Do(", ", nil)
|
||||
}
|
||||
sw.Do("$.|raw$", typeArg)
|
||||
}
|
||||
sw.Do("]", nil)
|
||||
}
|
||||
sw.Do("{\n", nil)
|
||||
for _, e := range v.Elements {
|
||||
toGolangSourceDataLiteral(sw, emitterContext{Context: c.Context, isElement: true}, e)
|
||||
sw.Do(",\n", nil)
|
||||
}
|
||||
sw.Do("}", nil)
|
||||
default:
|
||||
rv := reflect.ValueOf(value)
|
||||
switch rv.Kind() {
|
||||
case reflect.Slice, reflect.Array:
|
||||
case reflect.Array:
|
||||
arraySize := ""
|
||||
if rv.Kind() == reflect.Array {
|
||||
arraySize = strconv.Itoa(rv.Len())
|
||||
@@ -1472,6 +1536,37 @@ func toGolangSourceDataLiteral(sw *generator.SnippetWriter, c *generator.Context
|
||||
}
|
||||
}
|
||||
|
||||
func emitFunctionCall(sw *generator.SnippetWriter, c emitterContext, v validators.FunctionGen, leadingArgs ...string) {
|
||||
targs := generator.Args{
|
||||
"funcName": c.Universe.Type(v.Function),
|
||||
}
|
||||
sw.Do("$.funcName|raw$", targs)
|
||||
if typeArgs := v.TypeArgs; len(typeArgs) > 0 {
|
||||
sw.Do("[", nil)
|
||||
for i, typeArg := range typeArgs {
|
||||
sw.Do("$.|raw$", c.Universe.Type(typeArg))
|
||||
if i < len(typeArgs)-1 {
|
||||
sw.Do(",", nil)
|
||||
}
|
||||
}
|
||||
sw.Do("]", nil)
|
||||
}
|
||||
sw.Do("(", nil)
|
||||
if len(leadingArgs) > 0 {
|
||||
sw.Do(strings.Join(leadingArgs, ", "), nil)
|
||||
}
|
||||
if len(leadingArgs) > 0 && len(v.Args) > 0 {
|
||||
sw.Do(", ", nil)
|
||||
}
|
||||
for i, arg := range v.Args {
|
||||
if i != 0 {
|
||||
sw.Do(", ", nil)
|
||||
}
|
||||
toGolangSourceDataLiteral(sw, c, arg)
|
||||
}
|
||||
sw.Do(")", nil)
|
||||
}
|
||||
|
||||
// getLeafTypeAndPrefixes returns the "leaf value type" for a given type, as
|
||||
// well as type and expression prefix strings for the input type. The type
|
||||
// prefix can be prepended to the given type's name to produce the nilable form
|
||||
|
||||
@@ -81,7 +81,7 @@ func init() {
|
||||
}
|
||||
|
||||
// This applies to all tags in this file.
|
||||
var listTagsValidScopes = sets.New(ScopeAny)
|
||||
var listTagsValidScopes = sets.New(ScopeType, ScopeField, ScopeListVal, ScopeMapKey, ScopeMapVal)
|
||||
|
||||
// listMetadata collects information about a single list with map or set semantics.
|
||||
type listMetadata struct {
|
||||
|
||||
@@ -26,22 +26,55 @@ import (
|
||||
"k8s.io/apimachinery/pkg/util/sets"
|
||||
"k8s.io/code-generator/cmd/validation-gen/util"
|
||||
"k8s.io/gengo/v2/codetags"
|
||||
"k8s.io/gengo/v2/generator"
|
||||
"k8s.io/gengo/v2/types"
|
||||
)
|
||||
|
||||
const enumTagName = "k8s:enum"
|
||||
const (
|
||||
enumTagName = "k8s:enum"
|
||||
enumExcludeTagName = "k8s:enumExclude"
|
||||
)
|
||||
|
||||
func init() {
|
||||
RegisterTagValidator(&enumTagValidator{})
|
||||
RegisterTagValidator(&enumExcludeTagValidator{})
|
||||
}
|
||||
|
||||
type enumExcludeTagValidator struct {
|
||||
}
|
||||
|
||||
func (*enumExcludeTagValidator) Init(_ Config) {
|
||||
}
|
||||
|
||||
func (*enumExcludeTagValidator) TagName() string {
|
||||
return enumExcludeTagName
|
||||
}
|
||||
|
||||
var enumExcludeValidScope = sets.New(ScopeConst)
|
||||
|
||||
func (*enumExcludeTagValidator) ValidScopes() sets.Set[Scope] {
|
||||
return enumExcludeValidScope
|
||||
}
|
||||
|
||||
func (*enumExcludeTagValidator) GetValidations(_ Context, _ codetags.Tag) (Validations, error) {
|
||||
return Validations{}, nil
|
||||
}
|
||||
|
||||
func (eetv *enumExcludeTagValidator) Docs() TagDoc {
|
||||
return TagDoc{
|
||||
Tag: eetv.TagName(),
|
||||
Scopes: eetv.ValidScopes().UnsortedList(),
|
||||
Description: `Indicates that an constant value is not part of an enum, even if the constant's type is tagged with k8s:enum.
|
||||
May be conditionally excluded via +k8s:ifEnabled(Option)=+k8s:enumExclude or +k8s:ifDisabled(Option)=+k8s:enumExclude.
|
||||
If multiple +k8s:ifEnabled/+k8s:ifDisabled tags are used, the value is excluded if any of the exclude conditions are met.`,
|
||||
}
|
||||
}
|
||||
|
||||
type enumTagValidator struct {
|
||||
enumContext *enumContext
|
||||
validator Validator
|
||||
}
|
||||
|
||||
func (etv *enumTagValidator) Init(cfg Config) {
|
||||
etv.enumContext = newEnumContext(cfg.GengoContext)
|
||||
etv.validator = cfg.Validator
|
||||
}
|
||||
|
||||
func (enumTagValidator) TagName() string {
|
||||
@@ -55,29 +88,104 @@ func (enumTagValidator) ValidScopes() sets.Set[Scope] {
|
||||
}
|
||||
|
||||
var (
|
||||
enumValidator = types.Name{Package: libValidationPkg, Name: "Enum"}
|
||||
enumValidator = types.Name{Package: libValidationPkg, Name: "Enum"}
|
||||
enumExclusionType = types.Name{Package: libValidationPkg, Name: "EnumExclusion"}
|
||||
setsNew = types.Name{Package: "k8s.io/apimachinery/pkg/util/sets", Name: "New"}
|
||||
)
|
||||
|
||||
var setsNew = types.Name{Package: "k8s.io/apimachinery/pkg/util/sets", Name: "New"}
|
||||
|
||||
func (etv *enumTagValidator) GetValidations(context Context, _ codetags.Tag) (Validations, error) {
|
||||
// NOTE: typedefs to pointers are not supported, so we should never see a pointer here.
|
||||
if t := util.NativeType(context.Type); t != types.String {
|
||||
return Validations{}, fmt.Errorf("can only be used on string types (%s)", rootTypeString(context.Type, t))
|
||||
}
|
||||
|
||||
enum := &enumType{Name: context.Type.Name}
|
||||
for _, c := range context.Constants {
|
||||
var exclusions []enumExclude
|
||||
isExcluded := false
|
||||
for _, tag := range c.Tags {
|
||||
switch tag.Name {
|
||||
case enumExcludeTagName:
|
||||
isExcluded = true
|
||||
case ifEnabledTag, ifDisabledTag:
|
||||
if tag.ValueTag != nil && tag.ValueTag.Name == enumExcludeTagName {
|
||||
if option, ok := tag.PositionalArg(); ok {
|
||||
exclusions = append(exclusions, enumExclude{
|
||||
excludeWhen: tag.Name == ifEnabledTag,
|
||||
option: option.Value,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if isExcluded {
|
||||
continue
|
||||
}
|
||||
|
||||
value := &enumValue{
|
||||
Name: c.Constant.Name,
|
||||
Value: *c.Constant.ConstValue,
|
||||
Comment: strings.Join(c.Constant.CommentLines, " "),
|
||||
Exclusions: exclusions,
|
||||
}
|
||||
enum.addIfNotPresent(value)
|
||||
}
|
||||
|
||||
// Sort the values for the codegen that happens later.
|
||||
slices.SortFunc(enum.Values, func(a, b *enumValue) int {
|
||||
return cmp.Compare(a.Name.Name, b.Name.Name)
|
||||
})
|
||||
for _, v := range enum.Values {
|
||||
slices.SortFunc(v.Exclusions, func(a, b enumExclude) int {
|
||||
if a.excludeWhen == b.excludeWhen {
|
||||
return cmp.Compare(a.option, b.option)
|
||||
}
|
||||
if a.excludeWhen {
|
||||
return 1
|
||||
}
|
||||
return -1
|
||||
})
|
||||
}
|
||||
|
||||
var result Validations
|
||||
|
||||
if enum, ok := etv.enumContext.EnumType(context.Type); ok {
|
||||
// TODO: Avoid the "local" here. This was added to avoid errors caused when the package is an empty string.
|
||||
// The correct package would be the output package but is not known here. This does not show up in generated code.
|
||||
// TODO: Append a consistent hash suffix to avoid generated name conflicts?
|
||||
supportVarName := PrivateVar{Name: "SymbolsFor" + context.Type.Name.Name, Package: "local"}
|
||||
supportVar := Variable(supportVarName, Function(enumTagName, DefaultFlags, setsNew, enum.ValueArgs()...).WithTypeArgs(enum.Name))
|
||||
result.AddVariable(supportVar)
|
||||
fn := Function(enumTagName, DefaultFlags, enumValidator, supportVarName)
|
||||
result.AddFunction(fn)
|
||||
// TODO: Avoid the "local" here. This was added to avoid errors caused when the package is an empty string.
|
||||
// The correct package would be the output package but is not known here. This does not show up in generated code.
|
||||
// TODO: Append a consistent hash suffix to avoid generated name conflicts?
|
||||
symbolsVarName := PrivateVar{Name: "SymbolsFor" + context.Type.Name.Name, Package: "local"}
|
||||
var allValues []any
|
||||
var exclusionRules []any
|
||||
for _, v := range enum.Values {
|
||||
allValues = append(allValues, Identifier(v.Name))
|
||||
for _, exclusion := range v.Exclusions {
|
||||
exclusionRules = append(exclusionRules, StructLiteral{
|
||||
Type: enumExclusionType,
|
||||
TypeArgs: []*types.Type{context.Type},
|
||||
Fields: []StructLiteralField{
|
||||
{"Value", Identifier(v.Name)},
|
||||
{"Option", exclusion.option},
|
||||
{"ExcludeWhen", exclusion.excludeWhen},
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
initFn := Function("setsNew", DefaultFlags, setsNew, allValues...)
|
||||
if len(allValues) == 0 {
|
||||
initFn = initFn.WithTypeArgs(enum.Name)
|
||||
}
|
||||
result.AddVariable(Variable(symbolsVarName, initFn))
|
||||
var exclusions any = Literal("nil")
|
||||
if len(exclusionRules) > 0 {
|
||||
exclusionsVar := PrivateVar{Name: "ExclusionsFor" + context.Type.Name.Name, Package: "local"}
|
||||
result.AddVariable(Variable(exclusionsVar, SliceLiteral{
|
||||
ElementType: enumExclusionType,
|
||||
ElementTypeArgs: []*types.Type{context.Type},
|
||||
Elements: exclusionRules,
|
||||
}))
|
||||
exclusions = exclusionsVar
|
||||
}
|
||||
fn := Function(enumTagName, DefaultFlags, enumValidator, symbolsVarName, exclusions)
|
||||
result.AddFunction(fn)
|
||||
|
||||
return result, nil
|
||||
}
|
||||
@@ -86,7 +194,7 @@ func (etv *enumTagValidator) Docs() TagDoc {
|
||||
return TagDoc{
|
||||
Tag: etv.TagName(),
|
||||
Scopes: etv.ValidScopes().UnsortedList(),
|
||||
Description: "Indicates that a string type is an enum. All const values of this type are considered values in the enum.",
|
||||
Description: "Indicates that a string type is an enum. All constant values of this type are considered values in the enum unless excluded using +k8s:enumExclude.",
|
||||
}
|
||||
}
|
||||
|
||||
@@ -111,38 +219,25 @@ func (et *enumType) SymbolConstants() []Identifier {
|
||||
|
||||
// TODO: Everything below this comment is copied from kube-openapi's enum.go.
|
||||
|
||||
type enumValue struct {
|
||||
Name types.Name
|
||||
Value string
|
||||
Comment string
|
||||
}
|
||||
|
||||
type enumType struct {
|
||||
Name types.Name
|
||||
Values []*enumValue
|
||||
}
|
||||
|
||||
// enumMap is a map from the name to the matching enum type.
|
||||
type enumMap map[types.Name]*enumType
|
||||
|
||||
type enumContext struct {
|
||||
enumTypes enumMap
|
||||
type enumValue struct {
|
||||
Name types.Name
|
||||
Value string
|
||||
Comment string
|
||||
Exclusions []enumExclude
|
||||
}
|
||||
|
||||
func newEnumContext(c *generator.Context) *enumContext {
|
||||
return &enumContext{enumTypes: parseEnums(c)}
|
||||
}
|
||||
|
||||
// EnumType checks and finds the enumType for a given type.
|
||||
// If the given type is a known enum type, returns the enumType, true
|
||||
// Otherwise, returns nil, false
|
||||
func (ec *enumContext) EnumType(t *types.Type) (enum *enumType, isEnum bool) {
|
||||
// if t is a pointer, use its underlying type instead
|
||||
if t.Kind == types.Pointer {
|
||||
t = t.Elem
|
||||
}
|
||||
enum, ok := ec.enumTypes[t.Name]
|
||||
return enum, ok
|
||||
type enumExclude struct {
|
||||
// excludeWhen determines the condition for exclusion.
|
||||
// If true, the value is excluded if the option is present.
|
||||
// If false, the value is excluded if the option is NOT present.
|
||||
excludeWhen bool
|
||||
// option is the name of the feature option that controls the exclusion.
|
||||
option string
|
||||
}
|
||||
|
||||
// ValueStrings returns all possible values of the enum type as strings
|
||||
@@ -157,39 +252,6 @@ func (et *enumType) ValueStrings() []string {
|
||||
return values
|
||||
}
|
||||
|
||||
func parseEnums(c *generator.Context) enumMap {
|
||||
// find all enum types.
|
||||
enumTypes := make(enumMap)
|
||||
for _, p := range c.Universe {
|
||||
for _, t := range p.Types {
|
||||
if isEnumType(t) {
|
||||
if _, ok := enumTypes[t.Name]; !ok {
|
||||
enumTypes[t.Name] = &enumType{
|
||||
Name: t.Name,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// find all enum values from constants, and try to match each with its type.
|
||||
for _, p := range c.Universe {
|
||||
for _, c := range p.Constants {
|
||||
enumType := c.Underlying
|
||||
if _, ok := enumTypes[enumType.Name]; ok {
|
||||
value := &enumValue{
|
||||
Name: c.Name,
|
||||
Value: *c.ConstValue,
|
||||
Comment: strings.Join(c.CommentLines, " "),
|
||||
}
|
||||
enumTypes[enumType.Name].addIfNotPresent(value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return enumTypes
|
||||
}
|
||||
|
||||
func (et *enumType) addIfNotPresent(value *enumValue) {
|
||||
// If we already have an enum case with the same value, then ignore this new
|
||||
// one. This can happen if an enum aliases one from another package and
|
||||
@@ -207,14 +269,3 @@ func (et *enumType) addIfNotPresent(value *enumValue) {
|
||||
}
|
||||
et.Values = append(et.Values, value)
|
||||
}
|
||||
|
||||
// isEnumType checks if a given type is an enum by the definition
|
||||
// An enum type should be an alias of string and has tag '+enum' in its comment.
|
||||
// Additionally, pass the type of builtin 'string' to check against.
|
||||
func isEnumType(t *types.Type) bool {
|
||||
return t.Kind == types.Alias && t.Underlying == types.String && hasEnumTag(t)
|
||||
}
|
||||
|
||||
func hasEnumTag(t *types.Type) bool {
|
||||
return codetags.Extract("+", t.CommentLines)[enumTagName] != nil
|
||||
}
|
||||
|
||||
@@ -42,7 +42,7 @@ func (neqTagValidator) TagName() string {
|
||||
return neqTagName
|
||||
}
|
||||
|
||||
var neqTagValidScopes = sets.New(ScopeAny)
|
||||
var neqTagValidScopes = sets.New(ScopeType, ScopeField, ScopeListVal, ScopeMapKey, ScopeMapVal)
|
||||
|
||||
func (neqTagValidator) ValidScopes() sets.Set[Scope] {
|
||||
return neqTagValidScopes
|
||||
|
||||
@@ -58,7 +58,7 @@ func (itemTagValidator) TagName() string {
|
||||
return itemTagName
|
||||
}
|
||||
|
||||
var itemTagValidScopes = sets.New(ScopeAny)
|
||||
var itemTagValidScopes = sets.New(ScopeType, ScopeField, ScopeListVal, ScopeMapKey, ScopeMapVal)
|
||||
|
||||
func (itemTagValidator) ValidScopes() sets.Set[Scope] {
|
||||
return itemTagValidScopes
|
||||
|
||||
@@ -42,9 +42,7 @@ func (minimumTagValidator) TagName() string {
|
||||
return minimumTagName
|
||||
}
|
||||
|
||||
var minimumTagValidScopes = sets.New(
|
||||
ScopeAny,
|
||||
)
|
||||
var minimumTagValidScopes = sets.New(ScopeType, ScopeField, ScopeListVal, ScopeMapKey, ScopeMapVal)
|
||||
|
||||
func (minimumTagValidator) ValidScopes() sets.Set[Scope] {
|
||||
return minimumTagValidScopes
|
||||
|
||||
@@ -38,7 +38,7 @@ func (opaqueTypeTagValidator) TagName() string {
|
||||
}
|
||||
|
||||
func (opaqueTypeTagValidator) ValidScopes() sets.Set[Scope] {
|
||||
return sets.New(ScopeAny)
|
||||
return sets.New(ScopeType, ScopeField, ScopeListVal, ScopeMapKey, ScopeMapVal)
|
||||
}
|
||||
|
||||
func (opaqueTypeTagValidator) GetValidations(_ Context, _ codetags.Tag) (Validations, error) {
|
||||
|
||||
@@ -0,0 +1,108 @@
|
||||
/*
|
||||
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 validators
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"k8s.io/apimachinery/pkg/util/sets"
|
||||
"k8s.io/gengo/v2/codetags"
|
||||
"k8s.io/gengo/v2/types"
|
||||
)
|
||||
|
||||
const (
|
||||
ifEnabledTag = "k8s:ifEnabled"
|
||||
ifDisabledTag = "k8s:ifDisabled"
|
||||
)
|
||||
|
||||
func init() {
|
||||
RegisterTagValidator(&ifTagValidator{true, nil})
|
||||
RegisterTagValidator(&ifTagValidator{false, nil})
|
||||
}
|
||||
|
||||
type ifTagValidator struct {
|
||||
enabled bool
|
||||
validator Validator
|
||||
}
|
||||
|
||||
func (itv *ifTagValidator) Init(cfg Config) {
|
||||
itv.validator = cfg.Validator
|
||||
}
|
||||
|
||||
func (itv ifTagValidator) TagName() string {
|
||||
if itv.enabled {
|
||||
return ifEnabledTag
|
||||
}
|
||||
return ifDisabledTag
|
||||
}
|
||||
|
||||
var ifEnabledDisabledTagValidScopes = sets.New(ScopeType, ScopeField, ScopeListVal, ScopeMapKey, ScopeMapVal, ScopeConst)
|
||||
|
||||
func (ifTagValidator) ValidScopes() sets.Set[Scope] {
|
||||
return ifEnabledDisabledTagValidScopes
|
||||
}
|
||||
|
||||
var (
|
||||
ifOption = types.Name{Package: libValidationPkg, Name: "IfOption"}
|
||||
)
|
||||
|
||||
func (itv ifTagValidator) GetValidations(context Context, tag codetags.Tag) (Validations, error) {
|
||||
optionArg, ok := tag.PositionalArg()
|
||||
if !ok {
|
||||
return Validations{}, fmt.Errorf("missing required option name positional argument")
|
||||
}
|
||||
result := Validations{}
|
||||
if validations, err := itv.validator.ExtractValidations(context, *tag.ValueTag); err != nil {
|
||||
return Validations{}, err
|
||||
} else {
|
||||
for _, fn := range validations.Functions {
|
||||
f := Function(itv.TagName(), fn.Flags, ifOption, optionArg.Value, itv.enabled, WrapperFunction{Function: fn, ObjType: context.Type})
|
||||
result.Variables = append(result.Variables, validations.Variables...)
|
||||
result.AddFunction(f)
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
}
|
||||
|
||||
func (itv ifTagValidator) Docs() TagDoc {
|
||||
doc := TagDoc{
|
||||
Tag: itv.TagName(),
|
||||
Args: []TagArgDoc{{
|
||||
Description: "<option>",
|
||||
Type: codetags.ArgTypeString,
|
||||
Required: true,
|
||||
}},
|
||||
Scopes: itv.ValidScopes().UnsortedList(),
|
||||
}
|
||||
|
||||
doc.PayloadsType = codetags.ValueTypeTag
|
||||
doc.PayloadsRequired = true
|
||||
if itv.enabled {
|
||||
doc.Description = "Declares a validation that only applies when an option is enabled."
|
||||
doc.Payloads = []TagPayloadDoc{{
|
||||
Description: "<validation-tag>",
|
||||
Docs: "This validation tag will be evaluated only if the validation option is enabled.",
|
||||
}}
|
||||
} else {
|
||||
doc.Description = "Declares a validation that only applies when an option is disabled."
|
||||
doc.Payloads = []TagPayloadDoc{{
|
||||
Description: "<validation-tag>",
|
||||
Docs: "This validation tag will be evaluated only if the validation option is disabled.",
|
||||
}}
|
||||
}
|
||||
return doc
|
||||
}
|
||||
@@ -152,7 +152,8 @@ func (reg *registry) ExtractValidations(context Context, tags ...codetags.Tag) (
|
||||
for _, tags := range phases {
|
||||
for _, tag := range tags {
|
||||
tv := reg.tagValidators[tag.Name]
|
||||
if scopes := tv.ValidScopes(); !scopes.Has(context.Scope) && !scopes.Has(ScopeAny) {
|
||||
// At this point we know tv exists and is not nil due to the upfront check
|
||||
if scopes := tv.ValidScopes(); !scopes.Has(context.Scope) {
|
||||
return Validations{}, fmt.Errorf("tag %q cannot be specified on %s", tv.TagName(), context.Scope)
|
||||
}
|
||||
if err := typeCheck(tag, tv.Docs()); err != nil {
|
||||
@@ -195,28 +196,24 @@ func (reg *registry) ExtractValidations(context Context, tags ...codetags.Tag) (
|
||||
|
||||
func (reg *registry) sortTagsIntoPhases(tags []codetags.Tag) [][]codetags.Tag {
|
||||
// First sort all tags by their name, so the final output is deterministic.
|
||||
// It is important to do this before validations are generated.
|
||||
//
|
||||
// It makes more sense to sort here, rather than when emitting because:
|
||||
// Some tags are "meta" tags which wrap other tags. For example:
|
||||
//
|
||||
// Consider a type or field with the following comments:
|
||||
// // +k8s:validateFalse="111"
|
||||
// // +k8s:validateFalse="222"
|
||||
// // +k8s:ifEnabled(Foo)=+k8s:validateFalse="333"
|
||||
//
|
||||
// // +k8s:validateFalse="111"
|
||||
// // +k8s:validateFalse="222"
|
||||
// // +k8s:ifOptionEnabled(Foo)=+k8s:validateFalse="333"
|
||||
// Tag extraction will group these by tag name. The first two are
|
||||
// instances of "k8s:validateFalse", while the third is an instance of
|
||||
// "k8s:ifEnabled".
|
||||
//
|
||||
// Tag extraction will retain the relative order between 111 and 222, but
|
||||
// 333 is extracted as tag "k8s:ifOptionEnabled". Those are all in a map,
|
||||
// which we iterate (in a random order). When it reaches the emit stage,
|
||||
// the "ifOptionEnabled" part is gone, and we will have 3 FunctionGen
|
||||
// objects, all with tag "k8s:validateFalse", in a non-deterministic order
|
||||
// because of the map iteration. If we sort them at that point, we won't
|
||||
// have enough information to do something smart, unless we look at the
|
||||
// args, which are opaque to us.
|
||||
//
|
||||
// Sorting it earlier means we can sort "k8s:ifOptionEnabled" against
|
||||
// "k8s:validateFalse". All of the records within each of those is
|
||||
// relatively ordered, so the result here would be to put "ifOptionEnabled"
|
||||
// before "validateFalse" (lexicographical is better than random).
|
||||
// Without sorting, the order in which tag validators are called is not defined
|
||||
// (map iteration). This can lead to non-deterministic order of the generated
|
||||
// validations. By sorting the tags by name first, we ensure that "k8s:ifEnabled"
|
||||
// is processed before or after "k8s:validateFalse" consistently, allowing the
|
||||
// "k8s:validateFalse" tags to remain grouped together. The tags for each name
|
||||
// are processed in order of appearance, so relative ordering is preserved.
|
||||
sortedTags := make([]codetags.Tag, len(tags))
|
||||
copy(sortedTags, tags)
|
||||
slices.SortFunc(sortedTags, func(a, b codetags.Tag) int {
|
||||
|
||||
@@ -45,7 +45,7 @@ func (subfieldTagValidator) TagName() string {
|
||||
return subfieldTagName
|
||||
}
|
||||
|
||||
var subfieldTagValidScopes = sets.New(ScopeAny)
|
||||
var subfieldTagValidScopes = sets.New(ScopeType, ScopeField, ScopeListVal, ScopeMapKey, ScopeMapVal)
|
||||
|
||||
func (subfieldTagValidator) ValidScopes() sets.Set[Scope] {
|
||||
return subfieldTagValidScopes
|
||||
|
||||
@@ -56,7 +56,7 @@ func (frtv fixedResultTagValidator) TagName() string {
|
||||
return validateFalseTagName
|
||||
}
|
||||
|
||||
var fixedResultTagValidScopes = sets.New(ScopeAny)
|
||||
var fixedResultTagValidScopes = sets.New(ScopeType, ScopeField, ScopeListVal, ScopeMapKey, ScopeMapVal)
|
||||
|
||||
func (fixedResultTagValidator) ValidScopes() sets.Set[Scope] {
|
||||
return fixedResultTagValidScopes
|
||||
|
||||
@@ -152,11 +152,6 @@ type Scope string
|
||||
// Note: All of these values should be strings which can be used in an error
|
||||
// message such as "may not be used in %s".
|
||||
const (
|
||||
// ScopeAny indicates that a validator may be use in any context. This value
|
||||
// should never appear in a Context struct, since that indicates a
|
||||
// specific use.
|
||||
ScopeAny Scope = "anywhere"
|
||||
|
||||
// ScopeType indicates a validation on a type definition, which applies to
|
||||
// all instances of that type.
|
||||
ScopeType Scope = "type definitions"
|
||||
@@ -177,6 +172,9 @@ const (
|
||||
// field or type.
|
||||
ScopeMapVal Scope = "map values"
|
||||
|
||||
// ScopeConst indicates a validation which applies to constant values only.
|
||||
ScopeConst Scope = "constant values"
|
||||
|
||||
// TODO: It's not clear if we need to distinguish (e.g.) list values of
|
||||
// fields from list values of typedefs. We could make {type,field} be
|
||||
// orthogonal to {scalar, list, list-value, map, map-key, map-value} (and
|
||||
@@ -195,22 +193,58 @@ type Context struct {
|
||||
// this is the field's type (which may be a pointer, an alias, or both).
|
||||
// When Scope indicates a list-value, map-key, or map-value, this is the
|
||||
// type of that key or value (which, again, may be a pointer, and alias, or
|
||||
// both).
|
||||
// both). When Scope is ScopeConst this is the constant's type.
|
||||
Type *types.Type
|
||||
|
||||
// ParentPath provides the field path to the parent type or field, enabling
|
||||
// unique identification of validation contexts for the same type in
|
||||
// different locations.
|
||||
ParentPath *field.Path
|
||||
// Path provides a path to the type or field being validated. This is
|
||||
// useful for identifying an exact context, e.g. to track information
|
||||
// between related tags. When Scope is ScopeType, this is the Go package
|
||||
// path and type name (e.g. "k8s.io/api/core/v1.Pod"). When Scope is
|
||||
// ScopeField, this is the field path (e.g. "spec.containers[*].image").
|
||||
// When Scope indicates a list-value, map-key, or map-value, this is the
|
||||
// type or field path, as described above, with a suffix indicating
|
||||
// that it refers to the keys or values. For ScopeConst, this will be nil.
|
||||
Path *field.Path
|
||||
|
||||
// Member provides details about a field within a struct when Scope is
|
||||
// ScopeField. For all other values of Scope, this will be nil.
|
||||
Member *types.Member
|
||||
|
||||
// Path provides the field path to the type or field being validated. This
|
||||
// is useful for identifying an exact context, e.g. to track information
|
||||
// between related tags.
|
||||
Path *field.Path
|
||||
// ListSelector provides a list of key-value pairs that represent criteria
|
||||
// for selecting one or more items from a list. When Scope is
|
||||
// ScopeListVal, this will be non-nil. An empty selector means that
|
||||
// all items in the list should be selected. For all other values of
|
||||
// Scope, this will be nil.
|
||||
ListSelector []ListSelectorTerm
|
||||
|
||||
// ParentPath provides a path to the parent type or field of the object
|
||||
// being validated, when applicable. enabling unique identification of
|
||||
// validation contexts for the same type in different locations. When
|
||||
// Scope is ScopeField, this is the path to the containing struct type or
|
||||
// field (depending on where the validation tag was sepcified). When Scope
|
||||
// indicates a list-value, map-key, or map-value, this is the path to the
|
||||
// list or map type or field (depending on where the validation tag was
|
||||
// specified). When Scope is ScopeType, this is nil.
|
||||
ParentPath *field.Path
|
||||
|
||||
// Constants provides access to all constants of the type being
|
||||
// validated. Only set when Scope is ScopeType.
|
||||
Constants []*Constant
|
||||
}
|
||||
|
||||
// Constant represents a constant value.
|
||||
type Constant struct {
|
||||
Constant *types.Type
|
||||
Tags []codetags.Tag
|
||||
}
|
||||
|
||||
// ListSelectorTerm represents a field name and value pair.
|
||||
type ListSelectorTerm struct {
|
||||
// Field is the JSON name of the field to match.
|
||||
Field string
|
||||
// Value is the value to match. This must be a primitive type which can
|
||||
// be used as list-map keys: string, int, or bool.
|
||||
Value any
|
||||
}
|
||||
|
||||
// TagDoc describes a comment-tag and its usage.
|
||||
@@ -461,11 +495,11 @@ func (fg FunctionGen) WithComment(comment string) FunctionGen {
|
||||
return fg
|
||||
}
|
||||
|
||||
// Variable creates a VariableGen for a given function name and extraArgs.
|
||||
func Variable(variable PrivateVar, initFunc FunctionGen) VariableGen {
|
||||
// Variable creates a VariableGen for a given variable name and init value.
|
||||
func Variable(variable PrivateVar, initializer any) VariableGen {
|
||||
return VariableGen{
|
||||
Variable: variable,
|
||||
InitFunc: initFunc,
|
||||
Variable: variable,
|
||||
Initializer: initializer,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -473,8 +507,9 @@ type VariableGen struct {
|
||||
// Variable holds the variable identifier.
|
||||
Variable PrivateVar
|
||||
|
||||
// InitFunc describes the function call that the variable is assigned to.
|
||||
InitFunc FunctionGen
|
||||
// Initializer is the value to initialize the variable with.
|
||||
// Initializer may be any function call or literal type supported by toGolangSourceDataLiteral.
|
||||
Initializer any
|
||||
}
|
||||
|
||||
// WrapperFunction describes a function literal which has the fingerprint of a
|
||||
@@ -499,6 +534,31 @@ type FunctionLiteral struct {
|
||||
Body string
|
||||
}
|
||||
|
||||
// StructLiteral represents a struct literal expression that can be used as
|
||||
// an argument to a validator.
|
||||
type StructLiteral struct {
|
||||
// Type is the type of the struct literal to be generated.
|
||||
Type types.Name
|
||||
// TypeArgs are the generic type arguments for the struct type.
|
||||
TypeArgs []*types.Type
|
||||
Fields []StructLiteralField
|
||||
}
|
||||
|
||||
// SliceLiteral represents a slice literal expression that can be used as
|
||||
// an argument to a validator.
|
||||
type SliceLiteral struct {
|
||||
// ElementType is the type of the elements in the slice.
|
||||
ElementType types.Name
|
||||
// ElementTypeArgs are the generic type arguments for the element type.
|
||||
ElementTypeArgs []*types.Type
|
||||
Elements []any
|
||||
}
|
||||
|
||||
type StructLiteralField struct {
|
||||
Name string
|
||||
Value any
|
||||
}
|
||||
|
||||
// ParamResult represents a parameter or a result of a function.
|
||||
type ParamResult struct {
|
||||
Name string
|
||||
|
||||
Reference in New Issue
Block a user