Use apiextension v1

- upgrade from apiextension v1beta1 to v1
 - generate yaml manifest for crd intead of applying it at runtime
  - users will have to apply the manifest with kubectl
 - kg and kgctl log an error if the crd is not present
 - now validation should actually work

Signed-off-by: leonnicolas <leonloechner@gmx.de>
This commit is contained in:
leonnicolas
2021-06-14 09:08:46 +02:00
parent e272d725a5
commit 36643b77b4
584 changed files with 50911 additions and 55838 deletions

122
vendor/sigs.k8s.io/controller-tools/pkg/crd/conv.go generated vendored Normal file
View File

@@ -0,0 +1,122 @@
package crd
import (
"fmt"
apiextinternal "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions"
apiext "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
apiextv1beta1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1"
"k8s.io/apimachinery/pkg/api/equality"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
)
var (
conversionScheme = runtime.NewScheme()
)
func init() {
if err := apiextinternal.AddToScheme(conversionScheme); err != nil {
panic("must be able to add internal apiextensions to the CRD conversion Scheme")
}
if err := apiext.AddToScheme(conversionScheme); err != nil {
panic("must be able to add apiextensions/v1 to the CRD conversion Scheme")
}
if err := apiextv1beta1.AddToScheme(conversionScheme); err != nil {
panic("must be able to add apiextensions/v1beta1 to the CRD conversion Scheme")
}
}
// AsVersion converts a CRD from the canonical internal form (currently v1) to some external form.
func AsVersion(original apiext.CustomResourceDefinition, gv schema.GroupVersion) (runtime.Object, error) {
// We can use the internal versions an existing conversions from kubernetes, since they're not in k/k itself.
// This punts the problem of conversion down the road for a future maintainer (or future instance of @directxman12)
// when we have to support older versions that get removed, or when API machinery decides to yell at us for this
// questionable decision.
intVer, err := conversionScheme.ConvertToVersion(&original, apiextinternal.SchemeGroupVersion)
if err != nil {
return nil, fmt.Errorf("unable to convert to internal CRD version: %w", err)
}
return conversionScheme.ConvertToVersion(intVer, gv)
}
// mergeIdenticalSubresources checks to see if subresources are identical across
// all versions, and if so, merges them into a top-level version.
//
// This assumes you're not using trivial versions.
func mergeIdenticalSubresources(crd *apiextv1beta1.CustomResourceDefinition) {
subres := crd.Spec.Versions[0].Subresources
for _, ver := range crd.Spec.Versions {
if ver.Subresources == nil || !equality.Semantic.DeepEqual(subres, ver.Subresources) {
// either all nil, or not identical
return
}
}
// things are identical if we've gotten this far, so move the subresources up
// and discard the identical per-version ones
crd.Spec.Subresources = subres
for i := range crd.Spec.Versions {
crd.Spec.Versions[i].Subresources = nil
}
}
// mergeIdenticalSchemata checks to see if schemata are identical across
// all versions, and if so, merges them into a top-level version.
//
// This assumes you're not using trivial versions.
func mergeIdenticalSchemata(crd *apiextv1beta1.CustomResourceDefinition) {
schema := crd.Spec.Versions[0].Schema
for _, ver := range crd.Spec.Versions {
if ver.Schema == nil || !equality.Semantic.DeepEqual(schema, ver.Schema) {
// either all nil, or not identical
return
}
}
// things are identical if we've gotten this far, so move the schemata up
// to a single schema and discard the identical per-version ones
crd.Spec.Validation = schema
for i := range crd.Spec.Versions {
crd.Spec.Versions[i].Schema = nil
}
}
// mergeIdenticalPrinterColumns checks to see if schemata are identical across
// all versions, and if so, merges them into a top-level version.
//
// This assumes you're not using trivial versions.
func mergeIdenticalPrinterColumns(crd *apiextv1beta1.CustomResourceDefinition) {
cols := crd.Spec.Versions[0].AdditionalPrinterColumns
for _, ver := range crd.Spec.Versions {
if len(ver.AdditionalPrinterColumns) == 0 || !equality.Semantic.DeepEqual(cols, ver.AdditionalPrinterColumns) {
// either all nil, or not identical
return
}
}
// things are identical if we've gotten this far, so move the printer columns up
// and discard the identical per-version ones
crd.Spec.AdditionalPrinterColumns = cols
for i := range crd.Spec.Versions {
crd.Spec.Versions[i].AdditionalPrinterColumns = nil
}
}
// MergeIdenticalVersionInfo makes sure that components of the Versions field that are identical
// across all versions get merged into the top-level fields in v1beta1.
//
// This is required by the Kubernetes API server validation.
//
// The reason is that a v1beta1 -> v1 -> v1beta1 conversion cycle would need to
// round-trip identically, v1 doesn't have top-level subresources, and without
// this restriction it would be ambiguous how a v1-with-identical-subresources
// converts into a v1beta1).
func MergeIdenticalVersionInfo(crd *apiextv1beta1.CustomResourceDefinition) {
if len(crd.Spec.Versions) > 0 {
mergeIdenticalSubresources(crd)
mergeIdenticalSchemata(crd)
mergeIdenticalPrinterColumns(crd)
}
}

View File

@@ -0,0 +1,78 @@
/*
Copyright 2019 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 crd
import (
"strings"
"unicode"
apiext "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
)
// TruncateDescription truncates the description of fields in given schema if it
// exceeds maxLen.
// It tries to chop off the description at the closest sentence boundary.
func TruncateDescription(schema *apiext.JSONSchemaProps, maxLen int) {
EditSchema(schema, descVisitor{maxLen: maxLen})
}
// descVisitor recursively visits all fields in the schema and truncates the
// description of the fields to specified maxLen.
type descVisitor struct {
// maxLen is the maximum allowed length for decription of a field
maxLen int
}
func (v descVisitor) Visit(schema *apiext.JSONSchemaProps) SchemaVisitor {
if schema == nil {
return v
}
if v.maxLen < 0 {
return nil /* no further work to be done for this schema */
}
if v.maxLen == 0 {
schema.Description = ""
return v
}
if len(schema.Description) > v.maxLen {
schema.Description = truncateString(schema.Description, v.maxLen)
return v
}
return v
}
// truncateString truncates given desc string if it exceeds maxLen. It may
// return string with length less than maxLen even in cases where original desc
// exceeds maxLen because it tries to chop off the desc at the closest sentence
// boundary to avoid incomplete sentences.
func truncateString(desc string, maxLen int) string {
desc = desc[0:maxLen]
// Trying to chop off at closest sentence boundary.
if n := strings.LastIndexFunc(desc, isSentenceTerminal); n > 0 {
return desc[0 : n+1]
}
// TODO(droot): Improve the logic to chop off at closest word boundary
// or add elipses (...) to indicate that it's chopped incase no closest
// sentence found within maxLen.
return desc
}
// helper function to determine if given rune is a sentence terminal or not.
func isSentenceTerminal(r rune) bool {
return unicode.Is(unicode.STerm, r)
}

63
vendor/sigs.k8s.io/controller-tools/pkg/crd/doc.go generated vendored Normal file
View File

