Skip to content

✨ Add support for k8s:enum tag. #1250

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 1 commit into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions pkg/crd/markers/validation.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,9 @@ const (

ValidationExactlyOneOfPrefix = validationPrefix + "ExactlyOneOf"
ValidationAtMostOneOfPrefix = validationPrefix + "AtMostOneOf"

// K8sEnumTag indicates that the given type is an enum; all const values of this type are considered values in the enum
K8sEnumTag = "k8s:enum"
)

// ValidationMarkers lists all available markers that affect CRD schema generation,
Expand Down Expand Up @@ -84,6 +87,8 @@ var TypeOnlyMarkers = []*definitionWithHelp{
WithHelp(markers.SimpleHelp("CRD validation", "specifies a list of field names that must conform to the AtMostOneOf constraint.")),
must(markers.MakeDefinition(ValidationExactlyOneOfPrefix, markers.DescribesType, ExactlyOneOf(nil))).
WithHelp(markers.SimpleHelp("CRD validation", "specifies a list of field names that must conform to the ExactlyOneOf constraint.")),
must(markers.MakeDefinition(K8sEnumTag, markers.DescribesType, struct{}{})).
WithHelp(markers.SimpleHelp("CRD", "indicates that the given type is an enum; all const values of this type are considered values in the enum")),
}

// FieldOnlyMarkers list field-specific validation markers (i.e. those markers that don't make
Expand Down
10 changes: 10 additions & 0 deletions pkg/crd/parser_integration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,16 @@ var _ = Describe("CRD Generation From Parsing to CustomResourceDefinition", func
})
})

Context("Enum API", func() {
BeforeEach(func() {
pkgPaths = []string{"./enum/..."}
expPkgLen = 1
})
It("should successfully generate the CRD with enum validation constraints", func() {
assertCRD(pkgs[0], "Enum", "testdata.kubebuilder.io_enums.yaml")
})
})

