Merge pull request #133768 from jpbetz/dv-options

Add  +k8s:ifEnabled, +k8s:ifDisabled and +k8s:enumExclude tags
This commit is contained in:
Kubernetes Prow Robot
2025-09-02 21:41:13 -07:00
committed by GitHub
24 changed files with 1544 additions and 272 deletions

View File

@@ -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
}

View File

@@ -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())
}
}
})
}
}

View 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
}

View File

@@ -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}),
})
}

View File

@@ -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"
)

View File

@@ -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()
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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"`
}

View File

@@ -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"},
})
}

View File

@@ -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
}

View File

@@ -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()

View File

@@ -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

View File

@@ -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 {

View File

@@ -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
}

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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) {

View File

@@ -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
}

View File

@@ -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 {

View File

@@ -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

View File

@@ -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

View File

@@ -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