update dependencies

Signed-off-by: hongming <talonwan@yunify.com>
This commit is contained in:
hongming
2020-12-22 16:48:26 +08:00
parent 4a11a50544
commit fe6c5de00f
2857 changed files with 252134 additions and 115656 deletions

View File

@@ -35,7 +35,7 @@ func AsVersion(original apiext.CustomResourceDefinition, gv schema.GroupVersion)
// questionable decision.
intVer, err := conversionScheme.ConvertToVersion(&original, apiextinternal.SchemeGroupVersion)
if err != nil {
return nil, fmt.Errorf("unable to convert to internal CRD version: %v", err)
return nil, fmt.Errorf("unable to convert to internal CRD version: %w", err)
}
return conversionScheme.ConvertToVersion(intVer, gv)

View File

@@ -31,6 +31,9 @@ import (
"sigs.k8s.io/controller-tools/pkg/version"
)
// The default CustomResourceDefinition version to generate.
const defaultVersion = "v1"
// +controllertools:marker:generateHelp
// Generator generates CustomResourceDefinition objects.
@@ -53,6 +56,16 @@ type Generator struct {
// 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.
@@ -61,7 +74,7 @@ type Generator struct {
MaxDescLen *int `marker:",optional"`
// CRDVersions specifies the target API versions of the CRD type itself to
// generate. Defaults to v1beta1.
// 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.
@@ -78,6 +91,8 @@ 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,
}
AddKnownTypes(parser)
@@ -101,10 +116,10 @@ func (g Generator) Generate(ctx *genall.GenerationContext) error {
crdVersions := g.CRDVersions
if len(crdVersions) == 0 {
crdVersions = []string{"v1beta1"}
crdVersions = []string{defaultVersion}
}
for _, groupKind := range kubeKinds {
for groupKind := range kubeKinds {
parser.NeedCRDFor(groupKind, g.MaxDescLen)
crdRaw := parser.CustomResourceDefinitions[groupKind]
addAttribution(&crdRaw)
@@ -206,9 +221,9 @@ func FindMetav1(roots []*loader.Package) *loader.Package {
// 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) []schema.GroupKind {
func FindKubeKinds(parser *Parser, metav1Pkg *loader.Package) map[schema.GroupKind]struct{} {
// TODO(directxman12): technically, we should be finding metav1 per-package
var kubeKinds []schema.GroupKind
kubeKinds := map[schema.GroupKind]struct{}{}
for typeIdent, info := range parser.Types {
hasObjectMeta := false
hasTypeMeta := false
@@ -257,7 +272,7 @@ func FindKubeKinds(parser *Parser, metav1Pkg *loader.Package) []schema.GroupKind
Group: parser.GroupVersions[pkg].Group,
Kind: typeIdent.Name,
}
kubeKinds = append(kubeKinds, groupKind)
kubeKinds[groupKind] = struct{}{}
}
return kubeKinds

View File

@@ -55,7 +55,12 @@ var KnownPackages = map[string]PackageOverride{
"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)
Type: "string",
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
},

View File

@@ -45,6 +45,9 @@ var CRDMarkers = []*definitionWithHelp{
must(markers.MakeDefinition("kubebuilder:skipversion", markers.DescribesType, SkipVersion{})).
WithHelp(SkipVersion{}.Help()),
must(markers.MakeDefinition("kubebuilder:unservedversion", markers.DescribesType, UnservedVersion{})).
WithHelp(UnservedVersion{}.Help()),
}
// TODO: categories and singular used to be annotations types
@@ -277,6 +280,9 @@ func (s Resource) ApplyToCRD(crd *apiext.CustomResourceDefinitionSpec, version s
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
@@ -290,4 +296,23 @@ func (s Resource) ApplyToCRD(crd *apiext.CustomResourceDefinitionSpec, version s
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

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

@@ -84,6 +84,39 @@ func (Format) Help() *markers.DefinitionHelp {
}
}
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",
@@ -244,8 +277,8 @@ func (Resource) Help() *markers.DefinitionHelp {
Details: "The singular form is otherwise defaulted off the plural (path).",
},
"Scope": markers.DetailedHelp{
Summary: "overrides the scope of the CRD (cluster vs namespaced). ",
Details: "Scope defaults to \"namespaced\". Cluster-scoped (\"cluster\") resources don't exist in namespaces.",
Summary: "overrides the scope of the CRD (Cluster vs Namespaced). ",
Details: "Scope defaults to \"Namespaced\". Cluster-scoped (\"Cluster\") resources don't exist in namespaces.",
},
},
}
@@ -273,6 +306,17 @@ func (StorageVersion) Help() *markers.DefinitionHelp {
}
}
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",
@@ -330,6 +374,17 @@ func (UniqueItems) Help() *markers.DefinitionHelp {
}
}
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",

View File

@@ -73,6 +73,20 @@ type Parser struct {
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
}
func (p *Parser) init() {
@@ -162,7 +176,7 @@ func (p *Parser) NeedSchemaFor(typ TypeIdent) {
// avoid tripping recursive schemata, like ManagedFields, by adding an empty WIP schema
p.Schemata[typ] = apiext.JSONSchemaProps{}
schemaCtx := newSchemaContext(typ.Package, p)
schemaCtx := newSchemaContext(typ.Package, p, p.AllowDangerousTypes)
ctxForInfo := schemaCtx.ForInfo(info)
pkgMarkers, err := markers.PackageMarkers(p.Collector, typ.Package)

View File

@@ -19,6 +19,7 @@ package crd
import (
"fmt"
"go/ast"
"go/token"
"go/types"
"strings"
@@ -67,15 +68,18 @@ type schemaContext struct {
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) *schemaContext {
func newSchemaContext(pkg *loader.Package, req schemaRequester, allowDangerousTypes bool) *schemaContext {
pkg.NeedTypesInfo()
return &schemaContext{
pkg: pkg,
schemaRequester: req,
pkg: pkg,
schemaRequester: req,
allowDangerousTypes: allowDangerousTypes,
}
}
@@ -83,9 +87,10 @@ func newSchemaContext(pkg *loader.Package, req schemaRequester) *schemaContext {
// 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,
pkg: c.pkg,
info: info,
schemaRequester: c.schemaRequester,
allowDangerousTypes: c.allowDangerousTypes,
}
}
@@ -104,6 +109,11 @@ func (c *schemaContext) requestSchema(pkgPath, typeName string) {
// infoToSchema creates a schema for the type in the given set of type information.
func infoToSchema(ctx *schemaContext) *apiext.JSONSchemaProps {
if obj := ctx.pkg.Types.Scope().Lookup(ctx.info.Name); obj != nil && implementsJSONMarshaler(obj.Type()) {
schema := &apiext.JSONSchemaProps{Type: "Any"}
applyMarkers(ctx, ctx.info.Markers, schema, ctx.info.RawSpec.Type)
return schema
}
return typeToSchema(ctx, ctx.info.RawSpec.Type)
}
@@ -200,7 +210,7 @@ func localNamedToSchema(ctx *schemaContext, ident *ast.Ident) *apiext.JSONSchema
return &apiext.JSONSchemaProps{}
}
if basicInfo, isBasic := typeInfo.(*types.Basic); isBasic {
typ, fmt, err := builtinToType(basicInfo)
typ, fmt, err := builtinToType(basicInfo, ctx.allowDangerousTypes)
if err != nil {
ctx.pkg.AddError(loader.ErrFromNode(err, ident))
}
@@ -297,6 +307,8 @@ func mapToSchema(ctx *schemaContext, mapType *ast.MapType) *apiext.JSONSchemaPro
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{}
@@ -390,8 +402,8 @@ func structToSchema(ctx *schemaContext, structType *ast.StructType) *apiext.JSON
// 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.
func builtinToType(basic *types.Basic) (typ string, format string, err error) {
// 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.
@@ -403,6 +415,8 @@ func builtinToType(basic *types.Basic) (typ string, format string, err error) {
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())
@@ -417,3 +431,16 @@ func builtinToType(basic *types.Basic) (typ string, format string, err error) {
return typ, format, nil
}
// Open coded go/types representation of encoding/json.Marshaller
var jsonMarshaler = types.NewInterfaceType([]*types.Func{
types.NewFunc(token.NoPos, nil, "MarshalJSON",
types.NewSignature(nil, nil,
types.NewTuple(
types.NewVar(token.NoPos, nil, "", types.NewSlice(types.Universe.Lookup("byte").Type())),
types.NewVar(token.NoPos, nil, "", types.Universe.Lookup("error").Type())), false)),
}, nil).Complete()
func implementsJSONMarshaler(typ types.Type) bool {
return types.Implements(typ, jsonMarshaler) || types.Implements(types.NewPointer(typ), jsonMarshaler)
}

View File

@@ -44,16 +44,37 @@ 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).
// 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) {
subVisitor := w.visitor.Visit(schema)
if subVisitor == nil {
return
// 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
}
defer subVisitor.Visit(nil)
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)

View File

@@ -120,7 +120,7 @@ func (p *Parser) NeedCRDFor(groupKind schema.GroupKind, maxDescLen *int) {
}
}
// fix the name if the plural was changed (this is the form the name *has* to take, so no harm in chaning it).
// 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
@@ -151,6 +151,19 @@ func (p *Parser) NeedCRDFor(groupKind schema.GroupKind, maxDescLen *int) {
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.

View File

@@ -36,12 +36,20 @@ func (Generator) Help() *markers.DefinitionHelp {
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": markers.DetailedHelp{
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": markers.DetailedHelp{
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": markers.DetailedHelp{
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": markers.DetailedHelp{
Summary: "specifies the target API versions of the CRD type itself to generate. Defaults to v1beta1. ",
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+).",
},
},

View File

@@ -128,7 +128,7 @@ func protoFromOptions(optionsRegistry *markers.Registry, options []string) (prot
val, err := defn.Parse(rawOpt)
if err != nil {
return protoRuntime{}, fmt.Errorf("unable to parse option %q: %v", rawOpt[1:], err)
return protoRuntime{}, fmt.Errorf("unable to parse option %q: %w", rawOpt[1:], err)
}
switch val := val.(type) {

View File

@@ -243,6 +243,9 @@ func (l *loader) typeCheck(pkg *Package) {
// The imports map is keyed by import path.
importedPkg := pkg.Imports()[path]
if importedPkg == nil {
return nil, fmt.Errorf("package %q possibly creates an import loop", path)
}
// it's possible to have a call to check in parallel to a call to this
// if one package in the package graph gets its dependency filtered out,
@@ -255,10 +258,6 @@ func (l *loader) typeCheck(pkg *Package) {
importedPkg.Lock()
defer importedPkg.Unlock()
if importedPkg == nil {
return nil, fmt.Errorf("no package information for %q", path)
}
if importedPkg.Types != nil && importedPkg.Types.Complete() {
return importedPkg.Types, nil
}

View File

@@ -588,7 +588,7 @@ func ArgumentFromType(rawType reflect.Type) (Argument, error) {
arg.Type = SliceType
itemType, err := ArgumentFromType(rawType.Elem())
if err != nil {
return Argument{}, fmt.Errorf("bad slice item type: %v", err)
return Argument{}, fmt.Errorf("bad slice item type: %w", err)
}
arg.ItemType = &itemType
case reflect.Map:
@@ -598,7 +598,7 @@ func ArgumentFromType(rawType reflect.Type) (Argument, error) {
}
itemType, err := ArgumentFromType(rawType.Elem())
if err != nil {
return Argument{}, fmt.Errorf("bad slice item type: %v", err)
return Argument{}, fmt.Errorf("bad slice item type: %w", err)
}
arg.ItemType = &itemType
default:
@@ -720,7 +720,7 @@ func (d *Definition) loadFields() error {
argType, err := ArgumentFromType(field.Type)
if err != nil {
return fmt.Errorf("unable to extract type information for field %q: %v", field.Name, err)
return fmt.Errorf("unable to extract type information for field %q: %w", field.Name, err)
}
if argType.Type == RawType {

View File

@@ -19,7 +19,7 @@ limitations under the License.
//
// The markers take the form:
//
// +kubebuilder:rbac:groups=<groups>,resources=<resources>,verbs=<verbs>,urls=<non resource urls>
// +kubebuilder:rbac:groups=<groups>,resources=<resources>,resourceNames=<resource names>,verbs=<verbs>,urls=<non resource urls>
package rbac
import (
@@ -48,6 +48,11 @@ type Rule struct {
Groups []string `marker:",optional"`
// Resources specifies the API resources that this rule encompasses.
Resources []string `marker:",optional"`
// ResourceNames specifies the names of the API resources that this rule encompasses.
//
// Create requests cannot be restricted by resourcename, as the object's name
// is not known at authorization time.
ResourceNames []string `marker:",optional"`
// Verbs specifies the (lowercase) kubernetes API verbs that this rule encompasses.
Verbs []string
// URL specifies the non-resource URLs that this rule encompasses.
@@ -60,13 +65,14 @@ type Rule struct {
// ruleKey represents the resources and non-resources a Rule applies.
type ruleKey struct {
Groups string
Resources string
URLs string
Groups string
Resources string
ResourceNames string
URLs string
}
func (key ruleKey) String() string {
return fmt.Sprintf("%s + %s + %s", key.Groups, key.Resources, key.URLs)
return fmt.Sprintf("%s + %s + %s + %s", key.Groups, key.Resources, key.ResourceNames, key.URLs)
}
// ruleKeys implements sort.Interface
@@ -80,9 +86,10 @@ func (keys ruleKeys) Less(i, j int) bool { return keys[i].String() < keys[j].Str
func (r *Rule) key() ruleKey {
r.normalize()
return ruleKey{
Groups: strings.Join(r.Groups, "&"),
Resources: strings.Join(r.Resources, "&"),
URLs: strings.Join(r.URLs, "&"),
Groups: strings.Join(r.Groups, "&"),
Resources: strings.Join(r.Resources, "&"),
ResourceNames: strings.Join(r.ResourceNames, "&"),
URLs: strings.Join(r.URLs, "&"),
}
}
@@ -96,6 +103,7 @@ func (r *Rule) addVerbs(verbs []string) {
func (r *Rule) normalize() {
r.Groups = removeDupAndSort(r.Groups)
r.Resources = removeDupAndSort(r.Resources)
r.ResourceNames = removeDupAndSort(r.ResourceNames)
r.Verbs = removeDupAndSort(r.Verbs)
r.URLs = removeDupAndSort(r.URLs)
}
@@ -130,6 +138,7 @@ func (r *Rule) ToRule() rbacv1.PolicyRule {
APIGroups: r.Groups,
Verbs: r.Verbs,
Resources: r.Resources,
ResourceNames: r.ResourceNames,
NonResourceURLs: r.URLs,
}
}

View File

@@ -56,6 +56,10 @@ func (Rule) Help() *markers.DefinitionHelp {
Summary: "specifies the API resources that this rule encompasses.",
Details: "",
},
"ResourceNames": markers.DetailedHelp{
Summary: "specifies the names of the API resources that this rule encompasses. ",
Details: "Create requests cannot be restricted by resourcename, as the object's name is not known at authorization time.",
},
"Verbs": markers.DetailedHelp{
Summary: "specifies the (lowercase) kubernetes API verbs that this rule encompasses.",
Details: "",

View File

@@ -115,7 +115,7 @@ func (g Generator) Generate(ctx *genall.GenerationContext) (result error) {
}
// generate schemata for the types we care about, and save them to be written later.
for _, groupKind := range crdgen.FindKubeKinds(parser, metav1Pkg) {
for groupKind := range crdgen.FindKubeKinds(parser, metav1Pkg) {
existingSet, wanted := partialCRDSets[groupKind]
if !wanted {
continue
@@ -170,11 +170,11 @@ func (g Generator) Generate(ctx *genall.GenerationContext) (result error) {
if allSame {
if err := existingSet.setGlobalSchema(); err != nil {
return fmt.Errorf("failed to set global firstSchema for %s: %v", existingSet.GroupKind, err)
return fmt.Errorf("failed to set global firstSchema for %s: %w", existingSet.GroupKind, err)
}
} else {
if err := existingSet.setVersionedSchemata(); err != nil {
return fmt.Errorf("failed to set versioned schemas for %s: %v", existingSet.GroupKind, err)
return fmt.Errorf("failed to set versioned schemas for %s: %w", existingSet.GroupKind, err)
}
}
}
@@ -278,7 +278,7 @@ func (e *partialCRD) setGlobalSchema(newSchema apiext.JSONSchemaProps) error {
}
schema, err := legacySchema(newSchema)
if err != nil {
return fmt.Errorf("failed to convert schema to legacy form: %v", err)
return fmt.Errorf("failed to convert schema to legacy form: %w", err)
}
schemaNodeTree, err := yamlop.ToYAML(schema)
if err != nil {
@@ -300,7 +300,7 @@ func (e *partialCRD) setGlobalSchema(newSchema apiext.JSONSchemaProps) error {
}
for i, verNode := range versions.Content {
if err := yamlop.DeleteNode(verNode, "schema"); err != nil {
return fmt.Errorf("spec.versions[%d]: %v", i, err)
return fmt.Errorf("spec.versions[%d]: %w", i, err)
}
}
@@ -363,7 +363,7 @@ func (e *partialCRD) setVersionedSchemata(newSchemata map[string]apiext.JSONSche
newSchema, found := newSchemata[name]
if !found {
if err := yamlop.DeleteNode(verNode, "schema"); err != nil {
return fmt.Errorf("spec.versions[%d]: %v", i, err)
return fmt.Errorf("spec.versions[%d]: %w", i, err)
}
} else {
// TODO(directxman12): if this gets to be more than 2 versions, use polymorphism to clean this up
@@ -371,18 +371,18 @@ func (e *partialCRD) setVersionedSchemata(newSchemata map[string]apiext.JSONSche
if e.CRDVersion == legacyAPIExtVersion {
verSchema, err = legacySchema(newSchema)
if err != nil {
return fmt.Errorf("failed to convert schema to legacy form: %v", err)
return fmt.Errorf("failed to convert schema to legacy form: %w", err)
}
}
schemaNodeTree, err := yamlop.ToYAML(verSchema)
if err != nil {
return fmt.Errorf("failed to convert schema to YAML: %v", err)
return fmt.Errorf("failed to convert schema to YAML: %w", err)
}
schemaNodeTree = schemaNodeTree.Content[0] // get rid of the document node
yamlop.SetStyle(schemaNodeTree, 0) // clear the style so it defaults to an auto-chosen one
if err := yamlop.SetNode(verNode, *schemaNodeTree, "schema", "openAPIV3Schema"); err != nil {
return fmt.Errorf("spec.versions[%d]: %v", i, err)
return fmt.Errorf("spec.versions[%d]: %w", i, err)
}
}
}

View File

@@ -33,12 +33,12 @@ func ToYAML(rawObj interface{}) (*yaml.Node, error) {
rawJSON, err := json.Marshal(rawObj)
if err != nil {
return nil, fmt.Errorf("failed to marshal object: %v", err)
return nil, fmt.Errorf("failed to marshal object: %w", err)
}
var out yaml.Node
if err := yaml.Unmarshal(rawJSON, &out); err != nil {
return nil, fmt.Errorf("unable to unmarshal marshalled object: %v", err)
return nil, fmt.Errorf("unable to unmarshal marshalled object: %w", err)
}
return &out, nil
}

View File

@@ -58,7 +58,7 @@ func asCloseAsPossible(root *yaml.Node, path ...string) (*yaml.Node, []string, e
nextNode, err := ValueInMapping(currNode, path[0])
if err != nil {
return nil, nil, fmt.Errorf("unable to get next node in path %v: %v", path, err)
return nil, nil, fmt.Errorf("unable to get next node in path %v: %w", path, err)
}
if nextNode == nil {

View File

@@ -0,0 +1,44 @@
/*
Copyright 2020 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 webhook
import (
admissionregv1 "k8s.io/api/admissionregistration/v1"
admissionregv1beta1 "k8s.io/api/admissionregistration/v1beta1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
)
var (
conversionScheme = runtime.NewScheme()
)
func init() {
utilruntime.Must(admissionregv1.AddToScheme(conversionScheme))
utilruntime.Must(admissionregv1beta1.AddToScheme(conversionScheme))
}
// MutatingWebhookConfigurationAsVersion converts a MutatingWebhookConfiguration from the canonical internal form (currently v1) to some external form.
func MutatingWebhookConfigurationAsVersion(original *admissionregv1.MutatingWebhookConfiguration, gv schema.GroupVersion) (runtime.Object, error) {
return conversionScheme.ConvertToVersion(original, gv)
}
// ValidatingWebhookConfigurationAsVersion converts a ValidatingWebhookConfiguration from the canonical internal form (currently v1) to some external form.
func ValidatingWebhookConfigurationAsVersion(original *admissionregv1.ValidatingWebhookConfiguration, gv schema.GroupVersion) (runtime.Object, error) {
return conversionScheme.ConvertToVersion(original, gv)
}

View File

@@ -19,26 +19,38 @@ limitations under the License.
//
// The markers take the form:
//
// +kubebuilder:webhook:failurePolicy=<string>,groups=<[]string>,resources=<[]string>,verbs=<[]string>,versions=<[]string>,name=<string>,path=<string>,mutating=<bool>
// +kubebuilder:webhook:webhookVersions=<[]string>,failurePolicy=<string>,matchPolicy=<string>,groups=<[]string>,resources=<[]string>,verbs=<[]string>,versions=<[]string>,name=<string>,path=<string>,mutating=<bool>,sideEffects=<string>
package webhook
import (
"fmt"
"strings"
admissionreg "k8s.io/api/admissionregistration/v1beta1"
admissionregv1 "k8s.io/api/admissionregistration/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/util/sets"
"sigs.k8s.io/controller-tools/pkg/genall"
"sigs.k8s.io/controller-tools/pkg/markers"
)
// The default {Mutating,Validating}WebhookConfiguration version to generate.
const (
defaultWebhookVersion = "v1"
)
var (
// ConfigDefinition s a marker for defining Webhook manifests.
// Call ToWebhook on the value to get a Kubernetes Webhook.
ConfigDefinition = markers.Must(markers.MakeDefinition("kubebuilder:webhook", markers.DescribesPackage, Config{}))
)
// supportedWebhookVersions returns currently supported API version of {Mutating,Validating}WebhookConfiguration.
func supportedWebhookVersions() []string {
return []string{defaultWebhookVersion, "v1beta1"}
}
// +controllertools:marker:generateHelp:category=Webhook
// Config specifies how a webhook should be served.
@@ -57,6 +69,17 @@ type Config struct {
// It may be either "ignore" (to skip the webhook and continue on) or "fail" (to reject
// the object in question).
FailurePolicy string
// MatchPolicy defines how the "rules" list is used to match incoming requests.
// Allowed values are "Exact" (match only if it exactly matches the specified rule)
// or "Equivalent" (match a request if it modifies a resource listed in rules, even via another API group or version).
MatchPolicy string `marker:",optional"`
// SideEffects specify whether calling the webhook will have side effects.
// This has an impact on dry runs and `kubectl diff`: if the sideEffect is "Unknown" (the default) or "Some", then
// the API server will not call the webhook on a dry-run request and fails instead.
// If the value is "None", then the webhook has no side effects and the API server will call it on dry-run.
// If the value is "NoneOnDryRun", then the webhook is responsible for inspecting the "dryRun" property of the
// AdmissionReview sent in the request, and avoiding side effects if that value is "true."
SideEffects string `marker:",optional"`
// Groups specifies the API groups that this webhook receives requests for.
Groups []string
@@ -70,69 +93,99 @@ type Config struct {
// Versions specifies the API versions that this webhook receives requests for.
Versions []string
// Name indicates the name of this webhook configuration.
// Name indicates the name of this webhook configuration. Should be a domain with at least three segments separated by dots
Name string
// Path specifies that path that the API server should connect to this webhook on.
// Path specifies that path that the API server should connect to this webhook on. Must be
// prefixed with a '/validate-' or '/mutate-' depending on the type, and followed by
// $GROUP-$VERSION-$KIND where all values are lower-cased and the periods in the group
// are substituted for hyphens. For example, a validating webhook path for type
// batch.tutorial.kubebuilder.io/v1,Kind=CronJob would be
// /validate-batch-tutorial-kubebuilder-io-v1-cronjob
Path string
// WebhookVersions specifies the target API versions of the {Mutating,Validating}WebhookConfiguration objects
// itself to generate. Defaults to v1.
WebhookVersions []string `marker:"webhookVersions,optional"`
}
// verbToAPIVariant converts a marker's verb to the proper value for the API.
// Unrecognized verbs are passed through.
func verbToAPIVariant(verbRaw string) admissionreg.OperationType {
func verbToAPIVariant(verbRaw string) admissionregv1.OperationType {
switch strings.ToLower(verbRaw) {
case strings.ToLower(string(admissionreg.Create)):
return admissionreg.Create
case strings.ToLower(string(admissionreg.Update)):
return admissionreg.Update
case strings.ToLower(string(admissionreg.Delete)):
return admissionreg.Delete
case strings.ToLower(string(admissionreg.Connect)):
return admissionreg.Connect
case strings.ToLower(string(admissionreg.OperationAll)):
return admissionreg.OperationAll
case strings.ToLower(string(admissionregv1.Create)):
return admissionregv1.Create
case strings.ToLower(string(admissionregv1.Update)):
return admissionregv1.Update
case strings.ToLower(string(admissionregv1.Delete)):
return admissionregv1.Delete
case strings.ToLower(string(admissionregv1.Connect)):
return admissionregv1.Connect
case strings.ToLower(string(admissionregv1.OperationAll)):
return admissionregv1.OperationAll
default:
return admissionreg.OperationType(verbRaw)
return admissionregv1.OperationType(verbRaw)
}
}
// ToMutatingWebhook converts this rule to its Kubernetes API form.
func (c Config) ToMutatingWebhook() (admissionreg.MutatingWebhook, error) {
func (c Config) ToMutatingWebhook() (admissionregv1.MutatingWebhook, error) {
if !c.Mutating {
return admissionreg.MutatingWebhook{}, fmt.Errorf("%s is a validating webhook", c.Name)
return admissionregv1.MutatingWebhook{}, fmt.Errorf("%s is a validating webhook", c.Name)
}
return admissionreg.MutatingWebhook{
matchPolicy, err := c.matchPolicy()
if err != nil {
return admissionregv1.MutatingWebhook{}, err
}
return admissionregv1.MutatingWebhook{
Name: c.Name,
Rules: c.rules(),
FailurePolicy: c.failurePolicy(),
MatchPolicy: matchPolicy,
ClientConfig: c.clientConfig(),
SideEffects: c.sideEffects(),
// TODO(jiachengxu): AdmissionReviewVersions becomes required in admissionregistration/v1, here we default it
// to `v1` and `v1beta1`, and we should support to config the `AdmissionReviewVersions` as a marker.
AdmissionReviewVersions: []string{defaultWebhookVersion, "v1beta1"},
}, nil
}
// ToValidatingWebhook converts this rule to its Kubernetes API form.
func (c Config) ToValidatingWebhook() (admissionreg.ValidatingWebhook, error) {
func (c Config) ToValidatingWebhook() (admissionregv1.ValidatingWebhook, error) {
if c.Mutating {
return admissionreg.ValidatingWebhook{}, fmt.Errorf("%s is a mutating webhook", c.Name)
return admissionregv1.ValidatingWebhook{}, fmt.Errorf("%s is a mutating webhook", c.Name)
}
return admissionreg.ValidatingWebhook{
matchPolicy, err := c.matchPolicy()
if err != nil {
return admissionregv1.ValidatingWebhook{}, err
}
return admissionregv1.ValidatingWebhook{
Name: c.Name,
Rules: c.rules(),
FailurePolicy: c.failurePolicy(),
MatchPolicy: matchPolicy,
ClientConfig: c.clientConfig(),
SideEffects: c.sideEffects(),
// TODO(jiachengxu): AdmissionReviewVersions becomes required in admissionregistration/v1, here we default it
// to `v1` and `v1beta1`, and we should support to config the `AdmissionReviewVersions` as a marker.
AdmissionReviewVersions: []string{defaultWebhookVersion, "v1beta1"},
}, nil
}
// rules returns the configuration of what operations on what
// resources/subresources a webhook should care about.
func (c Config) rules() []admissionreg.RuleWithOperations {
whConfig := admissionreg.RuleWithOperations{
Rule: admissionreg.Rule{
func (c Config) rules() []admissionregv1.RuleWithOperations {
whConfig := admissionregv1.RuleWithOperations{
Rule: admissionregv1.Rule{
APIGroups: c.Groups,
APIVersions: c.Versions,
Resources: c.Resources,
},
Operations: make([]admissionreg.OperationType, len(c.Verbs)),
Operations: make([]admissionregv1.OperationType, len(c.Verbs)),
}
for i, verbRaw := range c.Verbs {
@@ -146,29 +199,45 @@ func (c Config) rules() []admissionreg.RuleWithOperations {
}
}
return []admissionreg.RuleWithOperations{whConfig}
return []admissionregv1.RuleWithOperations{whConfig}
}
// failurePolicy converts the string value to the proper value for the API.
// Unrecognized values are passed through.
func (c Config) failurePolicy() *admissionreg.FailurePolicyType {
var failurePolicy admissionreg.FailurePolicyType
func (c Config) failurePolicy() *admissionregv1.FailurePolicyType {
var failurePolicy admissionregv1.FailurePolicyType
switch strings.ToLower(c.FailurePolicy) {
case strings.ToLower(string(admissionreg.Ignore)):
failurePolicy = admissionreg.Ignore
case strings.ToLower(string(admissionreg.Fail)):
failurePolicy = admissionreg.Fail
case strings.ToLower(string(admissionregv1.Ignore)):
failurePolicy = admissionregv1.Ignore
case strings.ToLower(string(admissionregv1.Fail)):
failurePolicy = admissionregv1.Fail
default:
failurePolicy = admissionreg.FailurePolicyType(c.FailurePolicy)
failurePolicy = admissionregv1.FailurePolicyType(c.FailurePolicy)
}
return &failurePolicy
}
// matchPolicy converts the string value to the proper value for the API.
func (c Config) matchPolicy() (*admissionregv1.MatchPolicyType, error) {
var matchPolicy admissionregv1.MatchPolicyType
switch strings.ToLower(c.MatchPolicy) {
case strings.ToLower(string(admissionregv1.Exact)):
matchPolicy = admissionregv1.Exact
case strings.ToLower(string(admissionregv1.Equivalent)):
matchPolicy = admissionregv1.Equivalent
case "":
return nil, nil
default:
return nil, fmt.Errorf("unknown value %q for matchPolicy", c.MatchPolicy)
}
return &matchPolicy, nil
}
// clientConfig returns the client config for a webhook.
func (c Config) clientConfig() admissionreg.WebhookClientConfig {
func (c Config) clientConfig() admissionregv1.WebhookClientConfig {
path := c.Path
return admissionreg.WebhookClientConfig{
Service: &admissionreg.ServiceReference{
return admissionregv1.WebhookClientConfig{
Service: &admissionregv1.ServiceReference{
Name: "webhook-service",
Namespace: "system",
Path: &path,
@@ -180,6 +249,39 @@ func (c Config) clientConfig() admissionreg.WebhookClientConfig {
}
}
// sideEffects returns the sideEffects config for a webhook.
func (c Config) sideEffects() *admissionregv1.SideEffectClass {
var sideEffects admissionregv1.SideEffectClass
switch strings.ToLower(c.SideEffects) {
case strings.ToLower(string(admissionregv1.SideEffectClassNone)):
sideEffects = admissionregv1.SideEffectClassNone
case strings.ToLower(string(admissionregv1.SideEffectClassNoneOnDryRun)):
sideEffects = admissionregv1.SideEffectClassNoneOnDryRun
case strings.ToLower(string(admissionregv1.SideEffectClassSome)):
sideEffects = admissionregv1.SideEffectClassSome
case "":
return nil
default:
return nil
}
return &sideEffects
}
// webhookVersions returns the target API versions of the {Mutating,Validating}WebhookConfiguration objects for a webhook.
func (c Config) webhookVersions() ([]string, error) {
// If WebhookVersions is not specified, we default it to `v1`.
if len(c.WebhookVersions) == 0 {
return []string{defaultWebhookVersion}, nil
}
supportedWebhookVersions := sets.NewString(supportedWebhookVersions()...)
for _, version := range c.WebhookVersions {
if !supportedWebhookVersions.Has(version) {
return nil, fmt.Errorf("unsupported webhook version: %s", version)
}
}
return sets.NewString(c.WebhookVersions...).UnsortedList(), nil
}
// +controllertools:marker:generateHelp
// Generator generates (partial) {Mutating,Validating}WebhookConfiguration objects.
@@ -194,8 +296,9 @@ func (Generator) RegisterMarkers(into *markers.Registry) error {
}
func (Generator) Generate(ctx *genall.GenerationContext) error {
var mutatingCfgs []admissionreg.MutatingWebhook
var validatingCfgs []admissionreg.ValidatingWebhook
supportedWebhookVersions := supportedWebhookVersions()
mutatingCfgs := make(map[string][]admissionregv1.MutatingWebhook, len(supportedWebhookVersions))
validatingCfgs := make(map[string][]admissionregv1.ValidatingWebhook, len(supportedWebhookVersions))
for _, root := range ctx.Roots {
markerSet, err := markers.PackageMarkers(ctx.Collector, root)
if err != nil {
@@ -204,47 +307,128 @@ func (Generator) Generate(ctx *genall.GenerationContext) error {
for _, cfg := range markerSet[ConfigDefinition.Name] {
cfg := cfg.(Config)
webhookVersions, err := cfg.webhookVersions()
if err != nil {
return err
}
if cfg.Mutating {
w, _ := cfg.ToMutatingWebhook()
mutatingCfgs = append(mutatingCfgs, w)
w, err := cfg.ToMutatingWebhook()
if err != nil {
return err
}
for _, webhookVersion := range webhookVersions {
mutatingCfgs[webhookVersion] = append(mutatingCfgs[webhookVersion], w)
}
} else {
w, _ := cfg.ToValidatingWebhook()
validatingCfgs = append(validatingCfgs, w)
w, err := cfg.ToValidatingWebhook()
if err != nil {
return err
}
for _, webhookVersion := range webhookVersions {
validatingCfgs[webhookVersion] = append(validatingCfgs[webhookVersion], w)
}
}
}
}
var objs []interface{}
if len(mutatingCfgs) > 0 {
objs = append(objs, &admissionreg.MutatingWebhookConfiguration{
TypeMeta: metav1.TypeMeta{
Kind: "MutatingWebhookConfiguration",
APIVersion: admissionreg.SchemeGroupVersion.String(),
},
ObjectMeta: metav1.ObjectMeta{
Name: "mutating-webhook-configuration",
},
Webhooks: mutatingCfgs,
})
versionedWebhooks := make(map[string][]interface{}, len(supportedWebhookVersions))
for _, version := range supportedWebhookVersions {
if cfgs, ok := mutatingCfgs[version]; ok {
objRaw := &admissionregv1.MutatingWebhookConfiguration{
TypeMeta: metav1.TypeMeta{
Kind: "MutatingWebhookConfiguration",
APIVersion: admissionregv1.SchemeGroupVersion.String(),
},
ObjectMeta: metav1.ObjectMeta{
Name: "mutating-webhook-configuration",
},
Webhooks: cfgs,
}
// SideEffects in required in admissionregistration/v1, if this is not set or set to `Some` or `Known`,
// we return an error
if version == defaultWebhookVersion {
for i := range objRaw.Webhooks {
if err := checkSideEffectsForV1(objRaw.Webhooks[i].SideEffects); err != nil {
return err
}
}
}
// AdmissionReviewVersions is optional in admissionregistration/v1beta1, so let kubernetes to default it.
if version == "v1beta1" {
for i := range objRaw.Webhooks {
objRaw.Webhooks[i].AdmissionReviewVersions = nil
}
}
if version != defaultWebhookVersion {
conv, err := MutatingWebhookConfigurationAsVersion(objRaw, schema.GroupVersion{Group: admissionregv1.SchemeGroupVersion.Group, Version: version})
versionedWebhooks[version] = append(versionedWebhooks[version], conv)
if err != nil {
return err
}
} else {
versionedWebhooks[version] = append(versionedWebhooks[version], objRaw)
}
}
if cfgs, ok := validatingCfgs[version]; ok {
objRaw := &admissionregv1.ValidatingWebhookConfiguration{
TypeMeta: metav1.TypeMeta{
Kind: "ValidatingWebhookConfiguration",
APIVersion: admissionregv1.SchemeGroupVersion.String(),
},
ObjectMeta: metav1.ObjectMeta{
Name: "validating-webhook-configuration",
},
Webhooks: cfgs,
}
// SideEffects in required in admissionregistration/v1, if this is not set or set to `Some` or `Known`,
// we return an error
if version == defaultWebhookVersion {
for i := range objRaw.Webhooks {
if err := checkSideEffectsForV1(objRaw.Webhooks[i].SideEffects); err != nil {
return err
}
}
}
// AdmissionReviewVersions is optional in admissionregistration/v1beta1, so let kubernetes to default it.
if version == "v1beta1" {
for i := range objRaw.Webhooks {
objRaw.Webhooks[i].AdmissionReviewVersions = nil
}
}
if version != defaultWebhookVersion {
conv, err := ValidatingWebhookConfigurationAsVersion(objRaw, schema.GroupVersion{Group: admissionregv1.SchemeGroupVersion.Group, Version: version})
versionedWebhooks[version] = append(versionedWebhooks[version], conv)
if err != nil {
return err
}
} else {
versionedWebhooks[version] = append(versionedWebhooks[version], objRaw)
}
}
}
if len(validatingCfgs) > 0 {
objs = append(objs, &admissionreg.ValidatingWebhookConfiguration{
TypeMeta: metav1.TypeMeta{
Kind: "ValidatingWebhookConfiguration",
APIVersion: admissionreg.SchemeGroupVersion.String(),
},
ObjectMeta: metav1.ObjectMeta{
Name: "validating-webhook-configuration",
},
Webhooks: validatingCfgs,
})
for k, v := range versionedWebhooks {
var fileName string
if k == defaultWebhookVersion {
fileName = fmt.Sprintf("manifests.yaml")
} else {
fileName = fmt.Sprintf("manifests.%s.yaml", k)
}
if err := ctx.WriteYAML(fileName, v...); err != nil {
return err
}
}
return nil
}
func checkSideEffectsForV1(sideEffects *admissionregv1.SideEffectClass) error {
if sideEffects == nil {
return fmt.Errorf("SideEffects is required for creating v1 {Mutating,Validating}WebhookConfiguration")
}
if *sideEffects == admissionregv1.SideEffectClassUnknown ||
*sideEffects == admissionregv1.SideEffectClassSome {
return fmt.Errorf("SideEffects should not be set to `Some` or `Unknown` for v1 {Mutating,Validating}WebhookConfiguration")
}
if err := ctx.WriteYAML("manifests.yaml", objs...); err != nil {
return err
}
return nil
}

View File

@@ -40,6 +40,14 @@ func (Config) Help() *markers.DefinitionHelp {
Summary: "specifies what should happen if the API server cannot reach the webhook. ",
Details: "It may be either \"ignore\" (to skip the webhook and continue on) or \"fail\" (to reject the object in question).",
},
"MatchPolicy": markers.DetailedHelp{
Summary: "defines how the \"rules\" list is used to match incoming requests. Allowed values are \"Exact\" (match only if it exactly matches the specified rule) or \"Equivalent\" (match a request if it modifies a resource listed in rules, even via another API group or version).",
Details: "",
},
"SideEffects": markers.DetailedHelp{
Summary: "specify whether calling the webhook will have side effects. This has an impact on dry runs and `kubectl diff`: if the sideEffect is \"Unknown\" (the default) or \"Some\", then the API server will not call the webhook on a dry-run request and fails instead. If the value is \"None\", then the webhook has no side effects and the API server will call it on dry-run. If the value is \"NoneOnDryRun\", then the webhook is responsible for inspecting the \"dryRun\" property of the AdmissionReview sent in the request, and avoiding side effects if that value is \"true.\"",
Details: "",
},
"Groups": markers.DetailedHelp{
Summary: "specifies the API groups that this webhook receives requests for.",
Details: "",
@@ -57,11 +65,15 @@ func (Config) Help() *markers.DefinitionHelp {
Details: "",
},
"Name": markers.DetailedHelp{
Summary: "indicates the name of this webhook configuration.",
Summary: "indicates the name of this webhook configuration. Should be a domain with at least three segments separated by dots",
Details: "",
},
"Path": markers.DetailedHelp{
Summary: "specifies that path that the API server should connect to this webhook on.",
Summary: "specifies that path that the API server should connect to this webhook on. Must be prefixed with a '/validate-' or '/mutate-' depending on the type, and followed by $GROUP-$VERSION-$KIND where all values are lower-cased and the periods in the group are substituted for hyphens. For example, a validating webhook path for type batch.tutorial.kubebuilder.io/v1,Kind=CronJob would be /validate-batch-tutorial-kubebuilder-io-v1-cronjob",
Details: "",
},
"WebhookVersions": markers.DetailedHelp{
Summary: "specifies the target API versions of the {Mutating,Validating}WebhookConfiguration objects itself to generate. Defaults to v1.",
Details: "",
},
},