@@ -0,0 +1,63 @@
/*
Copyright 2019 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 crd contains utilities for generating CustomResourceDefinitions and
// their corresponding OpenAPI validation schemata.
//
// Markers
//
// Markers live under the markers subpackage. Two types of markers exist:
// those that modify schema generation (for validation), and those that modify
// the rest of the CRD. See the subpackage for more information and all
// supported markers.
//
// Collecting Types and Generating CRDs
//
// The Parser is the entrypoint for collecting the information required to
// generate CRDs. Like loader and collector, its methods are idemptotent, not
// doing extra work if called multiple times.
//
// Parser's method start with Need. Calling NeedXYZ indicates that XYZ should
// be made present in the eqivalent field in the Parser, where it can then be
// loaded from. Each Need method will in turn call Need on anything it needs.
//
// In general, root packages should first be loaded into the Parser with
// NeedPackage. Then, CRDs can be generated with NeedCRDFor.
//
// Errors are generally attached directly to the relevant Package with
// AddError.
//
// Known Packages
//
// There are a few types from Kubernetes that have special meaning, but don't
// have validation markers attached. Those specific types have overrides
// listed in KnownPackages that can be added as overrides to any parser.
//
// Flattening
//
// Once schemata are generated, they can be used directly by external tooling
// (like JSONSchema validators), but must first be "flattened" to not contain
// references before use in a CRD (Kubernetes doesn't allow references in the
// CRD's validation schema).
//
// The Flattener built in to the Parser takes care of flattening out references
// when requesting the CRDs, but can be invoked manually. It will not modify
// the input schemata.
//
// Flattened schemata may further be passed to FlattenEmbedded to remove the
// use of AllOf (which is used to describe embedded struct fields when
// references are in use). This done automatically when fetching CRDs.
package crd

441
vendor/sigs.k8s.io/controller-tools/pkg/crd/flatten.go generated vendored Normal file
View File

@@ -0,0 +1,441 @@
/*
Copyright 2019 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 crd
import (
"fmt"
"reflect"
"sort"
"strings"
"sync"
apiext "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
"sigs.k8s.io/controller-tools/pkg/loader"
)
// ErrorRecorder knows how to record errors. It wraps the part of
// pkg/loader.Package that we need to record errors in places were it might not
// make sense to have a loader.Package
type ErrorRecorder interface {
// AddError records that the given error occurred.
// See the documentation on loader.Package.AddError for more information.
AddError(error)
}
// isOrNil checks if val is nil if val is of a nillable type, otherwise,
// it compares val to valInt (which should probably be the zero value).
func isOrNil(val reflect.Value, valInt interface{}, zeroInt interface{}) bool {
switch valKind := val.Kind(); valKind {
case reflect.Chan, reflect.Func, reflect.Interface, reflect.Map, reflect.Ptr, reflect.Slice:
return val.IsNil()
default:
return valInt == zeroInt
}
}
// flattenAllOfInto copies properties from src to dst, then copies the properties
// of each item in src's allOf to dst's properties as well.
func flattenAllOfInto(dst *apiext.JSONSchemaProps, src apiext.JSONSchemaProps, errRec ErrorRecorder) {
if len(src.AllOf) > 0 {
for _, embedded := range src.AllOf {
flattenAllOfInto(dst, embedded, errRec)
}
}
dstVal := reflect.Indirect(reflect.ValueOf(dst))
srcVal := reflect.ValueOf(src)
typ := dstVal.Type()
srcRemainder := apiext.JSONSchemaProps{}
srcRemVal := reflect.Indirect(reflect.ValueOf(&srcRemainder))
dstRemainder := apiext.JSONSchemaProps{}
dstRemVal := reflect.Indirect(reflect.ValueOf(&dstRemainder))
hoisted := false
for i := 0; i < srcVal.NumField(); i++ {
fieldName := typ.Field(i).Name
switch fieldName {
case "AllOf":
// don't merge because we deal with it above
continue
case "Title", "Description", "Example", "ExternalDocs":
// don't merge because we pre-merge to properly preserve field docs
continue
}
srcField := srcVal.Field(i)
fldTyp := srcField.Type()
zeroVal := reflect.Zero(fldTyp)
zeroInt := zeroVal.Interface()
srcInt := srcField.Interface()
if isOrNil(srcField, srcInt, zeroInt) {
// nothing to copy from src, continue
continue
}
dstField := dstVal.Field(i)
dstInt := dstField.Interface()
if isOrNil(dstField, dstInt, zeroInt) {
// dst is empty, continue
dstField.Set(srcField)
continue
}
if fldTyp.Comparable() && srcInt == dstInt {
// same value, continue
continue
}
// resolve conflict
switch fieldName {
case "Properties":
// merge if possible, use all of otherwise
srcMap := srcInt.(map[string]apiext.JSONSchemaProps)
dstMap := dstInt.(map[string]apiext.JSONSchemaProps)
for k, v := range srcMap {
dstProp, exists := dstMap[k]
if !exists {
dstMap[k] = v
continue
}
flattenAllOfInto(&dstProp, v, errRec)
dstMap[k] = dstProp
}
case "Required":
// merge
dstField.Set(reflect.AppendSlice(dstField, srcField))
case "Type":
if srcInt != dstInt {
// TODO(directxman12): figure out how to attach this back to a useful point in the Go source or in the schema
errRec.AddError(fmt.Errorf("conflicting types in allOf branches in schema: %s vs %s", dstInt, srcInt))
}
// keep the destination value, for now
// TODO(directxman12): Default -- use field?
// TODO(directxman12):
// - Dependencies: if field x is present, then either schema validates or all props are present
// - AdditionalItems: like AdditionalProperties
// - Definitions: common named validation sets that can be references (merge, bail if duplicate)
case "AdditionalProperties":
// as of the time of writing, `allows: false` is not allowed, so we don't have to handle it
srcProps := srcInt.(*apiext.JSONSchemaPropsOrBool)
if srcProps.Schema == nil {
// nothing to merge
continue
}
dstProps := dstInt.(*apiext.JSONSchemaPropsOrBool)
if dstProps.Schema == nil {
dstProps.Schema = &apiext.JSONSchemaProps{}
}
flattenAllOfInto(dstProps.Schema, *srcProps.Schema, errRec)
// NB(directxman12): no need to explicitly handle nullable -- false is considered to be the zero value
// TODO(directxman12): src isn't necessarily the field value -- it's just the most recent allOf entry
default:
// hoist into allOf...
hoisted = true
srcRemVal.Field(i).Set(srcField)
dstRemVal.Field(i).Set(dstField)
// ...and clear the original
dstField.Set(zeroVal)
}
}
if hoisted {
dst.AllOf = append(dst.AllOf, dstRemainder, srcRemainder)
}
// dedup required
if len(dst.Required) > 0 {
reqUniq := make(map[string]struct{})
for _, req := range dst.Required {
reqUniq[req] = struct{}{}
}
dst.Required = make([]string, 0, len(reqUniq))
for req := range reqUniq {
dst.Required = append(dst.Required, req)
}
// be deterministic
sort.Strings(dst.Required)
}
}
// allOfVisitor recursively visits allOf fields in the schema,
// merging nested allOf properties into the root schema.
type allOfVisitor struct {
// errRec is used to record errors while flattening (like two conflicting
// field values used in an allOf)
errRec ErrorRecorder
}
func (v *allOfVisitor) Visit(schema *apiext.JSONSchemaProps) SchemaVisitor {
if schema == nil {
return v
}
// clear this now so that we can safely preserve edits made my flattenAllOfInto
origAllOf := schema.AllOf
schema.AllOf = nil
for _, embedded := range origAllOf {
flattenAllOfInto(schema, embedded, v.errRec)
}
return v
}
// NB(directxman12): FlattenEmbedded is separate from Flattener because
// some tooling wants to flatten out embedded fields, but only actually
// flatten a few specific types first.
// FlattenEmbedded flattens embedded fields (represented via AllOf) which have
// already had their references resolved into simple properties in the containing
// schema.
func FlattenEmbedded(schema *apiext.JSONSchemaProps, errRec ErrorRecorder) *apiext.JSONSchemaProps {
outSchema := schema.DeepCopy()
EditSchema(outSchema, &allOfVisitor{errRec: errRec})
return outSchema
}
// Flattener knows how to take a root type, and flatten all references in it
// into a single, flat type. Flattened types are cached, so it's relatively
// cheap to make repeated calls with the same type.
type Flattener struct {
// Parser is used to lookup package and type details, and parse in new packages.
Parser *Parser
LookupReference func(ref string, contextPkg *loader.Package) (TypeIdent, error)
// flattenedTypes hold the flattened version of each seen type for later reuse.
flattenedTypes map[TypeIdent]apiext.JSONSchemaProps
initOnce sync.Once
}
func (f *Flattener) init() {
f.initOnce.Do(func() {
f.flattenedTypes = make(map[TypeIdent]apiext.JSONSchemaProps)
if f.LookupReference == nil {
f.LookupReference = identFromRef
}
})
}
// cacheType saves the flattened version of the given type for later reuse
func (f *Flattener) cacheType(typ TypeIdent, schema apiext.JSONSchemaProps) {
f.init()
f.flattenedTypes[typ] = schema
}
// loadUnflattenedSchema fetches a fresh, unflattened schema from the parser.
func (f *Flattener) loadUnflattenedSchema(typ TypeIdent) (*apiext.JSONSchemaProps, error) {
f.Parser.NeedSchemaFor(typ)
baseSchema, found := f.Parser.Schemata[typ]
if !found {
return nil, fmt.Errorf("unable to locate schema for type %s", typ)
}
return &baseSchema, nil
}
// FlattenType flattens the given pre-loaded type, removing any references from it.
// It deep-copies the schema first, so it won't affect the parser's version of the schema.
func (f *Flattener) FlattenType(typ TypeIdent) *apiext.JSONSchemaProps {
f.init()
if cachedSchema, isCached := f.flattenedTypes[typ]; isCached {
return &cachedSchema
}
baseSchema, err := f.loadUnflattenedSchema(typ)
if err != nil {
typ.Package.AddError(err)
return nil
}
resSchema := f.FlattenSchema(*baseSchema, typ.Package)
f.cacheType(typ, *resSchema)
return resSchema
}
// FlattenSchema flattens the given schema, removing any references.
// It deep-copies the schema first, so the input schema won't be affected.
func (f *Flattener) FlattenSchema(baseSchema apiext.JSONSchemaProps, currentPackage *loader.Package) *apiext.JSONSchemaProps {
resSchema := baseSchema.DeepCopy()
EditSchema(resSchema, &flattenVisitor{
Flattener: f,
currentPackage: currentPackage,
})
return resSchema
}
// RefParts splits a reference produced by the schema generator into its component
// type name and package name (if it's a cross-package reference). Note that
// referenced packages *must* be looked up relative to the current package.
func RefParts(ref string) (typ string, pkgName string, err error) {
if !strings.HasPrefix(ref, defPrefix) {
return "", "", fmt.Errorf("non-standard reference link %q", ref)
}
ref = ref[len(defPrefix):]
// decode the json pointer encodings
ref = strings.Replace(ref, "~1", "/", -1)
ref = strings.Replace(ref, "~0", "~", -1)
nameParts := strings.SplitN(ref, "~", 2)
if len(nameParts) == 1 {
// local reference
return nameParts[0], "", nil
}
// cross-package reference
return nameParts[1], nameParts[0], nil
}
// identFromRef converts the given schema ref from the given package back
// into the TypeIdent that it represents.
func identFromRef(ref string, contextPkg *loader.Package) (TypeIdent, error) {
typ, pkgName, err := RefParts(ref)
if err != nil {
return TypeIdent{}, err
}
if pkgName == "" {
// a local reference
return TypeIdent{
Name: typ,
Package: contextPkg,
}, nil
}
// an external reference
return TypeIdent{
Name: typ,
Package: contextPkg.Imports()[pkgName],
}, nil
}
// preserveFields copies documentation fields from src into dst, preserving
// field-level documentation when flattening, and preserving field-level validation
// as allOf entries.
func preserveFields(dst *apiext.JSONSchemaProps, src apiext.JSONSchemaProps) {
srcDesc := src.Description
srcTitle := src.Title
srcExDoc := src.ExternalDocs
srcEx := src.Example
src.Description, src.Title, src.ExternalDocs, src.Example = "", "", nil, nil
src.Ref = nil
*dst = apiext.JSONSchemaProps{
AllOf: []apiext.JSONSchemaProps{*dst, src},
// keep these, in case the source field doesn't specify anything useful
Description: dst.Description,
Title: dst.Title,
ExternalDocs: dst.ExternalDocs,
Example: dst.Example,
}
if srcDesc != "" {
dst.Description = srcDesc
}
if srcTitle != "" {
dst.Title = srcTitle
}
if srcExDoc != nil {
dst.ExternalDocs = srcExDoc
}
if srcEx != nil {
dst.Example = srcEx
}
}
// flattenVisitor visits each node in the schema, recursively flattening references.
type flattenVisitor struct {
*Flattener
currentPackage *loader.Package
currentType *TypeIdent
currentSchema *apiext.JSONSchemaProps
originalField apiext.JSONSchemaProps
}
func (f *flattenVisitor) Visit(baseSchema *apiext.JSONSchemaProps) SchemaVisitor {
if baseSchema == nil {
// end-of-node marker, cache the results
if f.currentType != nil {
f.cacheType(*f.currentType, *f.currentSchema)
// preserve field information *after* caching so that we don't
// accidentally cache field-level information onto the schema for
// the type in general.
preserveFields(f.currentSchema, f.originalField)
}
return f
}
// if we get a type that's just a ref, resolve it
if baseSchema.Ref != nil && len(*baseSchema.Ref) > 0 {
// resolve this ref
refIdent, err := f.LookupReference(*baseSchema.Ref, f.currentPackage)
if err != nil {
f.currentPackage.AddError(err)
return nil
}
// load and potentially flatten the schema
// check the cache first...
if refSchemaCached, isCached := f.flattenedTypes[refIdent]; isCached {
// shallow copy is fine, it's just to avoid overwriting the doc fields
preserveFields(&refSchemaCached, *baseSchema)
*baseSchema = refSchemaCached
return nil // don't recurse, we're done
}
// ...otherwise, we need to flatten
refSchema, err := f.loadUnflattenedSchema(refIdent)
if err != nil {
f.currentPackage.AddError(err)
return nil
}
refSchema = refSchema.DeepCopy()
// keep field around to preserve field-level validation, docs, etc
origField := *baseSchema
*baseSchema = *refSchema
// avoid loops (which shouldn't exist, but just in case)
// by marking a nil cached pointer before we start recursing
f.cacheType(refIdent, apiext.JSONSchemaProps{})
return &flattenVisitor{
Flattener: f.Flattener,
currentPackage: refIdent.Package,
currentType: &refIdent,
currentSchema: baseSchema,
originalField: origField,
}
}
// otherwise, continue recursing...
if f.currentType != nil {
// ...but don't accidentally end this node early (for caching purposes)
return &flattenVisitor{
Flattener: f.Flattener,
currentPackage: f.currentPackage,
}
}
return f
}

416
vendor/sigs.k8s.io/controller-tools/pkg/crd/gen.go generated vendored Normal file
View File

@@ -0,0 +1,416 @@
/*
Copyright 2018 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 crd
import (
"fmt"
"go/ast"
"go/types"
"os"
apiext "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
apiextlegacy "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1"
"k8s.io/apimachinery/pkg/runtime/schema"
crdmarkers "sigs.k8s.io/controller-tools/pkg/crd/markers"
"sigs.k8s.io/controller-tools/pkg/genall"
"sigs.k8s.io/controller-tools/pkg/loader"
"sigs.k8s.io/controller-tools/pkg/markers"
"sigs.k8s.io/controller-tools/pkg/version"
)
// The default CustomResourceDefinition version to generate.
const defaultVersion = "v1"
// +controllertools:marker:generateHelp
// Generator generates CustomResourceDefinition objects.
type Generator struct {
// TrivialVersions indicates that we should produce a single-version CRD.
//
// Single "trivial-version" CRDs are compatible with older (pre 1.13)
// Kubernetes API servers. The storage version's schema will be used as
// the CRD's schema.
//
// Only works with the v1beta1 CRD version.
TrivialVersions bool `marker:",optional"`
// PreserveUnknownFields indicates whether or not we should turn off pruning.
//
// Left unspecified, it'll default to true when only a v1beta1 CRD is
// generated (to preserve compatibility with older versions of this tool),
// or false otherwise.
//
// It's required to be false for v1 CRDs.
PreserveUnknownFields *bool `marker:",optional"`
// AllowDangerousTypes allows types which are usually omitted from CRD generation
// because they are not recommended.
//
// Currently the following additional types are allowed when this is true:
// float32
// float64
//
// Left unspecified, the default is false
AllowDangerousTypes *bool `marker:",optional"`
// MaxDescLen specifies the maximum description length for fields in CRD's OpenAPI schema.
//
// 0 indicates drop the description for all fields completely.
// n indicates limit the description to at most n characters and truncate the description to
// closest sentence boundary if it exceeds n characters.
MaxDescLen *int `marker:",optional"`
// CRDVersions specifies the target API versions of the CRD type itself to
// generate. Defaults to v1.
//
// The first version listed will be assumed to be the "default" version and
// will not get a version suffix in the output filename.
//
// You'll need to use "v1" to get support for features like defaulting,
// along with an API server that supports it (Kubernetes 1.16+).
CRDVersions []string `marker:"crdVersions,optional"`
// GenerateEmbeddedObjectMeta specifies if any embedded ObjectMeta in the CRD should be generated
GenerateEmbeddedObjectMeta *bool `marker:",optional"`
}
func (Generator) CheckFilter() loader.NodeFilter {
return filterTypesForCRDs
}
func (Generator) RegisterMarkers(into *markers.Registry) error {
return crdmarkers.Register(into)
}
func (g Generator) Generate(ctx *genall.GenerationContext) error {
parser := &Parser{
Collector: ctx.Collector,
Checker: ctx.Checker,
// Perform defaulting here to avoid ambiguity later
AllowDangerousTypes: g.AllowDangerousTypes != nil && *g.AllowDangerousTypes == true,
// Indicates the parser on whether to register the ObjectMeta type or not
GenerateEmbeddedObjectMeta: g.GenerateEmbeddedObjectMeta != nil && *g.GenerateEmbeddedObjectMeta == true,
}
AddKnownTypes(parser)
for _, root := range ctx.Roots {
parser.NeedPackage(root)
}
metav1Pkg := FindMetav1(ctx.Roots)
if metav1Pkg == nil {
// no objects in the roots, since nothing imported metav1
return nil
}
// TODO: allow selecting a specific object
kubeKinds := FindKubeKinds(parser, metav1Pkg)
if len(kubeKinds) == 0 {
// no objects in the roots
return nil
}
crdVersions := g.CRDVersions
if len(crdVersions) == 0 {
crdVersions = []string{defaultVersion}
}
for groupKind := range kubeKinds {
parser.NeedCRDFor(groupKind, g.MaxDescLen)
crdRaw := parser.CustomResourceDefinitions[groupKind]
addAttribution(&crdRaw)
// Prevent the top level metadata for the CRD to be generate regardless of the intention in the arguments
FixTopLevelMetadata(crdRaw)
versionedCRDs := make([]interface{}, len(crdVersions))
for i, ver := range crdVersions {
conv, err := AsVersion(crdRaw, schema.GroupVersion{Group: apiext.SchemeGroupVersion.Group, Version: ver})
if err != nil {
return err
}
versionedCRDs[i] = conv
}
if g.TrivialVersions {
for i, crd := range versionedCRDs {
if crdVersions[i] == "v1beta1" {
toTrivialVersions(crd.(*apiextlegacy.CustomResourceDefinition))
}
}
}
// *If* we're only generating v1beta1 CRDs, default to `preserveUnknownFields: (unset)`
// for compatibility purposes. In any other case, default to false, since that's
// the sensible default and is required for v1.
v1beta1Only := len(crdVersions) == 1 && crdVersions[0] == "v1beta1"
switch {
case (g.PreserveUnknownFields == nil || *g.PreserveUnknownFields) && v1beta1Only:
crd := versionedCRDs[0].(*apiextlegacy.CustomResourceDefinition)
crd.Spec.PreserveUnknownFields = nil
case g.PreserveUnknownFields == nil, g.PreserveUnknownFields != nil && !*g.PreserveUnknownFields:
// it'll be false here (coming from v1) -- leave it as such
default:
return fmt.Errorf("you may only set PreserveUnknownFields to true with v1beta1 CRDs")
}
for i, crd := range versionedCRDs {
// defaults are not allowed to be specified in v1beta1 CRDs and
// decriptions are not allowed on the metadata regardless of version
// strip them before writing to a file
if crdVersions[i] == "v1beta1" {
removeDefaultsFromSchemas(crd.(*apiextlegacy.CustomResourceDefinition))
removeDescriptionFromMetadataLegacy(crd.(*apiextlegacy.CustomResourceDefinition))
} else {
removeDescriptionFromMetadata(crd.(*apiext.CustomResourceDefinition))
}
var fileName string
if i == 0 {
fileName = fmt.Sprintf("%s_%s.yaml", crdRaw.Spec.Group, crdRaw.Spec.Names.Plural)
} else {
fileName = fmt.Sprintf("%s_%s.%s.yaml", crdRaw.Spec.Group, crdRaw.Spec.Names.Plural, crdVersions[i])
}
if err := ctx.WriteYAML(fileName, crd); err != nil {
return err
}
}
}
return nil
}
func removeDescriptionFromMetadata(crd *apiext.CustomResourceDefinition) {
for _, versionSpec := range crd.Spec.Versions {
if versionSpec.Schema != nil {
removeDescriptionFromMetadataProps(versionSpec.Schema.OpenAPIV3Schema)
}
}
}
func removeDescriptionFromMetadataProps(v *apiext.JSONSchemaProps) {
if m, ok := v.Properties["metadata"]; ok {
meta := &m
if meta.Description != "" {
meta.Description = ""
v.Properties["metadata"] = m
}
}
}
func removeDescriptionFromMetadataLegacy(crd *apiextlegacy.CustomResourceDefinition) {
if crd.Spec.Validation != nil {
removeDescriptionFromMetadataPropsLegacy(crd.Spec.Validation.OpenAPIV3Schema)
}
for _, versionSpec := range crd.Spec.Versions {
if versionSpec.Schema != nil {
removeDescriptionFromMetadataPropsLegacy(versionSpec.Schema.OpenAPIV3Schema)
}
}
}
func removeDescriptionFromMetadataPropsLegacy(v *apiextlegacy.JSONSchemaProps) {
if m, ok := v.Properties["metadata"]; ok {
meta := &m
if meta.Description != "" {
meta.Description = ""
v.Properties["metadata"] = m
}
}
}
// removeDefaultsFromSchemas will remove all instances of default values being
// specified across all defined API versions
func removeDefaultsFromSchemas(crd *apiextlegacy.CustomResourceDefinition) {
if crd.Spec.Validation != nil {
removeDefaultsFromSchemaProps(crd.Spec.Validation.OpenAPIV3Schema)
}
for _, versionSpec := range crd.Spec.Versions {
if versionSpec.Schema != nil {
removeDefaultsFromSchemaProps(versionSpec.Schema.OpenAPIV3Schema)
}
}
}
// removeDefaultsFromSchemaProps will recurse into JSONSchemaProps to remove
// all instances of default values being specified
func removeDefaultsFromSchemaProps(v *apiextlegacy.JSONSchemaProps) {
if v == nil {
return
}
if v.Default != nil {
fmt.Fprintln(os.Stderr, "Warning: default unsupported in CRD version v1beta1, v1 required. Removing defaults.")
}
// nil-out the default field
v.Default = nil
for name, prop := range v.Properties {
// iter var reference is fine -- we handle the persistence of the modfications on the line below
//nolint:gosec
removeDefaultsFromSchemaProps(&prop)
v.Properties[name] = prop
}
if v.Items != nil {
removeDefaultsFromSchemaProps(v.Items.Schema)
for i := range v.Items.JSONSchemas {
props := v.Items.JSONSchemas[i]
removeDefaultsFromSchemaProps(&props)
v.Items.JSONSchemas[i] = props
}
}
}
// FixTopLevelMetadata resets the schema for the top-level metadata field which is needed for CRD validation
func FixTopLevelMetadata(crd apiext.CustomResourceDefinition) {
for _, v := range crd.Spec.Versions {
if v.Schema != nil && v.Schema.OpenAPIV3Schema != nil && v.Schema.OpenAPIV3Schema.Properties != nil {
schemaProperties := v.Schema.OpenAPIV3Schema.Properties
if _, ok := schemaProperties["metadata"]; ok {
schemaProperties["metadata"] = apiext.JSONSchemaProps{Type: "object"}
}
}
}
}
// toTrivialVersions strips out all schemata except for the storage schema,
// and moves that up into the root object. This makes the CRD compatible
// with pre 1.13 clusters.
func toTrivialVersions(crd *apiextlegacy.CustomResourceDefinition) {
var canonicalSchema *apiextlegacy.CustomResourceValidation
var canonicalSubresources *apiextlegacy.CustomResourceSubresources
var canonicalColumns []apiextlegacy.CustomResourceColumnDefinition
for i, ver := range crd.Spec.Versions {
if ver.Storage == true {
canonicalSchema = ver.Schema
canonicalSubresources = ver.Subresources
canonicalColumns = ver.AdditionalPrinterColumns
}
crd.Spec.Versions[i].Schema = nil
crd.Spec.Versions[i].Subresources = nil
crd.Spec.Versions[i].AdditionalPrinterColumns = nil
}
if canonicalSchema == nil {
return
}
crd.Spec.Validation = canonicalSchema
crd.Spec.Subresources = canonicalSubresources
crd.Spec.AdditionalPrinterColumns = canonicalColumns
}
// addAttribution adds attribution info to indicate controller-gen tool was used
// to generate this CRD definition along with the version info.
func addAttribution(crd *apiext.CustomResourceDefinition) {
if crd.ObjectMeta.Annotations == nil {
crd.ObjectMeta.Annotations = map[string]string{}
}
crd.ObjectMeta.Annotations["controller-gen.kubebuilder.io/version"] = version.Version()
}
// FindMetav1 locates the actual package representing metav1 amongst
// the imports of the roots.
func FindMetav1(roots []*loader.Package) *loader.Package {
for _, root := range roots {
pkg := root.Imports()["k8s.io/apimachinery/pkg/apis/meta/v1"]
if pkg != nil {
return pkg
}
}
return nil
}
// FindKubeKinds locates all types that contain TypeMeta and ObjectMeta
// (and thus may be a Kubernetes object), and returns the corresponding
// group-kinds.
func FindKubeKinds(parser *Parser, metav1Pkg *loader.Package) map[schema.GroupKind]struct{} {
// TODO(directxman12): technically, we should be finding metav1 per-package
kubeKinds := map[schema.GroupKind]struct{}{}
for typeIdent, info := range parser.Types {
hasObjectMeta := false
hasTypeMeta := false
pkg := typeIdent.Package
pkg.NeedTypesInfo()
typesInfo := pkg.TypesInfo
for _, field := range info.Fields {
if field.Name != "" {
// type and object meta are embedded,
// so they can't be this
continue
}
fieldType := typesInfo.TypeOf(field.RawField.Type)
namedField, isNamed := fieldType.(*types.Named)
if !isNamed {
// ObjectMeta and TypeMeta are named types
continue
}
if namedField.Obj().Pkg() == nil {
// Embedded non-builtin universe type (specifically, it's probably `error`),
// so it can't be ObjectMeta or TypeMeta
continue
}
fieldPkgPath := loader.NonVendorPath(namedField.Obj().Pkg().Path())
fieldPkg := pkg.Imports()[fieldPkgPath]
if fieldPkg != metav1Pkg {
continue
}
switch namedField.Obj().Name() {
case "ObjectMeta":
hasObjectMeta = true
case "TypeMeta":
hasTypeMeta = true
}
}
if !hasObjectMeta || !hasTypeMeta {
continue
}
groupKind := schema.GroupKind{
Group: parser.GroupVersions[pkg].Group,
Kind: typeIdent.Name,
}
kubeKinds[groupKind] = struct{}{}
}
return kubeKinds
}
// filterTypesForCRDs filters out all nodes that aren't used in CRD generation,
// like interfaces and struct fields without JSON tag.
func filterTypesForCRDs(node ast.Node) bool {
switch node := node.(type) {
case *ast.InterfaceType:
// skip interfaces, we never care about references in them
return false
case *ast.StructType:
return true
case *ast.Field:
_, hasTag := loader.ParseAstTag(node.Tag).Lookup("json")
// fields without JSON tags mean we have custom serialization,
// so only visit fields with tags.
return hasTag
default:
return true
}
}

View File

@@ -0,0 +1,179 @@
/*
Copyright 2019 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 crd
import (
apiext "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
"sigs.k8s.io/controller-tools/pkg/loader"
)
// KnownPackages overrides types in some comment packages that have custom validation
// but don't have validation markers on them (since they're from core Kubernetes).
var KnownPackages = map[string]PackageOverride{
"k8s.io/api/core/v1": func(p *Parser, pkg *loader.Package) {
// Explicit defaulting for the corev1.Protocol type in lieu of https://github.com/kubernetes/enhancements/pull/1928
p.Schemata[TypeIdent{Name: "Protocol", Package: pkg}] = apiext.JSONSchemaProps{
Type: "string",
Default: &apiext.JSON{Raw: []byte(`"TCP"`)},
}
p.AddPackage(pkg)
},
"k8s.io/apimachinery/pkg/apis/meta/v1": func(p *Parser, pkg *loader.Package) {
p.Schemata[TypeIdent{Name: "ObjectMeta", Package: pkg}] = apiext.JSONSchemaProps{
Type: "object",
}
p.Schemata[TypeIdent{Name: "Time", Package: pkg}] = apiext.JSONSchemaProps{
Type: "string",
Format: "date-time",
}
p.Schemata[TypeIdent{Name: "MicroTime", Package: pkg}] = apiext.JSONSchemaProps{
Type: "string",
Format: "date-time",
}
p.Schemata[TypeIdent{Name: "Duration", Package: pkg}] = apiext.JSONSchemaProps{
// TODO(directxman12): regexp validation for this (or get kube to support it as a format value)
Type: "string",
}
p.Schemata[TypeIdent{Name: "Fields", Package: pkg}] = apiext.JSONSchemaProps{
// this is a recursive structure that can't be flattened or, for that matter, properly generated.
// so just treat it as an arbitrary map
Type: "object",
AdditionalProperties: &apiext.JSONSchemaPropsOrBool{Allows: true},
}
p.AddPackage(pkg) // get the rest of the types
},
"k8s.io/apimachinery/pkg/api/resource": func(p *Parser, pkg *loader.Package) {
p.Schemata[TypeIdent{Name: "Quantity", Package: pkg}] = apiext.JSONSchemaProps{
// TODO(directxman12): regexp validation for this (or get kube to support it as a format value)
XIntOrString: true,
AnyOf: []apiext.JSONSchemaProps{
{Type: "integer"},
{Type: "string"},
},
Pattern: "^(\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))))?$",
}
// No point in calling AddPackage, this is the sole inhabitant
},
"k8s.io/apimachinery/pkg/runtime": func(p *Parser, pkg *loader.Package) {
p.Schemata[TypeIdent{Name: "RawExtension", Package: pkg}] = apiext.JSONSchemaProps{
// TODO(directxman12): regexp validation for this (or get kube to support it as a format value)
Type: "object",
}
p.AddPackage(pkg) // get the rest of the types
},
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured": func(p *Parser, pkg *loader.Package) {
p.Schemata[TypeIdent{Name: "Unstructured", Package: pkg}] = apiext.JSONSchemaProps{
Type: "object",
}
p.AddPackage(pkg) // get the rest of the types
},
"k8s.io/apimachinery/pkg/util/intstr": func(p *Parser, pkg *loader.Package) {
p.Schemata[TypeIdent{Name: "IntOrString", Package: pkg}] = apiext.JSONSchemaProps{
XIntOrString: true,
AnyOf: []apiext.JSONSchemaProps{
{Type: "integer"},
{Type: "string"},
},
}
// No point in calling AddPackage, this is the sole inhabitant
},
"k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1": func(p *Parser, pkg *loader.Package) {
p.Schemata[TypeIdent{Name: "JSON", Package: pkg}] = apiext.JSONSchemaProps{
XPreserveUnknownFields: boolPtr(true),
}
p.AddPackage(pkg) // get the rest of the types
},
"k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1": func(p *Parser, pkg *loader.Package) {
p.Schemata[TypeIdent{Name: "JSON", Package: pkg}] = apiext.JSONSchemaProps{
XPreserveUnknownFields: boolPtr(true),
}
p.AddPackage(pkg) // get the rest of the types
},
}
// ObjectMetaPackages overrides the ObjectMeta in all types
var ObjectMetaPackages = map[string]PackageOverride{
"k8s.io/apimachinery/pkg/apis/meta/v1": func(p *Parser, pkg *loader.Package) {
// execute the KnowPackages for `k8s.io/apimachinery/pkg/apis/meta/v1` if any
if f, ok := KnownPackages["k8s.io/apimachinery/pkg/apis/meta/v1"]; ok {
f(p, pkg)
}
// This is an allow-listed set of properties of ObjectMeta, other runtime properties are not part of this list
// See more discussion: https://github.com/kubernetes-sigs/controller-tools/pull/395#issuecomment-691919433
p.Schemata[TypeIdent{Name: "ObjectMeta", Package: pkg}] = apiext.JSONSchemaProps{
Type: "object",
Properties: map[string]apiext.JSONSchemaProps{
"name": {
Type: "string",
},
"namespace": {
Type: "string",
},
"annotations": {
Type: "object",
AdditionalProperties: &apiext.JSONSchemaPropsOrBool{
Schema: &apiext.JSONSchemaProps{
Type: "string",
},
},
},
"labels": {
Type: "object",
AdditionalProperties: &apiext.JSONSchemaPropsOrBool{
Schema: &apiext.JSONSchemaProps{
Type: "string",
},
},
},
"finalizers": {
Type: "array",
Items: &apiext.JSONSchemaPropsOrArray{
Schema: &apiext.JSONSchemaProps{
Type: "string",
},
},
},
},
}
},
}
func boolPtr(b bool) *bool {
return &b
}
// AddKnownTypes registers the packages overrides in KnownPackages with the given parser.
func AddKnownTypes(parser *Parser) {
// ensure everything is there before adding to PackageOverrides
// TODO(directxman12): this is a bit of a hack, maybe just use constructors?
parser.init()
for pkgName, override := range KnownPackages {
parser.PackageOverrides[pkgName] = override
}
// if we want to generate the embedded ObjectMeta in the CRD we need to add the ObjectMetaPackages
if parser.GenerateEmbeddedObjectMeta {
for pkgName, override := range ObjectMetaPackages {
parser.PackageOverrides[pkgName] = override
}
}
}

View File

@@ -0,0 +1,347 @@
/*
Copyright 2019 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 markers
import (
"fmt"
apiext "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
"sigs.k8s.io/controller-tools/pkg/markers"
)
// CRDMarkers lists all markers that directly modify the CRD (not validation
// schemas).
var CRDMarkers = []*definitionWithHelp{
// TODO(directxman12): more detailed help
must(markers.MakeDefinition("kubebuilder:subresource:status", markers.DescribesType, SubresourceStatus{})).
WithHelp(SubresourceStatus{}.Help()),
must(markers.MakeDefinition("kubebuilder:subresource:scale", markers.DescribesType, SubresourceScale{})).
WithHelp(SubresourceScale{}.Help()),
must(markers.MakeDefinition("kubebuilder:printcolumn", markers.DescribesType, PrintColumn{})).
WithHelp(PrintColumn{}.Help()),
must(markers.MakeDefinition("kubebuilder:resource", markers.DescribesType, Resource{})).
WithHelp(Resource{}.Help()),
must(markers.MakeDefinition("kubebuilder:storageversion", markers.DescribesType, StorageVersion{})).
WithHelp(StorageVersion{}.Help()),
must(markers.MakeDefinition("kubebuilder:skipversion", markers.DescribesType, SkipVersion{})).
WithHelp(SkipVersion{}.Help()),
must(markers.MakeDefinition("kubebuilder:unservedversion", markers.DescribesType, UnservedVersion{})).
WithHelp(UnservedVersion{}.Help()),
must(markers.MakeDefinition("kubebuilder:deprecatedversion", markers.DescribesType, DeprecatedVersion{})).
WithHelp(DeprecatedVersion{}.Help()),
}
// TODO: categories and singular used to be annotations types
// TODO: doc
func init() {
AllDefinitions = append(AllDefinitions, CRDMarkers...)
}
// +controllertools:marker:generateHelp:category=CRD
// SubresourceStatus enables the "/status" subresource on a CRD.
type SubresourceStatus struct{}
func (s SubresourceStatus) ApplyToCRD(crd *apiext.CustomResourceDefinitionSpec, version string) error {
var subresources *apiext.CustomResourceSubresources
for i := range crd.Versions {
ver := &crd.Versions[i]
if ver.Name != version {
continue
}
if ver.Subresources == nil {
ver.Subresources = &apiext.CustomResourceSubresources{}
}
subresources = ver.Subresources
break
}
if subresources == nil {
return fmt.Errorf("status subresource applied to version %q not in CRD", version)
}
subresources.Status = &apiext.CustomResourceSubresourceStatus{}
return nil
}
// +controllertools:marker:generateHelp:category=CRD
// SubresourceScale enables the "/scale" subresource on a CRD.
type SubresourceScale struct {
// marker names are leftover legacy cruft
// SpecPath specifies the jsonpath to the replicas field for the scale's spec.
SpecPath string `marker:"specpath"`
// StatusPath specifies the jsonpath to the replicas field for the scale's status.
StatusPath string `marker:"statuspath"`
// SelectorPath specifies the jsonpath to the pod label selector field for the scale's status.
//
// The selector field must be the *string* form (serialized form) of a selector.
// Setting a pod label selector is necessary for your type to work with the HorizontalPodAutoscaler.
SelectorPath *string `marker:"selectorpath"`
}
func (s SubresourceScale) ApplyToCRD(crd *apiext.CustomResourceDefinitionSpec, version string) error {
var subresources *apiext.CustomResourceSubresources
for i := range crd.Versions {
ver := &crd.Versions[i]
if ver.Name != version {
continue
}
if ver.Subresources == nil {
ver.Subresources = &apiext.CustomResourceSubresources{}
}
subresources = ver.Subresources
break
}
if subresources == nil {
return fmt.Errorf("scale subresource applied to version %q not in CRD", version)
}
subresources.Scale = &apiext.CustomResourceSubresourceScale{
SpecReplicasPath: s.SpecPath,
StatusReplicasPath: s.StatusPath,
LabelSelectorPath: s.SelectorPath,
}
return nil
}
// +controllertools:marker:generateHelp:category=CRD
// StorageVersion marks this version as the "storage version" for the CRD for conversion.
//
// When conversion is enabled for a CRD (i.e. it's not a trivial-versions/single-version CRD),
// one version is set as the "storage version" to be stored in etcd. Attempting to store any
// other version will result in conversion to the storage version via a conversion webhook.
type StorageVersion struct{}
func (s StorageVersion) ApplyToCRD(crd *apiext.CustomResourceDefinitionSpec, version string) error {
if version == "" {
// single-version, do nothing
return nil
}
// multi-version
for i := range crd.Versions {
ver := &crd.Versions[i]
if ver.Name != version {
continue
}
ver.Storage = true
break
}
return nil
}
// +controllertools:marker:generateHelp:category=CRD
// SkipVersion removes the particular version of the CRD from the CRDs spec.
//
// This is useful if you need to skip generating and listing version entries
// for 'internal' resource versions, which typically exist if using the
// Kubernetes upstream conversion-gen tool.
type SkipVersion struct{}
func (s SkipVersion) ApplyToCRD(crd *apiext.CustomResourceDefinitionSpec, version string) error {
if version == "" {
// single-version, this is an invalid state
return fmt.Errorf("cannot skip a version if there is only a single version")
}
var versions []apiext.CustomResourceDefinitionVersion
// multi-version
for i := range crd.Versions {
ver := crd.Versions[i]
if ver.Name == version {
// skip the skipped version
continue
}
versions = append(versions, ver)
}
crd.Versions = versions
return nil
}
// +controllertools:marker:generateHelp:category=CRD
// PrintColumn adds a column to "kubectl get" output for this CRD.
type PrintColumn struct {
// Name specifies the name of the column.
Name string
// Type indicates the type of the column.
//
// It may be any OpenAPI data type listed at
// https://github.com/OAI/OpenAPI-Specification/blob/master/versions/2.0.md#data-types.
Type string
// JSONPath specifies the jsonpath expression used to extract the value of the column.
JSONPath string `marker:"JSONPath"` // legacy cruft
// Description specifies the help/description for this column.
Description string `marker:",optional"`
// Format specifies the format of the column.
//
// It may be any OpenAPI data format corresponding to the type, listed at
// https://github.com/OAI/OpenAPI-Specification/blob/master/versions/2.0.md#data-types.
Format string `marker:",optional"`
// Priority indicates how important it is that this column be displayed.
//
// Lower priority (*higher* numbered) columns will be hidden if the terminal
// width is too small.
Priority int32 `marker:",optional"`
}
func (s PrintColumn) ApplyToCRD(crd *apiext.CustomResourceDefinitionSpec, version string) error {
var columns *[]apiext.CustomResourceColumnDefinition
for i := range crd.Versions {
ver := &crd.Versions[i]
if ver.Name != version {
continue
}
if ver.Subresources == nil {
ver.Subresources = &apiext.CustomResourceSubresources{}
}
columns = &ver.AdditionalPrinterColumns
break
}
if columns == nil {
return fmt.Errorf("printer columns applied to version %q not in CRD", version)
}
*columns = append(*columns, apiext.CustomResourceColumnDefinition{
Name: s.Name,
Type: s.Type,
JSONPath: s.JSONPath,
Description: s.Description,
Format: s.Format,
Priority: s.Priority,
})
return nil
}
// +controllertools:marker:generateHelp:category=CRD
// Resource configures naming and scope for a CRD.
type Resource struct {
// Path specifies the plural "resource" for this CRD.
//
// It generally corresponds to a plural, lower-cased version of the Kind.
// See https://book.kubebuilder.io/cronjob-tutorial/gvks.html.
Path string `marker:",optional"`
// ShortName specifies aliases for this CRD.
//
// Short names are often used when people have work with your resource
// over and over again. For instance, "rs" for "replicaset" or
// "crd" for customresourcedefinition.
ShortName []string `marker:",optional"`
// Categories specifies which group aliases this resource is part of.
//
// Group aliases are used to work with groups of resources at once.
// The most common one is "all" which covers about a third of the base
// resources in Kubernetes, and is generally used for "user-facing" resources.
Categories []string `marker:",optional"`
// Singular overrides the singular form of your resource.
//
// The singular form is otherwise defaulted off the plural (path).
Singular string `marker:",optional"`
// Scope overrides the scope of the CRD (Cluster vs Namespaced).
//
// Scope defaults to "Namespaced". Cluster-scoped ("Cluster") resources
// don't exist in namespaces.
Scope string `marker:",optional"`
}
func (s Resource) ApplyToCRD(crd *apiext.CustomResourceDefinitionSpec, version string) error {
if s.Path != "" {
crd.Names.Plural = s.Path
}
if s.Singular != "" {
crd.Names.Singular = s.Singular
}
crd.Names.ShortNames = s.ShortName
crd.Names.Categories = s.Categories
switch s.Scope {
case "":
crd.Scope = apiext.NamespaceScoped
default:
crd.Scope = apiext.ResourceScope(s.Scope)
}
return nil
}
// +controllertools:marker:generateHelp:category=CRD
// UnservedVersion does not serve this version.
//
// This is useful if you need to drop support for a version in favor of a newer version.
type UnservedVersion struct{}
func (s UnservedVersion) ApplyToCRD(crd *apiext.CustomResourceDefinitionSpec, version string) error {
for i := range crd.Versions {
ver := &crd.Versions[i]
if ver.Name != version {
continue
}
ver.Served = false
break
}
return nil
}
// NB(directxman12): singular was historically distinct, so we keep it here for backwards compat
// +controllertools:marker:generateHelp:category=CRD
// DeprecatedVersion marks this version as deprecated.
type DeprecatedVersion struct {
// Warning message to be shown on the deprecated version
Warning *string `marker:",optional"`
}
func (s DeprecatedVersion) ApplyToCRD(crd *apiext.CustomResourceDefinitionSpec, version string) error {
if version == "" {
// single-version, do nothing
return nil
}
// multi-version
for i := range crd.Versions {
ver := &crd.Versions[i]
if ver.Name != version {
continue
}
ver.Deprecated = true
ver.DeprecationWarning = s.Warning
break
}
return nil
}

View File

@@ -0,0 +1,46 @@
/*
Copyright 2019 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 markers defines markers for generating schema valiation
// and CRD structure.
//
// All markers related to CRD generation live in AllDefinitions.
//
// Validation Markers
//
// Validation markers have values that implement ApplyToSchema
// (crd.SchemaMarker). Any marker implementing this will automatically
// be run after the rest of a given schema node has been generated.
// Markers that need to be run before any other markers can also
// implement ApplyFirst, but this is discouraged and may change
// in the future.
//
// All validation markers start with "+kubebuilder:validation", and
// have the same name as their type name.
//
// CRD Markers
//
// Markers that modify anything in the CRD itself *except* for the schema
// implement ApplyToCRD (crd.CRDMarker). They are expected to detect whether
// they should apply themselves to a specific version in the CRD (as passed to
// them), or to the root-level CRD for legacy cases. They are applied *after*
// the rest of the CRD is computed.
//
// Misc
//
// This package also defines the "+groupName" and "+versionName" package-level
// markers, for defining package<->group-version mappings.
package markers

View File

@@ -0,0 +1,40 @@
/*
Copyright 2019 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 markers
import (
"sigs.k8s.io/controller-tools/pkg/markers"
)
func init() {
AllDefinitions = append(AllDefinitions,
must(markers.MakeDefinition("groupName", markers.DescribesPackage, "")).
WithHelp(markers.SimpleHelp("CRD", "specifies the API group name for this package.")),
must(markers.MakeDefinition("versionName", markers.DescribesPackage, "")).
WithHelp(markers.SimpleHelp("CRD", "overrides the API group version for this package (defaults to the package name).")),
must(markers.MakeDefinition("kubebuilder:validation:Optional", markers.DescribesPackage, struct{}{})).
WithHelp(markers.SimpleHelp("CRD validation", "specifies that all fields in this package are optional by default.")),
must(markers.MakeDefinition("kubebuilder:validation:Required", markers.DescribesPackage, struct{}{})).
WithHelp(markers.SimpleHelp("CRD validation", "specifies that all fields in this package are required by default.")),
must(markers.MakeDefinition("kubebuilder:skip", markers.DescribesPackage, struct{}{})).
WithHelp(markers.SimpleHelp("CRD", "don't consider this package as an API version.")),
)
}

View File

@@ -0,0 +1,83 @@
/*
Copyright 2019 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 markers
import (
"reflect"
"sigs.k8s.io/controller-tools/pkg/markers"
)
type definitionWithHelp struct {
*markers.Definition
Help *markers.DefinitionHelp
}
func (d *definitionWithHelp) WithHelp(help *markers.DefinitionHelp) *definitionWithHelp {
d.Help = help
return d
}
func (d *definitionWithHelp) Register(reg *markers.Registry) error {
if err := reg.Register(d.Definition); err != nil {
return err
}
if d.Help != nil {
reg.AddHelp(d.Definition, d.Help)
}
return nil
}
func must(def *markers.Definition, err error) *definitionWithHelp {
return &definitionWithHelp{
Definition: markers.Must(def, err),
}
}
// AllDefinitions contains all marker definitions for this package.
var AllDefinitions []*definitionWithHelp
type hasHelp interface {
Help() *markers.DefinitionHelp
}
// mustMakeAllWithPrefix converts each object into a marker definition using
// the object's type's with the prefix to form the marker name.
func mustMakeAllWithPrefix(prefix string, target markers.TargetType, objs ...interface{}) []*definitionWithHelp {
defs := make([]*definitionWithHelp, len(objs))
for i, obj := range objs {
name := prefix + ":" + reflect.TypeOf(obj).Name()
def, err := markers.MakeDefinition(name, target, obj)
if err != nil {
panic(err)
}
defs[i] = &definitionWithHelp{Definition: def, Help: obj.(hasHelp).Help()}
}
return defs
}
// Register registers all definitions for CRD generation to the given registry.
func Register(reg *markers.Registry) error {
for _, def := range AllDefinitions {
if err := def.Register(reg); err != nil {
return err
}
}
return nil
}

View File

@@ -0,0 +1,155 @@
/*
Copyright 2019 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 markers
import (
"fmt"
apiext "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
"sigs.k8s.io/controller-tools/pkg/markers"
)
// TopologyMarkers specify topology markers (i.e. markers that describe if a
// list behaves as an associative-list or a set, if a map is atomic or not).
var TopologyMarkers = []*definitionWithHelp{
must(markers.MakeDefinition("listMapKey", markers.DescribesField, ListMapKey(""))).
WithHelp(ListMapKey("").Help()),
must(markers.MakeDefinition("listType", markers.DescribesField, ListType(""))).
WithHelp(ListType("").Help()),
must(markers.MakeDefinition("mapType", markers.DescribesField, MapType(""))).
WithHelp(MapType("").Help()),
must(markers.MakeDefinition("structType", markers.DescribesField, StructType(""))).
WithHelp(StructType("").Help()),
}
func init() {
AllDefinitions = append(AllDefinitions, TopologyMarkers...)
}
// +controllertools:marker:generateHelp:category="CRD processing"
// ListType specifies the type of data-structure that the list
// represents (map, set, atomic).
//
// Possible data-structure types of a list are:
//
// - "map": it needs to have a key field, which will be used to build an
// associative list. A typical example is a the pod container list,
// which is indexed by the container name.
//
// - "set": Fields need to be "scalar", and there can be only one
// occurrence of each.
//
// - "atomic": All the fields in the list are treated as a single value,
// are typically manipulated together by the same actor.
type ListType string
// +controllertools:marker:generateHelp:category="CRD processing"
// ListMapKey specifies the keys to map listTypes.
//
// It indicates the index of a map list. They can be repeated if multiple keys
// must be used. It can only be used when ListType is set to map, and the keys
// should be scalar types.
type ListMapKey string
// +controllertools:marker:generateHelp:category="CRD processing"
// MapType specifies the level of atomicity of the map;
// i.e. whether each item in the map is independent of the others,
// or all fields are treated as a single unit.
//
// Possible values:
//
// - "granular": items in the map are independent of each other,
// and can be manipulated by different actors.
// This is the default behavior.
//
// - "atomic": all fields are treated as one unit.
// Any changes have to replace the entire map.
type MapType string
// +controllertools:marker:generateHelp:category="CRD processing"
// StructType specifies the level of atomicity of the struct;
// i.e. whether each field in the struct is independent of the others,
// or all fields are treated as a single unit.
//
// Possible values:
//
// - "granular": fields in the struct are independent of each other,
// and can be manipulated by different actors.
// This is the default behavior.
//
// - "atomic": all fields are treated as one unit.
// Any changes have to replace the entire struct.
type StructType string
func (l ListType) ApplyToSchema(schema *apiext.JSONSchemaProps) error {
if schema.Type != "array" {
return fmt.Errorf("must apply listType to an array")
}
if l != "map" && l != "atomic" && l != "set" {
return fmt.Errorf(`ListType must be either "map", "set" or "atomic"`)
}
p := string(l)
schema.XListType = &p
return nil
}
func (l ListType) ApplyFirst() {}
func (l ListMapKey) ApplyToSchema(schema *apiext.JSONSchemaProps) error {
if schema.Type != "array" {
return fmt.Errorf("must apply listMapKey to an array")
}
if schema.XListType == nil || *schema.XListType != "map" {
return fmt.Errorf("must apply listMapKey to an associative-list")
}
schema.XListMapKeys = append(schema.XListMapKeys, string(l))
return nil
}
func (m MapType) ApplyToSchema(schema *apiext.JSONSchemaProps) error {
if schema.Type != "object" {
return fmt.Errorf("must apply mapType to an object")
}
if m != "atomic" && m != "granular" {
return fmt.Errorf(`MapType must be either "granular" or "atomic"`)
}
p := string(m)
schema.XMapType = &p
return nil
}
func (s StructType) ApplyToSchema(schema *apiext.JSONSchemaProps) error {
if schema.Type != "object" && schema.Type != "" {
return fmt.Errorf("must apply structType to an object; either explicitly set or defaulted through an empty schema type")
}
if s != "atomic" && s != "granular" {
return fmt.Errorf(`StructType must be either "granular" or "atomic"`)
}
p := string(s)
schema.XMapType = &p
return nil
}

View File

@@ -0,0 +1,408 @@
/*
Copyright 2019 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 markers
import (
"fmt"
"encoding/json"
apiext "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
"sigs.k8s.io/controller-tools/pkg/markers"
)
const (
SchemalessName = "kubebuilder:validation:Schemaless"
)
// ValidationMarkers lists all available markers that affect CRD schema generation,
// except for the few that don't make sense as type-level markers (see FieldOnlyMarkers).
// All markers start with `+kubebuilder:validation:`, and continue with their type name.
// A copy is produced of all markers that describes types as well, for making types
// reusable and writing complex validations on slice items.
var ValidationMarkers = mustMakeAllWithPrefix("kubebuilder:validation", markers.DescribesField,
// integer markers
Maximum(0),
Minimum(0),
ExclusiveMaximum(false),
ExclusiveMinimum(false),
MultipleOf(0),
MinProperties(0),
MaxProperties(0),
// string markers
MaxLength(0),
MinLength(0),
Pattern(""),
// slice markers
MaxItems(0),
MinItems(0),
UniqueItems(false),
// general markers
Enum(nil),
Format(""),
Type(""),
XPreserveUnknownFields{},
XEmbeddedResource{},
)
// FieldOnlyMarkers list field-specific validation markers (i.e. those markers that don't make
// sense on a type, and thus aren't in ValidationMarkers).
var FieldOnlyMarkers = []*definitionWithHelp{
must(markers.MakeDefinition("kubebuilder:validation:Required", markers.DescribesField, struct{}{})).
WithHelp(markers.SimpleHelp("CRD validation", "specifies that this field is required, if fields are optional by default.")),
must(markers.MakeDefinition("kubebuilder:validation:Optional", markers.DescribesField, struct{}{})).
WithHelp(markers.SimpleHelp("CRD validation", "specifies that this field is optional, if fields are required by default.")),
must(markers.MakeDefinition("optional", markers.DescribesField, struct{}{})).
WithHelp(markers.SimpleHelp("CRD validation", "specifies that this field is optional, if fields are required by default.")),
must(markers.MakeDefinition("nullable", markers.DescribesField, Nullable{})).
WithHelp(Nullable{}.Help()),
must(markers.MakeAnyTypeDefinition("kubebuilder:default", markers.DescribesField, Default{})).
WithHelp(Default{}.Help()),
must(markers.MakeDefinition("kubebuilder:validation:EmbeddedResource", markers.DescribesField, XEmbeddedResource{})).
WithHelp(XEmbeddedResource{}.Help()),
must(markers.MakeDefinition(SchemalessName, markers.DescribesField, Schemaless{})).
WithHelp(Schemaless{}.Help()),
}
// ValidationIshMarkers are field-and-type markers that don't fall under the
// :validation: prefix, and/or don't have a name that directly matches their
// type.
var ValidationIshMarkers = []*definitionWithHelp{
must(markers.MakeDefinition("kubebuilder:pruning:PreserveUnknownFields", markers.DescribesField, XPreserveUnknownFields{})).
WithHelp(XPreserveUnknownFields{}.Help()),
must(markers.MakeDefinition("kubebuilder:pruning:PreserveUnknownFields", markers.DescribesType, XPreserveUnknownFields{})).
WithHelp(XPreserveUnknownFields{}.Help()),
}
func init() {
AllDefinitions = append(AllDefinitions, ValidationMarkers...)
for _, def := range ValidationMarkers {
newDef := *def.Definition
// copy both parts so we don't change the definition
typDef := definitionWithHelp{
Definition: &newDef,
Help: def.Help,
}
typDef.Target = markers.DescribesType
AllDefinitions = append(AllDefinitions, &typDef)
}
AllDefinitions = append(AllDefinitions, FieldOnlyMarkers...)
AllDefinitions = append(AllDefinitions, ValidationIshMarkers...)
}
// +controllertools:marker:generateHelp:category="CRD validation"
// Maximum specifies the maximum numeric value that this field can have.
type Maximum int
// +controllertools:marker:generateHelp:category="CRD validation"
// Minimum specifies the minimum numeric value that this field can have. Negative integers are supported.
type Minimum int
// +controllertools:marker:generateHelp:category="CRD validation"
// ExclusiveMinimum indicates that the minimum is "up to" but not including that value.
type ExclusiveMinimum bool
// +controllertools:marker:generateHelp:category="CRD validation"
// ExclusiveMaximum indicates that the maximum is "up to" but not including that value.
type ExclusiveMaximum bool
// +controllertools:marker:generateHelp:category="CRD validation"
// MultipleOf specifies that this field must have a numeric value that's a multiple of this one.
type MultipleOf int
// +controllertools:marker:generateHelp:category="CRD validation"
// MaxLength specifies the maximum length for this string.
type MaxLength int
// +controllertools:marker:generateHelp:category="CRD validation"
// MinLength specifies the minimum length for this string.
type MinLength int
// +controllertools:marker:generateHelp:category="CRD validation"
// Pattern specifies that this string must match the given regular expression.
type Pattern string
// +controllertools:marker:generateHelp:category="CRD validation"
// MaxItems specifies the maximum length for this list.
type MaxItems int
// +controllertools:marker:generateHelp:category="CRD validation"
// MinItems specifies the minimun length for this list.
type MinItems int
// +controllertools:marker:generateHelp:category="CRD validation"
// UniqueItems specifies that all items in this list must be unique.
type UniqueItems bool
// +controllertools:marker:generateHelp:category="CRD validation"
// MaxProperties restricts the number of keys in an object
type MaxProperties int
// +controllertools:marker:generateHelp:category="CRD validation"
// MinProperties restricts the number of keys in an object
type MinProperties int
// +controllertools:marker:generateHelp:category="CRD validation"
// Enum specifies that this (scalar) field is restricted to the *exact* values specified here.
type Enum []interface{}
// +controllertools:marker:generateHelp:category="CRD validation"
// Format specifies additional "complex" formatting for this field.
//
// For example, a date-time field would be marked as "type: string" and
// "format: date-time".
type Format string
// +controllertools:marker:generateHelp:category="CRD validation"
// Type overrides the type for this field (which defaults to the equivalent of the Go type).
//
// This generally must be paired with custom serialization. For example, the
// metav1.Time field would be marked as "type: string" and "format: date-time".
type Type string
// +controllertools:marker:generateHelp:category="CRD validation"
// Nullable marks this field as allowing the "null" value.
//
// This is often not necessary, but may be helpful with custom serialization.
type Nullable struct{}
// +controllertools:marker:generateHelp:category="CRD validation"
// Default sets the default value for this field.
//
// A default value will be accepted as any value valid for the
// field. Formatting for common types include: boolean: `true`, string:
// `Cluster`, numerical: `1.24`, array: `{1,2}`, object: `{policy:
// "delete"}`). Defaults should be defined in pruned form, and only best-effort
// validation will be performed. Full validation of a default requires
// submission of the containing CRD to an apiserver.
type Default struct {
Value interface{}
}
// +controllertools:marker:generateHelp:category="CRD processing"
// PreserveUnknownFields stops the apiserver from pruning fields which are not specified.
//
// By default the apiserver drops unknown fields from the request payload
// during the decoding step. This marker stops the API server from doing so.
// It affects fields recursively, but switches back to normal pruning behaviour
// if nested properties or additionalProperties are specified in the schema.
// This can either be true or undefined. False
// is forbidden.
//
// NB: The kubebuilder:validation:XPreserveUnknownFields variant is deprecated
// in favor of the kubebuilder:pruning:PreserveUnknownFields variant. They function
// identically.
type XPreserveUnknownFields struct{}
// +controllertools:marker:generateHelp:category="CRD validation"
// EmbeddedResource marks a fields as an embedded resource with apiVersion, kind and metadata fields.
//
// An embedded resource is a value that has apiVersion, kind and metadata fields.
// They are validated implicitly according to the semantics of the currently
// running apiserver. It is not necessary to add any additional schema for these
// field, yet it is possible. This can be combined with PreserveUnknownFields.
type XEmbeddedResource struct{}
// +controllertools:marker:generateHelp:category="CRD validation"
// Schemaless marks a field as being a schemaless object.
//
// Schemaless objects are not introspected, so you must provide
// any type and validation information yourself. One use for this
// tag is for embedding fields that hold JSONSchema typed objects.
// Because this field disables all type checking, it is recommended
// to be used only as a last resort.
type Schemaless struct{}
func (m Maximum) ApplyToSchema(schema *apiext.JSONSchemaProps) error {
if schema.Type != "integer" {
return fmt.Errorf("must apply maximum to an integer")
}
val := float64(m)
schema.Maximum = &val
return nil
}
func (m Minimum) ApplyToSchema(schema *apiext.JSONSchemaProps) error {
if schema.Type != "integer" {
return fmt.Errorf("must apply minimum to an integer")
}
val := float64(m)
schema.Minimum = &val
return nil
}
func (m ExclusiveMaximum) ApplyToSchema(schema *apiext.JSONSchemaProps) error {
if schema.Type != "integer" {
return fmt.Errorf("must apply exclusivemaximum to an integer")
}
schema.ExclusiveMaximum = bool(m)
return nil
}
func (m ExclusiveMinimum) ApplyToSchema(schema *apiext.JSONSchemaProps) error {
if schema.Type != "integer" {
return fmt.Errorf("must apply exclusiveminimum to an integer")
}
schema.ExclusiveMinimum = bool(m)
return nil
}
func (m MultipleOf) ApplyToSchema(schema *apiext.JSONSchemaProps) error {
if schema.Type != "integer" {
return fmt.Errorf("must apply multipleof to an integer")
}
val := float64(m)
schema.MultipleOf = &val
return nil
}
func (m MaxLength) ApplyToSchema(schema *apiext.JSONSchemaProps) error {
if schema.Type != "string" {
return fmt.Errorf("must apply maxlength to a string")
}
val := int64(m)
schema.MaxLength = &val
return nil
}
func (m MinLength) ApplyToSchema(schema *apiext.JSONSchemaProps) error {
if schema.Type != "string" {
return fmt.Errorf("must apply minlength to a string")
}
val := int64(m)
schema.MinLength = &val
return nil
}
func (m Pattern) ApplyToSchema(schema *apiext.JSONSchemaProps) error {
if schema.Type != "string" {
return fmt.Errorf("must apply pattern to a string")
}
schema.Pattern = string(m)
return nil
}
func (m MaxItems) ApplyToSchema(schema *apiext.JSONSchemaProps) error {
if schema.Type != "array" {
return fmt.Errorf("must apply maxitem to an array")
}
val := int64(m)
schema.MaxItems = &val
return nil
}
func (m MinItems) ApplyToSchema(schema *apiext.JSONSchemaProps) error {
if schema.Type != "array" {
return fmt.Errorf("must apply minitems to an array")
}
val := int64(m)
schema.MinItems = &val
return nil
}
func (m UniqueItems) ApplyToSchema(schema *apiext.JSONSchemaProps) error {
if schema.Type != "array" {
return fmt.Errorf("must apply uniqueitems to an array")
}
schema.UniqueItems = bool(m)
return nil
}
func (m MinProperties) ApplyToSchema(schema *apiext.JSONSchemaProps) error {
if schema.Type != "object" {
return fmt.Errorf("must apply minproperties to an object")
}
val := int64(m)
schema.MinProperties = &val
return nil
}
func (m MaxProperties) ApplyToSchema(schema *apiext.JSONSchemaProps) error {
if schema.Type != "object" {
return fmt.Errorf("must apply maxproperties to an object")
}
val := int64(m)
schema.MaxProperties = &val
return nil
}
func (m Enum) ApplyToSchema(schema *apiext.JSONSchemaProps) error {
// TODO(directxman12): this is a bit hacky -- we should
// probably support AnyType better + using the schema structure
vals := make([]apiext.JSON, len(m))
for i, val := range m {
// TODO(directxman12): check actual type with schema type?
// if we're expecting a string, marshal the string properly...
// NB(directxman12): we use json.Marshal to ensure we handle JSON escaping properly
valMarshalled, err := json.Marshal(val)
if err != nil {
return err
}
vals[i] = apiext.JSON{Raw: valMarshalled}
}
schema.Enum = vals
return nil
}
func (m Format) ApplyToSchema(schema *apiext.JSONSchemaProps) error {
schema.Format = string(m)
return nil
}
// NB(directxman12): we "typecheck" on target schema properties here,
// which means the "Type" marker *must* be applied first.
// TODO(directxman12): find a less hacky way to do this
// (we could preserve ordering of markers, but that feels bad in its own right).
func (m Type) ApplyToSchema(schema *apiext.JSONSchemaProps) error {
schema.Type = string(m)
return nil
}
func (m Type) ApplyFirst() {}
func (m Nullable) ApplyToSchema(schema *apiext.JSONSchemaProps) error {
schema.Nullable = true
return nil
}
// Defaults are only valid CRDs created with the v1 API
func (m Default) ApplyToSchema(schema *apiext.JSONSchemaProps) error {
marshalledDefault, err := json.Marshal(m.Value)
if err != nil {
return err
}
schema.Default = &apiext.JSON{Raw: marshalledDefault}
return nil
}
func (m XPreserveUnknownFields) ApplyToSchema(schema *apiext.JSONSchemaProps) error {
defTrue := true
schema.XPreserveUnknownFields = &defTrue
return nil
}
func (m XEmbeddedResource) ApplyToSchema(schema *apiext.JSONSchemaProps) error {
schema.XEmbeddedResource = true
return nil
}

View File

@@ -0,0 +1,457 @@
// +build !ignore_autogenerated
/*
Copyright2019 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 helpgen. DO NOT EDIT.
package markers
import (
"sigs.k8s.io/controller-tools/pkg/markers"
)
func (Default) Help() *markers.DefinitionHelp {
return &markers.DefinitionHelp{
Category: "CRD validation",
DetailedHelp: markers.DetailedHelp{
Summary: "sets the default value for this field. ",
Details: "A default value will be accepted as any value valid for the field. Formatting for common types include: boolean: `true`, string: `Cluster`, numerical: `1.24`, array: `{1,2}`, object: `{policy: \"delete\"}`). Defaults should be defined in pruned form, and only best-effort validation will be performed. Full validation of a default requires submission of the containing CRD to an apiserver.",
},
FieldHelp: map[string]markers.DetailedHelp{
"Value": {
Summary: "",
Details: "",
},
},
}
}
func (DeprecatedVersion) Help() *markers.DefinitionHelp {
return &markers.DefinitionHelp{
Category: "CRD",
DetailedHelp: markers.DetailedHelp{
Summary: "marks this version as deprecated.",
Details: "",
},
FieldHelp: map[string]markers.DetailedHelp{
"Warning": markers.DetailedHelp{
Summary: "message to be shown on the deprecated version",
Details: "",
},
},
}
}
func (Enum) Help() *markers.DefinitionHelp {
return &markers.DefinitionHelp{
Category: "CRD validation",
DetailedHelp: markers.DetailedHelp{
Summary: "specifies that this (scalar) field is restricted to the *exact* values specified here.",
Details: "",
},
FieldHelp: map[string]markers.DetailedHelp{},
}
}
func (ExclusiveMaximum) Help() *markers.DefinitionHelp {
return &markers.DefinitionHelp{
Category: "CRD validation",
DetailedHelp: markers.DetailedHelp{
Summary: "indicates that the maximum is \"up to\" but not including that value.",
Details: "",
},
FieldHelp: map[string]markers.DetailedHelp{},
}
}
func (ExclusiveMinimum) Help() *markers.DefinitionHelp {
return &markers.DefinitionHelp{
Category: "CRD validation",
DetailedHelp: markers.DetailedHelp{
Summary: "indicates that the minimum is \"up to\" but not including that value.",
Details: "",
},
FieldHelp: map[string]markers.DetailedHelp{},
}
}
func (Format) Help() *markers.DefinitionHelp {
return &markers.DefinitionHelp{
Category: "CRD validation",
DetailedHelp: markers.DetailedHelp{
Summary: "specifies additional \"complex\" formatting for this field. ",
Details: "For example, a date-time field would be marked as \"type: string\" and \"format: date-time\".",
},
FieldHelp: map[string]markers.DetailedHelp{},
}
}
func (ListMapKey) Help() *markers.DefinitionHelp {
return &markers.DefinitionHelp{
Category: "CRD processing",
DetailedHelp: markers.DetailedHelp{
Summary: "specifies the keys to map listTypes. ",
Details: "It indicates the index of a map list. They can be repeated if multiple keys must be used. It can only be used when ListType is set to map, and the keys should be scalar types.",
},
FieldHelp: map[string]markers.DetailedHelp{},
}
}
func (ListType) Help() *markers.DefinitionHelp {
return &markers.DefinitionHelp{
Category: "CRD processing",
DetailedHelp: markers.DetailedHelp{
Summary: "specifies the type of data-structure that the list represents (map, set, atomic). ",
Details: "Possible data-structure types of a list are: \n - \"map\": it needs to have a key field, which will be used to build an associative list. A typical example is a the pod container list, which is indexed by the container name. \n - \"set\": Fields need to be \"scalar\", and there can be only one occurrence of each. \n - \"atomic\": All the fields in the list are treated as a single value, are typically manipulated together by the same actor.",
},
FieldHelp: map[string]markers.DetailedHelp{},
}
}
func (MapType) Help() *markers.DefinitionHelp {
return &markers.DefinitionHelp{
Category: "CRD processing",
DetailedHelp: markers.DetailedHelp{
Summary: "specifies the level of atomicity of the map; i.e. whether each item in the map is independent of the others, or all fields are treated as a single unit. ",
Details: "Possible values: \n - \"granular\": items in the map are independent of each other, and can be manipulated by different actors. This is the default behavior. \n - \"atomic\": all fields are treated as one unit. Any changes have to replace the entire map.",
},
FieldHelp: map[string]markers.DetailedHelp{},
}
}
func (MaxItems) Help() *markers.DefinitionHelp {
return &markers.DefinitionHelp{
Category: "CRD validation",
DetailedHelp: markers.DetailedHelp{
Summary: "specifies the maximum length for this list.",
Details: "",
},
FieldHelp: map[string]markers.DetailedHelp{},
}
}
func (MaxLength) Help() *markers.DefinitionHelp {
return &markers.DefinitionHelp{
Category: "CRD validation",
DetailedHelp: markers.DetailedHelp{
Summary: "specifies the maximum length for this string.",
Details: "",
},
FieldHelp: map[string]markers.DetailedHelp{},
}
}
func (MaxProperties) Help() *markers.DefinitionHelp {
return &markers.DefinitionHelp{
Category: "CRD validation",
DetailedHelp: markers.DetailedHelp{
Summary: "restricts the number of keys in an object",
Details: "",
},
FieldHelp: map[string]markers.DetailedHelp{},
}
}
func (Maximum) Help() *markers.DefinitionHelp {
return &markers.DefinitionHelp{
Category: "CRD validation",
DetailedHelp: markers.DetailedHelp{
Summary: "specifies the maximum numeric value that this field can have.",
Details: "",
},
FieldHelp: map[string]markers.DetailedHelp{},
}
}
func (MinItems) Help() *markers.DefinitionHelp {
return &markers.DefinitionHelp{
Category: "CRD validation",
DetailedHelp: markers.DetailedHelp{
Summary: "specifies the minimun length for this list.",
Details: "",
},
FieldHelp: map[string]markers.DetailedHelp{},
}
}
func (MinLength) Help() *markers.DefinitionHelp {
return &markers.DefinitionHelp{
Category: "CRD validation",
DetailedHelp: markers.DetailedHelp{
Summary: "specifies the minimum length for this string.",
Details: "",
},
FieldHelp: map[string]markers.DetailedHelp{},
}
}
func (MinProperties) Help() *markers.DefinitionHelp {
return &markers.DefinitionHelp{
Category: "CRD validation",
DetailedHelp: markers.DetailedHelp{
Summary: "restricts the number of keys in an object",
Details: "",
},
FieldHelp: map[string]markers.DetailedHelp{},
}
}
func (Minimum) Help() *markers.DefinitionHelp {
return &markers.DefinitionHelp{
Category: "CRD validation",
DetailedHelp: markers.DetailedHelp{
Summary: "specifies the minimum numeric value that this field can have. Negative integers are supported.",
Details: "",
},
FieldHelp: map[string]markers.DetailedHelp{},
}
}
func (MultipleOf) Help() *markers.DefinitionHelp {
return &markers.DefinitionHelp{
Category: "CRD validation",
DetailedHelp: markers.DetailedHelp{
Summary: "specifies that this field must have a numeric value that's a multiple of this one.",
Details: "",
},
FieldHelp: map[string]markers.DetailedHelp{},
}
}
func (Nullable) Help() *markers.DefinitionHelp {
return &markers.DefinitionHelp{
Category: "CRD validation",
DetailedHelp: markers.DetailedHelp{
Summary: "marks this field as allowing the \"null\" value. ",
Details: "This is often not necessary, but may be helpful with custom serialization.",
},
FieldHelp: map[string]markers.DetailedHelp{},
}
}
func (Pattern) Help() *markers.DefinitionHelp {
return &markers.DefinitionHelp{
Category: "CRD validation",
DetailedHelp: markers.DetailedHelp{
Summary: "specifies that this string must match the given regular expression.",
Details: "",
},
FieldHelp: map[string]markers.DetailedHelp{},
}
}
func (PrintColumn) Help() *markers.DefinitionHelp {
return &markers.DefinitionHelp{
Category: "CRD",
DetailedHelp: markers.DetailedHelp{
Summary: "adds a column to \"kubectl get\" output for this CRD.",
Details: "",
},
FieldHelp: map[string]markers.DetailedHelp{
"Name": {
Summary: "specifies the name of the column.",
Details: "",
},
"Type": {
Summary: "indicates the type of the column. ",
Details: "It may be any OpenAPI data type listed at https://github.com/OAI/OpenAPI-Specification/blob/master/versions/2.0.md#data-types.",
},
"JSONPath": {
Summary: "specifies the jsonpath expression used to extract the value of the column.",
Details: "",
},
"Description": {
Summary: "specifies the help/description for this column.",
Details: "",
},
"Format": {
Summary: "specifies the format of the column. ",
Details: "It may be any OpenAPI data format corresponding to the type, listed at https://github.com/OAI/OpenAPI-Specification/blob/master/versions/2.0.md#data-types.",
},
"Priority": {
Summary: "indicates how important it is that this column be displayed. ",
Details: "Lower priority (*higher* numbered) columns will be hidden if the terminal width is too small.",
},
},
}
}
func (Resource) Help() *markers.DefinitionHelp {
return &markers.DefinitionHelp{
Category: "CRD",
DetailedHelp: markers.DetailedHelp{
Summary: "configures naming and scope for a CRD.",
Details: "",
},
FieldHelp: map[string]markers.DetailedHelp{
"Path": {
Summary: "specifies the plural \"resource\" for this CRD. ",
Details: "It generally corresponds to a plural, lower-cased version of the Kind. See https://book.kubebuilder.io/cronjob-tutorial/gvks.html.",
},
"ShortName": {
Summary: "specifies aliases for this CRD. ",
Details: "Short names are often used when people have work with your resource over and over again. For instance, \"rs\" for \"replicaset\" or \"crd\" for customresourcedefinition.",
},
"Categories": {
Summary: "specifies which group aliases this resource is part of. ",
Details: "Group aliases are used to work with groups of resources at once. The most common one is \"all\" which covers about a third of the base resources in Kubernetes, and is generally used for \"user-facing\" resources.",
},
"Singular": {
Summary: "overrides the singular form of your resource. ",
Details: "The singular form is otherwise defaulted off the plural (path).",
},
"Scope": {
Summary: "overrides the scope of the CRD (Cluster vs Namespaced). ",
Details: "Scope defaults to \"Namespaced\". Cluster-scoped (\"Cluster\") resources don't exist in namespaces.",
},
},
}
}
func (Schemaless) Help() *markers.DefinitionHelp {
return &markers.DefinitionHelp{
Category: "CRD validation",
DetailedHelp: markers.DetailedHelp{
Summary: "marks a field as being a schemaless object. ",
Details: "Schemaless objects are not introspected, so you must provide any type and validation information yourself. One use for this tag is for embedding fields that hold JSONSchema typed objects. Because this field disables all type checking, it is recommended to be used only as a last resort.",
},
FieldHelp: map[string]markers.DetailedHelp{},
}
}
func (SkipVersion) Help() *markers.DefinitionHelp {
return &markers.DefinitionHelp{
Category: "CRD",
DetailedHelp: markers.DetailedHelp{
Summary: "removes the particular version of the CRD from the CRDs spec. ",
Details: "This is useful if you need to skip generating and listing version entries for 'internal' resource versions, which typically exist if using the Kubernetes upstream conversion-gen tool.",
},
FieldHelp: map[string]markers.DetailedHelp{},
}
}
func (StorageVersion) Help() *markers.DefinitionHelp {
return &markers.DefinitionHelp{
Category: "CRD",
DetailedHelp: markers.DetailedHelp{
Summary: "marks this version as the \"storage version\" for the CRD for conversion. ",
Details: "When conversion is enabled for a CRD (i.e. it's not a trivial-versions/single-version CRD), one version is set as the \"storage version\" to be stored in etcd. Attempting to store any other version will result in conversion to the storage version via a conversion webhook.",
},
FieldHelp: map[string]markers.DetailedHelp{},
}
}
func (StructType) Help() *markers.DefinitionHelp {
return &markers.DefinitionHelp{
Category: "CRD processing",
DetailedHelp: markers.DetailedHelp{
Summary: "specifies the level of atomicity of the struct; i.e. whether each field in the struct is independent of the others, or all fields are treated as a single unit. ",
Details: "Possible values: \n - \"granular\": fields in the struct are independent of each other, and can be manipulated by different actors. This is the default behavior. \n - \"atomic\": all fields are treated as one unit. Any changes have to replace the entire struct.",
},
FieldHelp: map[string]markers.DetailedHelp{},
}
}
func (SubresourceScale) Help() *markers.DefinitionHelp {
return &markers.DefinitionHelp{
Category: "CRD",
DetailedHelp: markers.DetailedHelp{
Summary: "enables the \"/scale\" subresource on a CRD.",
Details: "",
},
FieldHelp: map[string]markers.DetailedHelp{
"SpecPath": {
Summary: "specifies the jsonpath to the replicas field for the scale's spec.",
Details: "",
},
"StatusPath": {
Summary: "specifies the jsonpath to the replicas field for the scale's status.",
Details: "",
},
"SelectorPath": {
Summary: "specifies the jsonpath to the pod label selector field for the scale's status. ",
Details: "The selector field must be the *string* form (serialized form) of a selector. Setting a pod label selector is necessary for your type to work with the HorizontalPodAutoscaler.",
},
},
}
}
func (SubresourceStatus) Help() *markers.DefinitionHelp {
return &markers.DefinitionHelp{
Category: "CRD",
DetailedHelp: markers.DetailedHelp{
Summary: "enables the \"/status\" subresource on a CRD.",
Details: "",
},
FieldHelp: map[string]markers.DetailedHelp{},
}
}
func (Type) Help() *markers.DefinitionHelp {
return &markers.DefinitionHelp{
Category: "CRD validation",
DetailedHelp: markers.DetailedHelp{
Summary: "overrides the type for this field (which defaults to the equivalent of the Go type). ",
Details: "This generally must be paired with custom serialization. For example, the metav1.Time field would be marked as \"type: string\" and \"format: date-time\".",
},
FieldHelp: map[string]markers.DetailedHelp{},
}
}
func (UniqueItems) Help() *markers.DefinitionHelp {
return &markers.DefinitionHelp{
Category: "CRD validation",
DetailedHelp: markers.DetailedHelp{
Summary: "specifies that all items in this list must be unique.",
Details: "",
},
FieldHelp: map[string]markers.DetailedHelp{},
}
}
func (UnservedVersion) Help() *markers.DefinitionHelp {
return &markers.DefinitionHelp{
Category: "CRD",
DetailedHelp: markers.DetailedHelp{
Summary: "does not serve this version. ",
Details: "This is useful if you need to drop support for a version in favor of a newer version.",
},
FieldHelp: map[string]markers.DetailedHelp{},
}
}
func (XEmbeddedResource) Help() *markers.DefinitionHelp {
return &markers.DefinitionHelp{
Category: "CRD validation",
DetailedHelp: markers.DetailedHelp{
Summary: "EmbeddedResource marks a fields as an embedded resource with apiVersion, kind and metadata fields. ",
Details: "An embedded resource is a value that has apiVersion, kind and metadata fields. They are validated implicitly according to the semantics of the currently running apiserver. It is not necessary to add any additional schema for these field, yet it is possible. This can be combined with PreserveUnknownFields.",
},
FieldHelp: map[string]markers.DetailedHelp{},
}
}
func (XPreserveUnknownFields) Help() *markers.DefinitionHelp {
return &markers.DefinitionHelp{
Category: "CRD processing",
DetailedHelp: markers.DetailedHelp{
Summary: "PreserveUnknownFields stops the apiserver from pruning fields which are not specified. ",
Details: "By default the apiserver drops unknown fields from the request payload during the decoding step. This marker stops the API server from doing so. It affects fields recursively, but switches back to normal pruning behaviour if nested properties or additionalProperties are specified in the schema. This can either be true or undefined. False is forbidden. \n NB: The kubebuilder:validation:XPreserveUnknownFields variant is deprecated in favor of the kubebuilder:pruning:PreserveUnknownFields variant. They function identically.",
},
FieldHelp: map[string]markers.DetailedHelp{},
}
}

240
vendor/sigs.k8s.io/controller-tools/pkg/crd/parser.go generated vendored Normal file
View File

@@ -0,0 +1,240 @@
/*
Copyright 2019 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 crd
import (
"fmt"
apiext "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
"k8s.io/apimachinery/pkg/runtime/schema"
"sigs.k8s.io/controller-tools/pkg/loader"
"sigs.k8s.io/controller-tools/pkg/markers"
)
// TypeIdent represents some type in a Package.
type TypeIdent struct {
Package *loader.Package
Name string
}
func (t TypeIdent) String() string {
return fmt.Sprintf("%q.%s", t.Package.ID, t.Name)
}
// PackageOverride overrides the loading of some package
// (potentially setting custom schemata, etc). It must
// call AddPackage if it wants to continue with the default
// loading behavior.
type PackageOverride func(p *Parser, pkg *loader.Package)
// Parser knows how to parse out CRD information and generate
// OpenAPI schemata from some collection of types and markers.
// Most methods on Parser cache their results automatically,
// and thus may be called any number of times.
type Parser struct {
Collector *markers.Collector
// Types contains the known TypeInfo for this parser.
Types map[TypeIdent]*markers.TypeInfo
// Schemata contains the known OpenAPI JSONSchemata for this parser.
Schemata map[TypeIdent]apiext.JSONSchemaProps
// GroupVersions contains the known group-versions of each package in this parser.
GroupVersions map[*loader.Package]schema.GroupVersion
// CustomResourceDefinitions contains the known CustomResourceDefinitions for types in this parser.
CustomResourceDefinitions map[schema.GroupKind]apiext.CustomResourceDefinition
// FlattenedSchemata contains fully flattened schemata for use in building
// CustomResourceDefinition validation. Each schema has been flattened by the flattener,
// and then embedded fields have been flattened with FlattenEmbedded.
FlattenedSchemata map[TypeIdent]apiext.JSONSchemaProps
// PackageOverrides indicates that the loading of any package with
// the given path should be handled by the given overrider.
PackageOverrides map[string]PackageOverride
// checker stores persistent partial type-checking/reference-traversal information.
Checker *loader.TypeChecker
// packages marks packages as loaded, to avoid re-loading them.
packages map[*loader.Package]struct{}
flattener *Flattener
// AllowDangerousTypes controls the handling of non-recommended types such as float. If
// false (the default), these types are not supported.
// There is a continuum here:
// 1. Types that are always supported.
// 2. Types that are allowed by default, but not recommended (warning emitted when they are encountered as per PR #443).
// Possibly they are allowed by default for historical reasons and may even be "on their way out" at some point in the future.
// 3. Types that are not allowed by default, not recommended, but there are some legitimate reasons to need them in certain corner cases.
// Possibly these types should also emit a warning as per PR #443 even when they are "switched on" (an integration point between
// this feature and #443 if desired). This is the category that this flag deals with.
// 4. Types that are not allowed and will not be allowed, possibly because it just "doesn't make sense" or possibly
// because the implementation is too difficult/clunky to promote them to category 3.
// TODO: Should we have a more formal mechanism for putting "type patterns" in each of the above categories?
AllowDangerousTypes bool
// GenerateEmbeddedObjectMeta specifies if any embedded ObjectMeta should be generated
GenerateEmbeddedObjectMeta bool
}
func (p *Parser) init() {
if p.packages == nil {
p.packages = make(map[*loader.Package]struct{})
}
if p.flattener == nil {
p.flattener = &Flattener{
Parser: p,
}
}
if p.Schemata == nil {
p.Schemata = make(map[TypeIdent]apiext.JSONSchemaProps)
}
if p.Types == nil {
p.Types = make(map[TypeIdent]*markers.TypeInfo)
}
if p.PackageOverrides == nil {
p.PackageOverrides = make(map[string]PackageOverride)
}
if p.GroupVersions == nil {
p.GroupVersions = make(map[*loader.Package]schema.GroupVersion)
}
if p.CustomResourceDefinitions == nil {
p.CustomResourceDefinitions = make(map[schema.GroupKind]apiext.CustomResourceDefinition)
}
if p.FlattenedSchemata == nil {
p.FlattenedSchemata = make(map[TypeIdent]apiext.JSONSchemaProps)
}
}
// indexTypes loads all types in the package into Types.
func (p *Parser) indexTypes(pkg *loader.Package) {
// autodetect
pkgMarkers, err := markers.PackageMarkers(p.Collector, pkg)
if err != nil {
pkg.AddError(err)
} else {
if skipPkg := pkgMarkers.Get("kubebuilder:skip"); skipPkg != nil {
return
}
if nameVal := pkgMarkers.Get("groupName"); nameVal != nil {
versionVal := pkg.Name // a reasonable guess
if versionMarker := pkgMarkers.Get("versionName"); versionMarker != nil {
versionVal = versionMarker.(string)
}
p.GroupVersions[pkg] = schema.GroupVersion{
Version: versionVal,
Group: nameVal.(string),
}
}
}
if err := markers.EachType(p.Collector, pkg, func(info *markers.TypeInfo) {
ident := TypeIdent{
Package: pkg,
Name: info.Name,
}
p.Types[ident] = info
}); err != nil {
pkg.AddError(err)
}
}
// LookupType fetches type info from Types.
func (p *Parser) LookupType(pkg *loader.Package, name string) *markers.TypeInfo {
return p.Types[TypeIdent{Package: pkg, Name: name}]
}
// NeedSchemaFor indicates that a schema should be generated for the given type.
func (p *Parser) NeedSchemaFor(typ TypeIdent) {
p.init()
p.NeedPackage(typ.Package)
if _, knownSchema := p.Schemata[typ]; knownSchema {
return
}
info, knownInfo := p.Types[typ]
if !knownInfo {
typ.Package.AddError(fmt.Errorf("unknown type %s", typ))
return
}
// avoid tripping recursive schemata, like ManagedFields, by adding an empty WIP schema
p.Schemata[typ] = apiext.JSONSchemaProps{}
schemaCtx := newSchemaContext(typ.Package, p, p.AllowDangerousTypes)
ctxForInfo := schemaCtx.ForInfo(info)
pkgMarkers, err := markers.PackageMarkers(p.Collector, typ.Package)
if err != nil {
typ.Package.AddError(err)
}
ctxForInfo.PackageMarkers = pkgMarkers
schema := infoToSchema(ctxForInfo)
p.Schemata[typ] = *schema
}
func (p *Parser) NeedFlattenedSchemaFor(typ TypeIdent) {
p.init()
if _, knownSchema := p.FlattenedSchemata[typ]; knownSchema {
return
}
p.NeedSchemaFor(typ)
partialFlattened := p.flattener.FlattenType(typ)
fullyFlattened := FlattenEmbedded(partialFlattened, typ.Package)
p.FlattenedSchemata[typ] = *fullyFlattened
}
// NeedCRDFor lives off in spec.go
// AddPackage indicates that types and type-checking information is needed
// for the the given package, *ignoring* overrides.
// Generally, consumers should call NeedPackage, while PackageOverrides should
// call AddPackage to continue with the normal loading procedure.
func (p *Parser) AddPackage(pkg *loader.Package) {
p.init()
if _, checked := p.packages[pkg]; checked {
return
}
p.indexTypes(pkg)
p.Checker.Check(pkg)
p.packages[pkg] = struct{}{}
}
// NeedPackage indicates that types and type-checking information
// is needed for the given package.
func (p *Parser) NeedPackage(pkg *loader.Package) {
p.init()
if _, checked := p.packages[pkg]; checked {
return
}
// overrides are going to be written without vendor. This is why we index by the actual
// object when we can.
if override, overridden := p.PackageOverrides[loader.NonVendorPath(pkg.PkgPath)]; overridden {
override(p, pkg)
p.packages[pkg] = struct{}{}
return
}
p.AddPackage(pkg)
}

433
vendor/sigs.k8s.io/controller-tools/pkg/crd/schema.go generated vendored Normal file
View File

@@ -0,0 +1,433 @@
/*
Copyright 2019 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 crd
import (
"fmt"
"go/ast"
"go/types"
"strings"
apiext "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
crdmarkers "sigs.k8s.io/controller-tools/pkg/crd/markers"
"sigs.k8s.io/controller-tools/pkg/loader"
"sigs.k8s.io/controller-tools/pkg/markers"
)
// Schema flattening is done in a recursive mapping method.
// Start reading at infoToSchema.
const (
// defPrefix is the prefix used to link to definitions in the OpenAPI schema.
defPrefix = "#/definitions/"
)
var (
// byteType is the types.Type for byte (see the types documention
// for why we need to look this up in the Universe), saved
// for quick comparison.
byteType = types.Universe.Lookup("byte").Type()
)
// SchemaMarker is any marker that needs to modify the schema of the underlying type or field.
type SchemaMarker interface {
// ApplyToSchema is called after the rest of the schema for a given type
// or field is generated, to modify the schema appropriately.
ApplyToSchema(*apiext.JSONSchemaProps) error
}
// applyFirstMarker is applied before any other markers. It's a bit of a hack.
type applyFirstMarker interface {
ApplyFirst()
}
// schemaRequester knows how to marker that another schema (e.g. via an external reference) is necessary.
type schemaRequester interface {
NeedSchemaFor(typ TypeIdent)
}
// schemaContext stores and provides information across a hierarchy of schema generation.
type schemaContext struct {
pkg *loader.Package
info *markers.TypeInfo
schemaRequester schemaRequester
PackageMarkers markers.MarkerValues
allowDangerousTypes bool
}
// newSchemaContext constructs a new schemaContext for the given package and schema requester.
// It must have type info added before use via ForInfo.
func newSchemaContext(pkg *loader.Package, req schemaRequester, allowDangerousTypes bool) *schemaContext {
pkg.NeedTypesInfo()
return &schemaContext{
pkg: pkg,
schemaRequester: req,
allowDangerousTypes: allowDangerousTypes,
}
}
// ForInfo produces a new schemaContext with containing the same information
// as this one, except with the given type information.
func (c *schemaContext) ForInfo(info *markers.TypeInfo) *schemaContext {
return &schemaContext{
pkg: c.pkg,
info: info,
schemaRequester: c.schemaRequester,
allowDangerousTypes: c.allowDangerousTypes,
}
}
// requestSchema asks for the schema for a type in the package with the
// given import path.
func (c *schemaContext) requestSchema(pkgPath, typeName string) {
pkg := c.pkg
if pkgPath != "" {
pkg = c.pkg.Imports()[pkgPath]
}
c.schemaRequester.NeedSchemaFor(TypeIdent{
Package: pkg,
Name: typeName,
})
}
// infoToSchema creates a schema for the type in the given set of type information.
func infoToSchema(ctx *schemaContext) *apiext.JSONSchemaProps {
return typeToSchema(ctx, ctx.info.RawSpec.Type)
}
// applyMarkers applies schema markers to the given schema, respecting "apply first" markers.
func applyMarkers(ctx *schemaContext, markerSet markers.MarkerValues, props *apiext.JSONSchemaProps, node ast.Node) {
// apply "apply first" markers first...
for _, markerValues := range markerSet {
for _, markerValue := range markerValues {
if _, isApplyFirst := markerValue.(applyFirstMarker); !isApplyFirst {
continue
}
schemaMarker, isSchemaMarker := markerValue.(SchemaMarker)
if !isSchemaMarker {
continue
}
if err := schemaMarker.ApplyToSchema(props); err != nil {
ctx.pkg.AddError(loader.ErrFromNode(err /* an okay guess */, node))
}
}
}
// ...then the rest of the markers
for _, markerValues := range markerSet {
for _, markerValue := range markerValues {
if _, isApplyFirst := markerValue.(applyFirstMarker); isApplyFirst {
// skip apply-first markers, which were already applied
continue
}
schemaMarker, isSchemaMarker := markerValue.(SchemaMarker)
if !isSchemaMarker {
continue
}
if err := schemaMarker.ApplyToSchema(props); err != nil {
ctx.pkg.AddError(loader.ErrFromNode(err /* an okay guess */, node))
}
}
}
}
// typeToSchema creates a schema for the given AST type.
func typeToSchema(ctx *schemaContext, rawType ast.Expr) *apiext.JSONSchemaProps {
var props *apiext.JSONSchemaProps
switch expr := rawType.(type) {
case *ast.Ident:
props = localNamedToSchema(ctx, expr)
case *ast.SelectorExpr:
props = namedToSchema(ctx, expr)
case *ast.ArrayType:
props = arrayToSchema(ctx, expr)
case *ast.MapType:
props = mapToSchema(ctx, expr)
case *ast.StarExpr:
props = typeToSchema(ctx, expr.X)
case *ast.StructType:
props = structToSchema(ctx, expr)
default:
ctx.pkg.AddError(loader.ErrFromNode(fmt.Errorf("unsupported AST kind %T", expr), rawType))
// NB(directxman12): we explicitly don't handle interfaces
return &apiext.JSONSchemaProps{}
}
props.Description = ctx.info.Doc
applyMarkers(ctx, ctx.info.Markers, props, rawType)
return props
}
// qualifiedName constructs a JSONSchema-safe qualified name for a type
// (`<typeName>` or `<safePkgPath>~0<typeName>`, where `<safePkgPath>`
// is the package path with `/` replaced by `~1`, according to JSONPointer
// escapes).
func qualifiedName(pkgName, typeName string) string {
if pkgName != "" {
return strings.Replace(pkgName, "/", "~1", -1) + "~0" + typeName
}
return typeName
}
// TypeRefLink creates a definition link for the given type and package.
func TypeRefLink(pkgName, typeName string) string {
return defPrefix + qualifiedName(pkgName, typeName)
}
// localNamedToSchema creates a schema (ref) for a *potentially* local type reference
// (could be external from a dot-import).
func localNamedToSchema(ctx *schemaContext, ident *ast.Ident) *apiext.JSONSchemaProps {
typeInfo := ctx.pkg.TypesInfo.TypeOf(ident)
if typeInfo == types.Typ[types.Invalid] {
ctx.pkg.AddError(loader.ErrFromNode(fmt.Errorf("unknown type %s", ident.Name), ident))
return &apiext.JSONSchemaProps{}
}
if basicInfo, isBasic := typeInfo.(*types.Basic); isBasic {
typ, fmt, err := builtinToType(basicInfo, ctx.allowDangerousTypes)
if err != nil {
ctx.pkg.AddError(loader.ErrFromNode(err, ident))
}
return &apiext.JSONSchemaProps{
Type: typ,
Format: fmt,
}
}
// NB(directxman12): if there are dot imports, this might be an external reference,
// so use typechecking info to get the actual object
typeNameInfo := typeInfo.(*types.Named).Obj()
pkg := typeNameInfo.Pkg()
pkgPath := loader.NonVendorPath(pkg.Path())
if pkg == ctx.pkg.Types {
pkgPath = ""
}
ctx.requestSchema(pkgPath, typeNameInfo.Name())
link := TypeRefLink(pkgPath, typeNameInfo.Name())
return &apiext.JSONSchemaProps{
Ref: &link,
}
}
// namedSchema creates a schema (ref) for an explicitly external type reference.
func namedToSchema(ctx *schemaContext, named *ast.SelectorExpr) *apiext.JSONSchemaProps {
typeInfoRaw := ctx.pkg.TypesInfo.TypeOf(named)
if typeInfoRaw == types.Typ[types.Invalid] {
ctx.pkg.AddError(loader.ErrFromNode(fmt.Errorf("unknown type %v.%s", named.X, named.Sel.Name), named))
return &apiext.JSONSchemaProps{}
}
typeInfo := typeInfoRaw.(*types.Named)
typeNameInfo := typeInfo.Obj()
nonVendorPath := loader.NonVendorPath(typeNameInfo.Pkg().Path())
ctx.requestSchema(nonVendorPath, typeNameInfo.Name())
link := TypeRefLink(nonVendorPath, typeNameInfo.Name())
return &apiext.JSONSchemaProps{
Ref: &link,
}
// NB(directxman12): we special-case things like resource.Quantity during the "collapse" phase.
}
// arrayToSchema creates a schema for the items of the given array, dealing appropriately
// with the special `[]byte` type (according to OpenAPI standards).
func arrayToSchema(ctx *schemaContext, array *ast.ArrayType) *apiext.JSONSchemaProps {
eltType := ctx.pkg.TypesInfo.TypeOf(array.Elt)
if eltType == byteType && array.Len == nil {
// byte slices are represented as base64-encoded strings
// (the format is defined in OpenAPI v3, but not JSON Schema)
return &apiext.JSONSchemaProps{
Type: "string",
Format: "byte",
}
}
// TODO(directxman12): backwards-compat would require access to markers from base info
items := typeToSchema(ctx.ForInfo(&markers.TypeInfo{}), array.Elt)
return &apiext.JSONSchemaProps{
Type: "array",
Items: &apiext.JSONSchemaPropsOrArray{Schema: items},
}
}
// mapToSchema creates a schema for items of the given map. Key types must eventually resolve
// to string (other types aren't allowed by JSON, and thus the kubernetes API standards).
func mapToSchema(ctx *schemaContext, mapType *ast.MapType) *apiext.JSONSchemaProps {
keyInfo := ctx.pkg.TypesInfo.TypeOf(mapType.Key)
// check that we've got a type that actually corresponds to a string
for keyInfo != nil {
switch typedKey := keyInfo.(type) {
case *types.Basic:
if typedKey.Info()&types.IsString == 0 {
ctx.pkg.AddError(loader.ErrFromNode(fmt.Errorf("map keys must be strings, not %s", keyInfo.String()), mapType.Key))
return &apiext.JSONSchemaProps{}
}
keyInfo = nil // stop iterating
case *types.Named:
keyInfo = typedKey.Underlying()
default:
ctx.pkg.AddError(loader.ErrFromNode(fmt.Errorf("map keys must be strings, not %s", keyInfo.String()), mapType.Key))
return &apiext.JSONSchemaProps{}
}
}
// TODO(directxman12): backwards-compat would require access to markers from base info
var valSchema *apiext.JSONSchemaProps
switch val := mapType.Value.(type) {
case *ast.Ident:
valSchema = localNamedToSchema(ctx.ForInfo(&markers.TypeInfo{}), val)
case *ast.SelectorExpr:
valSchema = namedToSchema(ctx.ForInfo(&markers.TypeInfo{}), val)
case *ast.ArrayType:
valSchema = arrayToSchema(ctx.ForInfo(&markers.TypeInfo{}), val)
if valSchema.Type == "array" && valSchema.Items.Schema.Type != "string" {
ctx.pkg.AddError(loader.ErrFromNode(fmt.Errorf("map values must be a named type, not %T", mapType.Value), mapType.Value))
return &apiext.JSONSchemaProps{}
}
case *ast.StarExpr:
valSchema = typeToSchema(ctx.ForInfo(&markers.TypeInfo{}), val)
default:
ctx.pkg.AddError(loader.ErrFromNode(fmt.Errorf("map values must be a named type, not %T", mapType.Value), mapType.Value))
return &apiext.JSONSchemaProps{}
}
return &apiext.JSONSchemaProps{
Type: "object",
AdditionalProperties: &apiext.JSONSchemaPropsOrBool{
Schema: valSchema,
Allows: true, /* set automatically by serialization, but useful for testing */
},
}
}
// structToSchema creates a schema for the given struct. Embedded fields are placed in AllOf,
// and can be flattened later with a Flattener.
func structToSchema(ctx *schemaContext, structType *ast.StructType) *apiext.JSONSchemaProps {
props := &apiext.JSONSchemaProps{
Type: "object",
Properties: make(map[string]apiext.JSONSchemaProps),
}
if ctx.info.RawSpec.Type != structType {
ctx.pkg.AddError(loader.ErrFromNode(fmt.Errorf("encountered non-top-level struct (possibly embedded), those aren't allowed"), structType))
return props
}
for _, field := range ctx.info.Fields {
jsonTag, hasTag := field.Tag.Lookup("json")
if !hasTag {
// if the field doesn't have a JSON tag, it doesn't belong in output (and shouldn't exist in a serialized type)
ctx.pkg.AddError(loader.ErrFromNode(fmt.Errorf("encountered struct field %q without JSON tag in type %q", field.Name, ctx.info.Name), field.RawField))
continue
}
jsonOpts := strings.Split(jsonTag, ",")
if len(jsonOpts) == 1 && jsonOpts[0] == "-" {
// skipped fields have the tag "-" (note that "-," means the field is named "-")
continue
}
inline := false
omitEmpty := false
for _, opt := range jsonOpts[1:] {
switch opt {
case "inline":
inline = true
case "omitempty":
omitEmpty = true
}
}
fieldName := jsonOpts[0]
inline = inline || fieldName == "" // anonymous fields are inline fields in YAML/JSON
// if no default required mode is set, default to required
defaultMode := "required"
if ctx.PackageMarkers.Get("kubebuilder:validation:Optional") != nil {
defaultMode = "optional"
}
switch defaultMode {
// if this package isn't set to optional default...
case "required":
// ...everything that's not inline, omitempty, or explicitly optional is required
if !inline && !omitEmpty && field.Markers.Get("kubebuilder:validation:Optional") == nil && field.Markers.Get("optional") == nil {
props.Required = append(props.Required, fieldName)
}
// if this package isn't set to required default...
case "optional":
// ...everything that isn't explicitly required is optional
if field.Markers.Get("kubebuilder:validation:Required") != nil {
props.Required = append(props.Required, fieldName)
}
}
var propSchema *apiext.JSONSchemaProps
if field.Markers.Get(crdmarkers.SchemalessName) != nil {
propSchema = &apiext.JSONSchemaProps{}
} else {
propSchema = typeToSchema(ctx.ForInfo(&markers.TypeInfo{}), field.RawField.Type)
}
propSchema.Description = field.Doc
applyMarkers(ctx, field.Markers, propSchema, field.RawField)
if inline {
props.AllOf = append(props.AllOf, *propSchema)
continue
}
props.Properties[fieldName] = *propSchema
}
return props
}
// builtinToType converts builtin basic types to their equivalent JSON schema form.
// It *only* handles types allowed by the kubernetes API standards. Floats are not
// allowed unless allowDangerousTypes is true
func builtinToType(basic *types.Basic, allowDangerousTypes bool) (typ string, format string, err error) {
// NB(directxman12): formats from OpenAPI v3 are slightly different than those defined
// in JSONSchema. This'll use the OpenAPI v3 ones, since they're useful for bounding our
// non-string types.
basicInfo := basic.Info()
switch {
case basicInfo&types.IsBoolean != 0:
typ = "boolean"
case basicInfo&types.IsString != 0:
typ = "string"
case basicInfo&types.IsInteger != 0:
typ = "integer"
case basicInfo&types.IsFloat != 0 && allowDangerousTypes:
typ = "number"
default:
// NB(directxman12): floats are *NOT* allowed in kubernetes APIs
return "", "", fmt.Errorf("unsupported type %q", basic.String())
}
switch basic.Kind() {
case types.Int32, types.Uint32:
format = "int32"
case types.Int64, types.Uint64:
format = "int64"
}
return typ, format, nil
}

