github.com/regadas/controller-tools@v0.5.1-0.20210408091555-18885b17ff7b/pkg/crd/gen.go (about)

     1  /*
     2  Copyright 2018 The Kubernetes Authors.
     3  
     4  Licensed under the Apache License, Version 2.0 (the "License");
     5  you may not use this file except in compliance with the License.
     6  You may obtain a copy of the License at
     7  
     8      http://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreed to in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  See the License for the specific language governing permissions and
    14  limitations under the License.
    15  */
    16  
    17  package crd
    18  
    19  import (
    20  	"fmt"
    21  	"go/ast"
    22  	"go/types"
    23  	"os"
    24  
    25  	apiext "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
    26  	apiextlegacy "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1"
    27  	"k8s.io/apimachinery/pkg/runtime/schema"
    28  
    29  	crdmarkers "github.com/regadas/controller-tools/pkg/crd/markers"
    30  	"github.com/regadas/controller-tools/pkg/genall"
    31  	"github.com/regadas/controller-tools/pkg/loader"
    32  	"github.com/regadas/controller-tools/pkg/markers"
    33  	"github.com/regadas/controller-tools/pkg/version"
    34  )
    35  
    36  // The default CustomResourceDefinition version to generate.
    37  const defaultVersion = "v1"
    38  
    39  // +controllertools:marker:generateHelp
    40  
    41  // Generator generates CustomResourceDefinition objects.
    42  type Generator struct {
    43  	// TrivialVersions indicates that we should produce a single-version CRD.
    44  	//
    45  	// Single "trivial-version" CRDs are compatible with older (pre 1.13)
    46  	// Kubernetes API servers.  The storage version's schema will be used as
    47  	// the CRD's schema.
    48  	//
    49  	// Only works with the v1beta1 CRD version.
    50  	TrivialVersions bool `marker:",optional"`
    51  
    52  	// PreserveUnknownFields indicates whether or not we should turn off pruning.
    53  	//
    54  	// Left unspecified, it'll default to true when only a v1beta1 CRD is
    55  	// generated (to preserve compatibility with older versions of this tool),
    56  	// or false otherwise.
    57  	//
    58  	// It's required to be false for v1 CRDs.
    59  	PreserveUnknownFields *bool `marker:",optional"`
    60  
    61  	// AllowDangerousTypes allows types which are usually omitted from CRD generation
    62  	// because they are not recommended.
    63  	//
    64  	// Currently the following additional types are allowed when this is true:
    65  	// float32
    66  	// float64
    67  	//
    68  	// Left unspecified, the default is false
    69  	AllowDangerousTypes *bool `marker:",optional"`
    70  
    71  	// MaxDescLen specifies the maximum description length for fields in CRD's OpenAPI schema.
    72  	//
    73  	// 0 indicates drop the description for all fields completely.
    74  	// n indicates limit the description to at most n characters and truncate the description to
    75  	// closest sentence boundary if it exceeds n characters.
    76  	MaxDescLen *int `marker:",optional"`
    77  
    78  	// CRDVersions specifies the target API versions of the CRD type itself to
    79  	// generate. Defaults to v1.
    80  	//
    81  	// The first version listed will be assumed to be the "default" version and
    82  	// will not get a version suffix in the output filename.
    83  	//
    84  	// You'll need to use "v1" to get support for features like defaulting,
    85  	// along with an API server that supports it (Kubernetes 1.16+).
    86  	CRDVersions []string `marker:"crdVersions,optional"`
    87  
    88  	// GenerateEmbeddedObjectMeta specifies if any embedded ObjectMeta in the CRD should be generated
    89  	GenerateEmbeddedObjectMeta *bool `marker:",optional"`
    90  }
    91  
    92  func (Generator) CheckFilter() loader.NodeFilter {
    93  	return filterTypesForCRDs
    94  }
    95  func (Generator) RegisterMarkers(into *markers.Registry) error {
    96  	return crdmarkers.Register(into)
    97  }
    98  func (g Generator) Generate(ctx *genall.GenerationContext) error {
    99  	parser := &Parser{
   100  		Collector: ctx.Collector,
   101  		Checker:   ctx.Checker,
   102  		// Perform defaulting here to avoid ambiguity later
   103  		AllowDangerousTypes: g.AllowDangerousTypes != nil && *g.AllowDangerousTypes == true,
   104  		// Indicates the parser on whether to register the ObjectMeta type or not
   105  		GenerateEmbeddedObjectMeta: g.GenerateEmbeddedObjectMeta != nil && *g.GenerateEmbeddedObjectMeta == true,
   106  	}
   107  
   108  	AddKnownTypes(parser)
   109  	for _, root := range ctx.Roots {
   110  		parser.NeedPackage(root)
   111  	}
   112  
   113  	metav1Pkg := FindMetav1(ctx.Roots)
   114  	if metav1Pkg == nil {
   115  		// no objects in the roots, since nothing imported metav1
   116  		return nil
   117  	}
   118  
   119  	// TODO: allow selecting a specific object
   120  	kubeKinds := FindKubeKinds(parser, metav1Pkg)
   121  	if len(kubeKinds) == 0 {
   122  		// no objects in the roots
   123  		return nil
   124  	}
   125  
   126  	crdVersions := g.CRDVersions
   127  
   128  	if len(crdVersions) == 0 {
   129  		crdVersions = []string{defaultVersion}
   130  	}
   131  
   132  	for groupKind := range kubeKinds {
   133  		parser.NeedCRDFor(groupKind, g.MaxDescLen)
   134  		crdRaw := parser.CustomResourceDefinitions[groupKind]
   135  		addAttribution(&crdRaw)
   136  
   137  		// Prevent the top level metadata for the CRD to be generate regardless of the intention in the arguments
   138  		FixTopLevelMetadata(crdRaw)
   139  
   140  		versionedCRDs := make([]interface{}, len(crdVersions))
   141  		for i, ver := range crdVersions {
   142  			conv, err := AsVersion(crdRaw, schema.GroupVersion{Group: apiext.SchemeGroupVersion.Group, Version: ver})
   143  			if err != nil {
   144  				return err
   145  			}
   146  			versionedCRDs[i] = conv
   147  		}
   148  
   149  		if g.TrivialVersions {
   150  			for i, crd := range versionedCRDs {
   151  				if crdVersions[i] == "v1beta1" {
   152  					toTrivialVersions(crd.(*apiextlegacy.CustomResourceDefinition))
   153  				}
   154  			}
   155  		}
   156  
   157  		// *If* we're only generating v1beta1 CRDs, default to `preserveUnknownFields: (unset)`
   158  		// for compatibility purposes.  In any other case, default to false, since that's
   159  		// the sensible default and is required for v1.
   160  		v1beta1Only := len(crdVersions) == 1 && crdVersions[0] == "v1beta1"
   161  		switch {
   162  		case (g.PreserveUnknownFields == nil || *g.PreserveUnknownFields) && v1beta1Only:
   163  			crd := versionedCRDs[0].(*apiextlegacy.CustomResourceDefinition)
   164  			crd.Spec.PreserveUnknownFields = nil
   165  		case g.PreserveUnknownFields == nil, g.PreserveUnknownFields != nil && !*g.PreserveUnknownFields:
   166  			// it'll be false here (coming from v1) -- leave it as such
   167  		default:
   168  			return fmt.Errorf("you may only set PreserveUnknownFields to true with v1beta1 CRDs")
   169  		}
   170  
   171  		for i, crd := range versionedCRDs {
   172  			// defaults are not allowed to be specified in v1beta1 CRDs and
   173  			// decriptions are not allowed on the metadata regardless of version
   174  			// strip them before writing to a file
   175  			if crdVersions[i] == "v1beta1" {
   176  				removeDefaultsFromSchemas(crd.(*apiextlegacy.CustomResourceDefinition))
   177  				removeDescriptionFromMetadataLegacy(crd.(*apiextlegacy.CustomResourceDefinition))
   178  			} else {
   179  				removeDescriptionFromMetadata(crd.(*apiext.CustomResourceDefinition))
   180  			}
   181  			var fileName string
   182  			if i == 0 {
   183  				fileName = fmt.Sprintf("%s_%s.yaml", crdRaw.Spec.Group, crdRaw.Spec.Names.Plural)
   184  			} else {
   185  				fileName = fmt.Sprintf("%s_%s.%s.yaml", crdRaw.Spec.Group, crdRaw.Spec.Names.Plural, crdVersions[i])
   186  			}
   187  			if err := ctx.WriteYAML(fileName, crd); err != nil {
   188  				return err
   189  			}
   190  		}
   191  	}
   192  
   193  	return nil
   194  }
   195  
   196  func removeDescriptionFromMetadata(crd *apiext.CustomResourceDefinition) {
   197  	for _, versionSpec := range crd.Spec.Versions {
   198  		if versionSpec.Schema != nil {
   199  			removeDescriptionFromMetadataProps(versionSpec.Schema.OpenAPIV3Schema)
   200  		}
   201  	}
   202  }
   203  
   204  func removeDescriptionFromMetadataProps(v *apiext.JSONSchemaProps) {
   205  	if m, ok := v.Properties["metadata"]; ok {
   206  		meta := &m
   207  		if meta.Description != "" {
   208  			meta.Description = ""
   209  			v.Properties["metadata"] = m
   210  
   211  		}
   212  	}
   213  }
   214  
   215  func removeDescriptionFromMetadataLegacy(crd *apiextlegacy.CustomResourceDefinition) {
   216  	if crd.Spec.Validation != nil {
   217  		removeDescriptionFromMetadataPropsLegacy(crd.Spec.Validation.OpenAPIV3Schema)
   218  	}
   219  	for _, versionSpec := range crd.Spec.Versions {
   220  		if versionSpec.Schema != nil {
   221  			removeDescriptionFromMetadataPropsLegacy(versionSpec.Schema.OpenAPIV3Schema)
   222  		}
   223  	}
   224  }
   225  
   226  func removeDescriptionFromMetadataPropsLegacy(v *apiextlegacy.JSONSchemaProps) {
   227  	if m, ok := v.Properties["metadata"]; ok {
   228  		meta := &m
   229  		if meta.Description != "" {
   230  			meta.Description = ""
   231  			v.Properties["metadata"] = m
   232  
   233  		}
   234  	}
   235  }
   236  
   237  // removeDefaultsFromSchemas will remove all instances of default values being
   238  // specified across all defined API versions
   239  func removeDefaultsFromSchemas(crd *apiextlegacy.CustomResourceDefinition) {
   240  	if crd.Spec.Validation != nil {
   241  		removeDefaultsFromSchemaProps(crd.Spec.Validation.OpenAPIV3Schema)
   242  	}
   243  
   244  	for _, versionSpec := range crd.Spec.Versions {
   245  		if versionSpec.Schema != nil {
   246  			removeDefaultsFromSchemaProps(versionSpec.Schema.OpenAPIV3Schema)
   247  		}
   248  	}
   249  }
   250  
   251  // removeDefaultsFromSchemaProps will recurse into JSONSchemaProps to remove
   252  // all instances of default values being specified
   253  func removeDefaultsFromSchemaProps(v *apiextlegacy.JSONSchemaProps) {
   254  	if v == nil {
   255  		return
   256  	}
   257  
   258  	if v.Default != nil {
   259  		fmt.Fprintln(os.Stderr, "Warning: default unsupported in CRD version v1beta1, v1 required. Removing defaults.")
   260  	}
   261  
   262  	// nil-out the default field
   263  	v.Default = nil
   264  	for name, prop := range v.Properties {
   265  		// iter var reference is fine -- we handle the persistence of the modfications on the line below
   266  		//nolint:gosec
   267  		removeDefaultsFromSchemaProps(&prop)
   268  		v.Properties[name] = prop
   269  	}
   270  	if v.Items != nil {
   271  		removeDefaultsFromSchemaProps(v.Items.Schema)
   272  		for i := range v.Items.JSONSchemas {
   273  			props := v.Items.JSONSchemas[i]
   274  			removeDefaultsFromSchemaProps(&props)
   275  			v.Items.JSONSchemas[i] = props
   276  		}
   277  	}
   278  }
   279  
   280  // FixTopLevelMetadata resets the schema for the top-level metadata field which is needed for CRD validation
   281  func FixTopLevelMetadata(crd apiext.CustomResourceDefinition) {
   282  	for _, v := range crd.Spec.Versions {
   283  		if v.Schema != nil && v.Schema.OpenAPIV3Schema != nil && v.Schema.OpenAPIV3Schema.Properties != nil {
   284  			schemaProperties := v.Schema.OpenAPIV3Schema.Properties
   285  			if _, ok := schemaProperties["metadata"]; ok {
   286  				schemaProperties["metadata"] = apiext.JSONSchemaProps{Type: "object"}
   287  			}
   288  		}
   289  
   290  	}
   291  
   292  }
   293  
   294  // toTrivialVersions strips out all schemata except for the storage schema,
   295  // and moves that up into the root object.  This makes the CRD compatible
   296  // with pre 1.13 clusters.
   297  func toTrivialVersions(crd *apiextlegacy.CustomResourceDefinition) {
   298  	var canonicalSchema *apiextlegacy.CustomResourceValidation
   299  	var canonicalSubresources *apiextlegacy.CustomResourceSubresources
   300  	var canonicalColumns []apiextlegacy.CustomResourceColumnDefinition
   301  	for i, ver := range crd.Spec.Versions {
   302  		if ver.Storage == true {
   303  			canonicalSchema = ver.Schema
   304  			canonicalSubresources = ver.Subresources
   305  			canonicalColumns = ver.AdditionalPrinterColumns
   306  		}
   307  		crd.Spec.Versions[i].Schema = nil
   308  		crd.Spec.Versions[i].Subresources = nil
   309  		crd.Spec.Versions[i].AdditionalPrinterColumns = nil
   310  	}
   311  	if canonicalSchema == nil {
   312  		return
   313  	}
   314  
   315  	crd.Spec.Validation = canonicalSchema
   316  	crd.Spec.Subresources = canonicalSubresources
   317  	crd.Spec.AdditionalPrinterColumns = canonicalColumns
   318  }
   319  
   320  // addAttribution adds attribution info to indicate controller-gen tool was used
   321  // to generate this CRD definition along with the version info.
   322  func addAttribution(crd *apiext.CustomResourceDefinition) {
   323  	if crd.ObjectMeta.Annotations == nil {
   324  		crd.ObjectMeta.Annotations = map[string]string{}
   325  	}
   326  	crd.ObjectMeta.Annotations["controller-gen.kubebuilder.io/version"] = version.Version()
   327  }
   328  
   329  // FindMetav1 locates the actual package representing metav1 amongst
   330  // the imports of the roots.
   331  func FindMetav1(roots []*loader.Package) *loader.Package {
   332  	for _, root := range roots {
   333  		pkg := root.Imports()["k8s.io/apimachinery/pkg/apis/meta/v1"]
   334  		if pkg != nil {
   335  			return pkg
   336  		}
   337  	}
   338  	return nil
   339  }
   340  
   341  // FindKubeKinds locates all types that contain TypeMeta and ObjectMeta
   342  // (and thus may be a Kubernetes object), and returns the corresponding
   343  // group-kinds.
   344  func FindKubeKinds(parser *Parser, metav1Pkg *loader.Package) map[schema.GroupKind]struct{} {
   345  	// TODO(directxman12): technically, we should be finding metav1 per-package
   346  	kubeKinds := map[schema.GroupKind]struct{}{}
   347  	for typeIdent, info := range parser.Types {
   348  		hasObjectMeta := false
   349  		hasTypeMeta := false
   350  
   351  		pkg := typeIdent.Package
   352  		pkg.NeedTypesInfo()
   353  		typesInfo := pkg.TypesInfo
   354  
   355  		for _, field := range info.Fields {
   356  			if field.Name != "" {
   357  				// type and object meta are embedded,
   358  				// so they can't be this
   359  				continue
   360  			}
   361  
   362  			fieldType := typesInfo.TypeOf(field.RawField.Type)
   363  			namedField, isNamed := fieldType.(*types.Named)
   364  			if !isNamed {
   365  				// ObjectMeta and TypeMeta are named types
   366  				continue
   367  			}
   368  			if namedField.Obj().Pkg() == nil {
   369  				// Embedded non-builtin universe type (specifically, it's probably `error`),
   370  				// so it can't be ObjectMeta or TypeMeta
   371  				continue
   372  			}
   373  			fieldPkgPath := loader.NonVendorPath(namedField.Obj().Pkg().Path())
   374  			fieldPkg := pkg.Imports()[fieldPkgPath]
   375  			if fieldPkg != metav1Pkg {
   376  				continue
   377  			}
   378  
   379  			switch namedField.Obj().Name() {
   380  			case "ObjectMeta":
   381  				hasObjectMeta = true
   382  			case "TypeMeta":
   383  				hasTypeMeta = true
   384  			}
   385  		}
   386  
   387  		if !hasObjectMeta || !hasTypeMeta {
   388  			continue
   389  		}
   390  
   391  		groupKind := schema.GroupKind{
   392  			Group: parser.GroupVersions[pkg].Group,
   393  			Kind:  typeIdent.Name,
   394  		}
   395  		kubeKinds[groupKind] = struct{}{}
   396  	}
   397  
   398  	return kubeKinds
   399  }
   400  
   401  // filterTypesForCRDs filters out all nodes that aren't used in CRD generation,
   402  // like interfaces and struct fields without JSON tag.
   403  func filterTypesForCRDs(node ast.Node) bool {
   404  	switch node := node.(type) {
   405  	case *ast.InterfaceType:
   406  		// skip interfaces, we never care about references in them
   407  		return false
   408  	case *ast.StructType:
   409  		return true
   410  	case *ast.Field:
   411  		_, hasTag := loader.ParseAstTag(node.Tag).Lookup("json")
   412  		// fields without JSON tags mean we have custom serialization,
   413  		// so only visit fields with tags.
   414  		return hasTag
   415  	default:
   416  		return true
   417  	}
   418  }