mirror of
https://github.com/k3s-io/kubernetes.git
synced 2026-07-28 01:34:19 +00:00
Add +k8s:enumExclude and options support to +k8s:enum
This commit is contained in:
@@ -28,17 +28,12 @@ import (
|
||||
// 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, values sets.Set[T], exclusions []EnumExclusion[T]) field.ErrorList {
|
||||
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 !values.Has(*value) {
|
||||
return field.ErrorList{field.NotSupported[T](fldPath, *value, supportedValues(op, values, exclusions))}
|
||||
}
|
||||
for _, rule := range exclusions {
|
||||
if rule.ExcludeWhenEnabled == op.HasOption(rule.Option) && *value == rule.Value {
|
||||
return field.ErrorList{field.NotSupported[T](fldPath, *value, supportedValues(op, values, exclusions))}
|
||||
}
|
||||
if !validValues.Has(*value) || isExcluded(op, exclusions, *value) {
|
||||
return field.ErrorList{field.NotSupported[T](fldPath, *value, supportedValues(op, validValues, exclusions))}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -66,17 +61,17 @@ func NewExclusions[T ~string](rules ...EnumExclusion[T]) []EnumExclusion[T] {
|
||||
type EnumExclusion[T ~string] struct {
|
||||
// Value specifies the enum value to be conditionally excluded.
|
||||
Value T
|
||||
// ExcludeWhenEnabled determines the condition for exclusion.
|
||||
// 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.
|
||||
ExcludeWhenEnabled bool
|
||||
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.ExcludeWhenEnabled == op.HasOption(rule.Option) {
|
||||
if rule.Value == value && rule.ExcludeWhen == op.HasOption(rule.Option) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,98 @@ func (enumTagValidator) ValidScopes() sets.Set[Scope] {
|
||||
}
|
||||
|
||||
var (
|
||||
enumValidator = types.Name{Package: libValidationPkg, Name: "Enum"}
|
||||
enumValidator = types.Name{Package: libValidationPkg, Name: "Enum"}
|
||||
newExclusions = types.Name{Package: libValidationPkg, Name: "NewExclusions"}
|
||||
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
|
||||
break
|
||||
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},
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
result.AddVariable(Variable(symbolsVarName, Function("setsNew", DefaultFlags, setsNew, allValues...).WithTypeArgs(enum.Name)))
|
||||
var exclusions any = Literal("nil")
|
||||
if len(exclusionRules) > 0 {
|
||||
exclusionsVar := PrivateVar{Name: "ExclusionsFor" + context.Type.Name.Name, Package: "local"}
|
||||
result.AddVariable(Variable(exclusionsVar, Function("newExclusions", DefaultFlags, newExclusions, exclusionRules...).WithTypeArgs(enum.Name)))
|
||||
exclusions = exclusionsVar
|
||||
}
|
||||
fn := Function(enumTagName, DefaultFlags, enumValidator, symbolsVarName, exclusions)
|
||||
result.AddFunction(fn)
|
||||
|
||||
return result, nil
|
||||
}
|
||||
@@ -86,7 +188,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 +213,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 +246,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 +263,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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user