View File

@@ -0,0 +1,131 @@
/*
Copyright 2019 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 crd
import (
apiext "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
)
// SchemaVisitor walks the nodes of a schema.
type SchemaVisitor interface {
// Visit is called for each schema node. If it returns a visitor,
// the visitor will be called on each direct child node, and then
// this visitor will be called again with `nil` to indicate that
// all children have been visited. If a nil visitor is returned,
// children are not visited.
//
// It is *NOT* safe to save references to the given schema.
// Make deepcopies if you need to keep things around beyond
// the lifetime of the call.
Visit(schema *apiext.JSONSchemaProps) SchemaVisitor
}
// EditSchema walks the given schema using the given visitor. Actual
// pointers to each schema node are passed to the visitor, so any changes
// made by the visitor will be reflected to the passed-in schema.
func EditSchema(schema *apiext.JSONSchemaProps, visitor SchemaVisitor) {
walker := schemaWalker{visitor: visitor}
walker.walkSchema(schema)
}
// schemaWalker knows how to walk the schema, saving modifications
// made by the given visitor.
type schemaWalker struct {
visitor SchemaVisitor
}
// walkSchema walks the given schema, saving modifications made by the visitor
// (this is as simple as passing a pointer in most cases, but special care
// needs to be taken to persist with maps). It also visits referenced
// schemata, dealing with circular references appropriately. The returned
// visitor will be used to visit all "children" of the current schema, followed
// by a nil schema with the returned visitor to mark completion. If a nil visitor
// is returned, traversal will no continue into the children of the current schema.
func (w schemaWalker) walkSchema(schema *apiext.JSONSchemaProps) {
// Walk a potential chain of schema references, keeping track of seen
// references to avoid circular references
subVisitor := w.visitor
seenRefs := map[string]bool{}
if schema.Ref != nil {
seenRefs[*schema.Ref] = true
}
for {
subVisitor = subVisitor.Visit(schema)
if subVisitor == nil {
return
}
// mark completion of the visitor
defer subVisitor.Visit(nil)
// Break if schema is not a reference or a cycle is detected
if schema.Ref == nil || len(*schema.Ref) == 0 || seenRefs[*schema.Ref] {
break
}
seenRefs[*schema.Ref] = true
}
// walk sub-schemata
subWalker := schemaWalker{visitor: subVisitor}
if schema.Items != nil {
subWalker.walkPtr(schema.Items.Schema)
subWalker.walkSlice(schema.Items.JSONSchemas)
}
subWalker.walkSlice(schema.AllOf)
subWalker.walkSlice(schema.OneOf)
subWalker.walkSlice(schema.AnyOf)
subWalker.walkPtr(schema.Not)
subWalker.walkMap(schema.Properties)
if schema.AdditionalProperties != nil {
subWalker.walkPtr(schema.AdditionalProperties.Schema)
}
subWalker.walkMap(schema.PatternProperties)
for name, dep := range schema.Dependencies {
subWalker.walkPtr(dep.Schema)
schema.Dependencies[name] = dep
}
if schema.AdditionalItems != nil {
subWalker.walkPtr(schema.AdditionalItems.Schema)
}
subWalker.walkMap(schema.Definitions)
}
// walkMap walks over values of the given map, saving changes to them.
func (w schemaWalker) walkMap(defs map[string]apiext.JSONSchemaProps) {
for name, def := range defs {
// this is iter var reference is because we immediately preseve it below
//nolint:gosec
w.walkSchema(&def)
// make sure the edits actually go through since we can't
// take a reference to the value in the map
defs[name] = def
}
}
// walkSlice walks over items of the given slice.
func (w schemaWalker) walkSlice(defs []apiext.JSONSchemaProps) {
for i := range defs {
w.walkSchema(&defs[i])
}
}
// walkPtr walks over the contents of the given pointer, if it's not nil.
func (w schemaWalker) walkPtr(def *apiext.JSONSchemaProps) {
if def == nil {
return
}
w.walkSchema(def)
}

174
vendor/sigs.k8s.io/controller-tools/pkg/crd/spec.go generated vendored Normal file
View File

@@ -0,0 +1,174 @@
/*
Copyright 2019 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 crd
import (
"fmt"
"sort"
"strings"
"github.com/gobuffalo/flect"
apiext "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime/schema"
"sigs.k8s.io/controller-tools/pkg/loader"
)
// SpecMarker is a marker that knows how to apply itself to a particular
// version in a CRD.
type SpecMarker interface {
// ApplyToCRD applies this marker to the given CRD, in the given version
// within that CRD. It's called after everything else in the CRD is populated.
ApplyToCRD(crd *apiext.CustomResourceDefinitionSpec, version string) error
}
// NeedCRDFor requests the full CRD for the given group-kind. It requires
// that the packages containing the Go structs for that CRD have already
// been loaded with NeedPackage.
func (p *Parser) NeedCRDFor(groupKind schema.GroupKind, maxDescLen *int) {
p.init()
if _, exists := p.CustomResourceDefinitions[groupKind]; exists {
return
}
var packages []*loader.Package
for pkg, gv := range p.GroupVersions {
if gv.Group != groupKind.Group {
continue
}
packages = append(packages, pkg)
}
defaultPlural := strings.ToLower(flect.Pluralize(groupKind.Kind))
crd := apiext.CustomResourceDefinition{
TypeMeta: metav1.TypeMeta{
APIVersion: apiext.SchemeGroupVersion.String(),
Kind: "CustomResourceDefinition",
},
ObjectMeta: metav1.ObjectMeta{
Name: defaultPlural + "." + groupKind.Group,
},
Spec: apiext.CustomResourceDefinitionSpec{
Group: groupKind.Group,
Names: apiext.CustomResourceDefinitionNames{
Kind: groupKind.Kind,
ListKind: groupKind.Kind + "List",
Plural: defaultPlural,
Singular: strings.ToLower(groupKind.Kind),
},
Scope: apiext.NamespaceScoped,
},
}
for _, pkg := range packages {
typeIdent := TypeIdent{Package: pkg, Name: groupKind.Kind}
typeInfo := p.Types[typeIdent]
if typeInfo == nil {
continue
}
p.NeedFlattenedSchemaFor(typeIdent)
fullSchema := p.FlattenedSchemata[typeIdent]
fullSchema = *fullSchema.DeepCopy() // don't mutate the cache (we might be truncating description, etc)
if maxDescLen != nil {
TruncateDescription(&fullSchema, *maxDescLen)
}
ver := apiext.CustomResourceDefinitionVersion{
Name: p.GroupVersions[pkg].Version,
Served: true,
Schema: &apiext.CustomResourceValidation{
OpenAPIV3Schema: &fullSchema, // fine to take a reference since we deepcopy above
},
}
crd.Spec.Versions = append(crd.Spec.Versions, ver)
}
// markers are applied *after* initial generation of objects
for _, pkg := range packages {
typeIdent := TypeIdent{Package: pkg, Name: groupKind.Kind}
typeInfo := p.Types[typeIdent]
if typeInfo == nil {
continue
}
ver := p.GroupVersions[pkg].Version
for _, markerVals := range typeInfo.Markers {
for _, val := range markerVals {
crdMarker, isCrdMarker := val.(SpecMarker)
if !isCrdMarker {
continue
}
if err := crdMarker.ApplyToCRD(&crd.Spec, ver); err != nil {
pkg.AddError(loader.ErrFromNode(err /* an okay guess */, typeInfo.RawSpec))
}
}
}
}
// fix the name if the plural was changed (this is the form the name *has* to take, so no harm in changing it).
crd.Name = crd.Spec.Names.Plural + "." + groupKind.Group
// nothing to actually write
if len(crd.Spec.Versions) == 0 {
return
}
// it is necessary to make sure the order of CRD versions in crd.Spec.Versions is stable and explicitly set crd.Spec.Version.
// Otherwise, crd.Spec.Version may point to different CRD versions across different runs.
sort.Slice(crd.Spec.Versions, func(i, j int) bool { return crd.Spec.Versions[i].Name < crd.Spec.Versions[j].Name })
// make sure we have *a* storage version
// (default it if we only have one, otherwise, bail)
if len(crd.Spec.Versions) == 1 {
crd.Spec.Versions[0].Storage = true
}
hasStorage := false
for _, ver := range crd.Spec.Versions {
if ver.Storage {
hasStorage = true
break
}
}
if !hasStorage {
// just add the error to the first relevant package for this CRD,
// since there's no specific error location
packages[0].AddError(fmt.Errorf("CRD for %s has no storage version", groupKind))
}
served := false
for _, ver := range crd.Spec.Versions {
if ver.Served {
served = true
break
}
}
if !served {
// just add the error to the first relevant package for this CRD,
// since there's no specific error location
packages[0].AddError(fmt.Errorf("CRD for %s with version(s) %v does not serve any version", groupKind, crd.Spec.Versions))
}
// NB(directxman12): CRD's status doesn't have omitempty markers, which means things
// get serialized as null, which causes the validator to freak out. Manually set
// these to empty till we get a better solution.
crd.Status.Conditions = []apiext.CustomResourceDefinitionCondition{}
crd.Status.StoredVersions = []string{}
p.CustomResourceDefinitions[groupKind] = crd
}