Context("OneOf API with invalid marker", func() {
BeforeEach(func() {
pkgPaths = []string{"./oneof_error/..."}
Expand Down
58 changes: 58 additions & 0 deletions pkg/crd/schema.go
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,9 @@ func (c *schemaContext) requestSchema(pkgPath, typeName string) {

// infoToSchema creates a schema for the type in the given set of type information.
func infoToSchema(ctx *schemaContext) *apiext.JSONSchemaProps {
if ctx.info.Markers.Get(crdmarkers.K8sEnumTag) != nil {
return enumToSchema(ctx)
}
if obj := ctx.pkg.Types.Scope().Lookup(ctx.info.Name); obj != nil {
switch {
// If the obj implements a JSON marshaler and has a marker, use the
Expand All @@ -139,6 +142,61 @@ func infoToSchema(ctx *schemaContext) *apiext.JSONSchemaProps {
return typeToSchema(ctx, ctx.info.RawSpec.Type)
}

func enumToSchema(ctx *schemaContext) *apiext.JSONSchemaProps {
rawType := ctx.info.RawSpec.Type
typeDef := ctx.pkg.TypesInfo.Defs[ctx.info.RawSpec.Name]
if typeDef == nil {
ctx.pkg.AddError(loader.ErrFromNode(fmt.Errorf("unknown enum type %s", ctx.info.Name), rawType))
return &apiext.JSONSchemaProps{}
}
typeInfo := typeDef.Type()
if basicInfo, isBasic := typeInfo.Underlying().(*types.Basic); !isBasic || basicInfo.Info()&types.IsString == 0 {
ctx.pkg.AddError(loader.ErrFromNode(fmt.Errorf("enum type must be a string, not %s", typeInfo.String()), rawType))
return &apiext.JSONSchemaProps{}
}

var enumValues []apiext.JSON
for _, file := range ctx.pkg.Syntax {
for _, decl := range file.Decls {
genDecl, ok := decl.(*ast.GenDecl)
if !ok || genDecl.Tok != token.CONST {
continue
}
for _, spec := range genDecl.Specs {
valueSpec, ok := spec.(*ast.ValueSpec)
if !ok {
continue
}
for i, name := range valueSpec.Names {
obj := ctx.pkg.TypesInfo.Defs[name]
if obj == nil || obj.Type() != typeInfo {
continue
}
val := valueSpec.Values[i]
basicLit, ok := val.(*ast.BasicLit)
if !ok || basicLit.Kind != token.STRING {
continue
}
// trim quotes
value := basicLit.Value[1 : len(basicLit.Value)-1]
enumValues = append(enumValues, apiext.JSON{Raw: []byte(`"` + value + `"`)})
}
}
}
}

sort.Slice(enumValues, func(i, j int) bool {
return string(enumValues[i].Raw) < string(enumValues[j].Raw)
})

schema := &apiext.JSONSchemaProps{
Type: "string",
Enum: enumValues,
}
applyMarkers(ctx, ctx.info.Markers, schema, rawType)
return schema
}

type schemaMarkerWithName struct {
SchemaMarker SchemaMarker
Name string
Expand Down
23 changes: 23 additions & 0 deletions pkg/crd/testdata/cronjob_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,14 @@ type CronJobSpec struct {
// +optional
ConcurrencyPolicy ConcurrencyPolicy `json:"concurrencyPolicy,omitempty"`

// Specifies how to treat concurrent executions of a Job.
// Valid values are:
// - "Allow" (default): allows CronJobs to run concurrently;
// - "Forbid": forbids concurrent runs, skipping next run if previous run hasn't finished yet;
// - "Replace": cancels currently running job and replaces it with a new one
// +optional
K8sConcurrencyPolicy K8sConcurrencyPolicy `json:"k8sConcurrencyPolicy,omitempty"`

// This flag tells the controller to suspend subsequent executions, it does
// not apply to already started executions. Defaults to false.
// +optional
Expand Down Expand Up @@ -696,6 +704,21 @@ const (
ReplaceConcurrent ConcurrencyPolicy = "Replace"
)

// +k8s:enum
type K8sConcurrencyPolicy string

const (
// AllowK8sConcurrencyPolicy allows CronJobs to run concurrently.
AllowK8sConcurrencyPolicy K8sConcurrencyPolicy = "Allow"

// ForbidK8sConcurrencyPolicy forbids concurrent runs, skipping next run if previous
// hasn't finished yet.
ForbidK8sConcurrencyPolicy K8sConcurrencyPolicy = "Forbid"

// ReplaceK8sConcurrencyPolicy cancels currently running job and replaces it with a new one.
ReplaceK8sConcurrencyPolicy K8sConcurrencyPolicy = "Replace"
)

// StringEvenType is a type that includes an expression-based validation.
// +kubebuilder:validation:XValidation:rule="self.size() % 2 == 0",message="must have even length"
type StringEvenType string
Expand Down
55 changes: 55 additions & 0 deletions pkg/crd/testdata/enum/api.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
/*
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.
*/

// +groupName=testdata.kubebuilder.io
// +versionName=v1
package enum

import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)

// +k8s:enum
type EnumType string

const (
Value1 EnumType = "Value1"
Value2 EnumType = "Value2"
)

// +kubebuilder:object:root=true

// Enum is a test CRD that contains an enum.
type Enum struct {
metav1.TypeMeta `json:",inline"`
metav1.ObjectMeta `json:"metadata,omitempty"`

Spec EnumSpec `json:"spec,omitempty"`
}

// EnumSpec defines the desired state of Enum
type EnumSpec struct {
Field EnumType `json:"field,omitempty"`
}

// +kubebuilder:object:root=true

// EnumList contains a list of Enum
type EnumList struct {
metav1.TypeMeta `json:",inline"`
metav1.ListMeta `json:"metadata,omitempty"`
Items []Enum `json:"items"`
}
12 changes: 12 additions & 0 deletions pkg/crd/testdata/testdata.kubebuilder.io_cronjobs.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,18 @@ spec:
- Forbid
- Replace
type: string
k8sConcurrencyPolicy:
description: |-
Specifies how to treat concurrent executions of a Job.
Valid values are:
- "Allow" (default): allows CronJobs to run concurrently;
- "Forbid": forbids concurrent runs, skipping next run if previous run hasn't finished yet;
- "Replace": cancels currently running job and replaces it with a new one
enum:
- Allow
- Forbid
- Replace
type: string
defaultedEmptyMap:
additionalProperties:
type: string
Expand Down
47 changes: 47 additions & 0 deletions pkg/crd/testdata/testdata.kubebuilder.io_enums.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
name: enums.testdata.kubebuilder.io
spec:
group: testdata.kubebuilder.io
names:
kind: Enum
listKind: EnumList
plural: enums
singular: enum
scope: Namespaced
versions:
- name: v1
schema:
openAPIV3Schema:
description: Enum is a test CRD that contains an enum.
properties:
apiVersion:
description: |-
APIVersion defines the versioned schema of this representation of an object.
Servers should convert recognized schemas to the latest internal value, and
may reject unrecognized values.
More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources
type: string
kind:
description: |-
Kind is a string value representing the REST resource this object represents.
Servers may infer this from the endpoint the client submits requests to.
Cannot be updated.
In CamelCase.
More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds
type: string
metadata:
type: object
spec:
description: EnumSpec defines the desired state of Enum
properties:
field:
enum:
- Value1
- Value2
type: string
type: object
type: object
served: true
storage: true
Loading