View File

@@ -0,0 +1,61 @@
// +build !ignore_autogenerated
/*
Copyright2019 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 helpgen. DO NOT EDIT.
package crd
import (
"sigs.k8s.io/controller-tools/pkg/markers"
)
func (Generator) Help() *markers.DefinitionHelp {
return &markers.DefinitionHelp{
Category: "",
DetailedHelp: markers.DetailedHelp{
Summary: "generates CustomResourceDefinition objects.",
Details: "",
},
FieldHelp: map[string]markers.DetailedHelp{
"TrivialVersions": {
Summary: "indicates that we should produce a single-version CRD. ",
Details: "Single \"trivial-version\" CRDs are compatible with older (pre 1.13) Kubernetes API servers. The storage version's schema will be used as the CRD's schema. \n Only works with the v1beta1 CRD version.",
},
"PreserveUnknownFields": {
Summary: "indicates whether or not we should turn off pruning. ",
Details: "Left unspecified, it'll default to true when only a v1beta1 CRD is generated (to preserve compatibility with older versions of this tool), or false otherwise. \n It's required to be false for v1 CRDs.",
},
"AllowDangerousTypes": {
Summary: "allows types which are usually omitted from CRD generation because they are not recommended. ",
Details: "Currently the following additional types are allowed when this is true: float32 float64 \n Left unspecified, the default is false",
},
"MaxDescLen": {
Summary: "specifies the maximum description length for fields in CRD's OpenAPI schema. ",
Details: "0 indicates drop the description for all fields completely. n indicates limit the description to at most n characters and truncate the description to closest sentence boundary if it exceeds n characters.",
},
"CRDVersions": {
Summary: "specifies the target API versions of the CRD type itself to generate. Defaults to v1. ",
Details: "The first version listed will be assumed to be the \"default\" version and will not get a version suffix in the output filename. \n You'll need to use \"v1\" to get support for features like defaulting, along with an API server that supports it (Kubernetes 1.16+).",
},
"GenerateEmbeddedObjectMeta": {
Summary: "specifies if any embedded ObjectMeta in the CRD should be generated",
Details: "",
},
},
}
}