github.com/cilium/controller-tools@v0.3.1-0.20230329170030-f2b7ff866fde/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/types"
    22  
    23  	apiext "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
    24  	apiextlegacy "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1"
    25  	"k8s.io/apimachinery/pkg/runtime/schema"
    26  
    27  	crdmarkers "sigs.k8s.io/controller-tools/pkg/crd/markers"
    28  	"sigs.k8s.io/controller-tools/pkg/genall"
    29  	"sigs.k8s.io/controller-tools/pkg/loader"
    30  	"sigs.k8s.io/controller-tools/pkg/markers"
    31  	"sigs.k8s.io/controller-tools/pkg/version"
    32  )
    33  
    34  // The default CustomResourceDefinition version to generate.
    35  const defaultVersion = "v1"
    36  
    37  // +controllertools:marker:generateHelp
    38  
    39  // Generator generates CustomResourceDefinition objects.
    40  type Generator struct {
    41  	// TrivialVersions indicates that we should produce a single-version CRD.
    42  	//
    43  	// Single "trivial-version" CRDs are compatible with older (pre 1.13)
    44  	// Kubernetes API servers.  The storage version's schema will be used as
    45  	// the CRD's schema.
    46  	//
    47  	// Only works with the v1beta1 CRD version.
    48  	TrivialVersions bool `marker:",optional"`
    49  
    50  	// PreserveUnknownFields indicates whether or not we should turn off pruning.
    51  	//
    52  	// Left unspecified, it'll default to true when only a v1beta1 CRD is
    53  	// generated (to preserve compatibility with older versions of this tool),
    54  	// or false otherwise.
    55  	//
    56  	// It's required to be false for v1 CRDs.
    57  	PreserveUnknownFields *bool `marker:",optional"`
    58  
    59  	// AllowDangerousTypes allows types which are usually omitted from CRD generation
    60  	// because they are not recommended.
    61  	//
    62  	// Currently the following additional types are allowed when this is true:
    63  	// float32
    64  	// float64
    65  	//
    66  	// Left unspecified, the default is false
    67  	AllowDangerousTypes *bool `marker:",optional"`
    68  
    69  	// MaxDescLen specifies the maximum description length for fields in CRD's OpenAPI schema.
    70  	//
    71  	// 0 indicates drop the description for all fields completely.
    72  	// n indicates limit the description to at most n characters and truncate the description to
    73  	// closest sentence boundary if it exceeds n characters.
    74  	MaxDescLen *int `marker:",optional"`
    75  
    76  	// CRDVersions specifies the target API versions of the CRD type itself to
    77  	// generate. Defaults to v1.
    78  	//
    79  	// The first version listed will be assumed to be the "default" version and
    80  	// will not get a version suffix in the output filename.
    81  	//
    82  	// You'll need to use "v1" to get support for features like defaulting,
    83  	// along with an API server that supports it (Kubernetes 1.16+).
    84  	CRDVersions []string `marker:"crdVersions,optional"`
    85  }
    86  
    87  func (Generator) RegisterMarkers(into *markers.Registry) error {
    88  	return crdmarkers.Register(into)
    89  }
    90  func (g Generator) Generate(ctx *genall.GenerationContext) error {
    91  	parser := &Parser{
    92  		Collector: ctx.Collector,
    93  		Checker:   ctx.Checker,
    94  		// Perform defaulting here to avoid ambiguity later
    95  		AllowDangerousTypes: g.AllowDangerousTypes != nil && *g.AllowDangerousTypes == true,
    96  	}
    97  
    98  	AddKnownTypes(parser)
    99  	for _, root := range ctx.Roots {
   100  		parser.NeedPackage(root)
   101  	}
   102  
   103  	metav1Pkg := FindMetav1(ctx.Roots)
   104  	if metav1Pkg == nil {
   105  		// no objects in the roots, since nothing imported metav1
   106  		return nil
   107  	}
   108  
   109  	// TODO: allow selecting a specific object
   110  	kubeKinds := FindKubeKinds(parser, metav1Pkg)
   111  	if len(kubeKinds) == 0 {
   112  		// no objects in the roots
   113  		return nil
   114  	}
   115  
   116  	crdVersions := g.CRDVersions
   117  
   118  	if len(crdVersions) == 0 {
   119  		crdVersions = []string{defaultVersion}
   120  	}
   121  
   122  	for groupKind := range kubeKinds {
   123  		parser.NeedCRDFor(groupKind, g.MaxDescLen)
   124  		crdRaw := parser.CustomResourceDefinitions[groupKind]
   125  		addAttribution(&crdRaw)
   126  
   127  		versionedCRDs := make([]interface{}, len(crdVersions))
   128  		for i, ver := range crdVersions {
   129  			conv, err := AsVersion(crdRaw, schema.GroupVersion{Group: apiext.SchemeGroupVersion.Group, Version: ver})
   130  			if err != nil {
   131  				return err
   132  			}
   133  			versionedCRDs[i] = conv
   134  		}
   135  
   136  		if g.TrivialVersions {
   137  			for i, crd := range versionedCRDs {
   138  				if crdVersions[i] == "v1beta1" {
   139  					toTrivialVersions(crd.(*apiextlegacy.CustomResourceDefinition))
   140  				}
   141  			}
   142  		}
   143  
   144  		// *If* we're only generating v1beta1 CRDs, default to `preserveUnknownFields: (unset)`
   145  		// for compatibility purposes.  In any other case, default to false, since that's
   146  		// the sensible default and is required for v1.
   147  		v1beta1Only := len(crdVersions) == 1 && crdVersions[0] == "v1beta1"
   148  		switch {
   149  		case (g.PreserveUnknownFields == nil || *g.PreserveUnknownFields) && v1beta1Only:
   150  			crd := versionedCRDs[0].(*apiextlegacy.CustomResourceDefinition)
   151  			crd.Spec.PreserveUnknownFields = nil
   152  		case g.PreserveUnknownFields == nil, g.PreserveUnknownFields != nil && !*g.PreserveUnknownFields:
   153  			// it'll be false here (coming from v1) -- leave it as such
   154  		default:
   155  			return fmt.Errorf("you may only set PreserveUnknownFields to true with v1beta1 CRDs")
   156  		}
   157  
   158  		for i, crd := range versionedCRDs {
   159  			// defaults are not allowed to be specified in v1beta1 CRDs, so strip them
   160  			// before writing to a file
   161  			if crdVersions[i] == "v1beta1" {
   162  				removeDefaultsFromSchemas(crd.(*apiextlegacy.CustomResourceDefinition))
   163  			}
   164  			var fileName string
   165  			if i == 0 {
   166  				fileName = fmt.Sprintf("%s_%s.yaml", crdRaw.Spec.Group, crdRaw.Spec.Names.Plural)
   167  			} else {
   168  				fileName = fmt.Sprintf("%s_%s.%s.yaml", crdRaw.Spec.Group, crdRaw.Spec.Names.Plural, crdVersions[i])
   169  			}
   170  			if err := ctx.WriteYAML(fileName, crd); err != nil {
   171  				return err
   172  			}
   173  		}
   174  	}
   175  
   176  	return nil
   177  }
   178  
   179  // removeDefaultsFromSchemas will remove all instances of default values being
   180  // specified across all defined API versions
   181  func removeDefaultsFromSchemas(crd *apiextlegacy.CustomResourceDefinition) {
   182  	if crd.Spec.Validation != nil {
   183  		removeDefaultsFromSchemaProps(crd.Spec.Validation.OpenAPIV3Schema)
   184  	}
   185  
   186  	for _, versionSpec := range crd.Spec.Versions {
   187  		if versionSpec.Schema != nil {
   188  			removeDefaultsFromSchemaProps(versionSpec.Schema.OpenAPIV3Schema)
   189  		}
   190  	}
   191  }
   192  
   193  // removeDefaultsFromSchemaProps will recurse into JSONSchemaProps to remove
   194  // all instances of default values being specified
   195  func removeDefaultsFromSchemaProps(v *apiextlegacy.JSONSchemaProps) {
   196  	if v == nil {
   197  		return
   198  	}
   199  
   200  	// nil-out the default field
   201  	v.Default = nil
   202  	for name, prop := range v.Properties {
   203  		removeDefaultsFromSchemaProps(&prop)
   204  		v.Properties[name] = prop
   205  	}
   206  	if v.Items != nil {
   207  		removeDefaultsFromSchemaProps(v.Items.Schema)
   208  		for i := range v.Items.JSONSchemas {
   209  			props := v.Items.JSONSchemas[i]
   210  			removeDefaultsFromSchemaProps(&props)
   211  			v.Items.JSONSchemas[i] = props
   212  		}
   213  	}
   214  }
   215  
   216  // toTrivialVersions strips out all schemata except for the storage schema,
   217  // and moves that up into the root object.  This makes the CRD compatible
   218  // with pre 1.13 clusters.
   219  func toTrivialVersions(crd *apiextlegacy.CustomResourceDefinition) {
   220  	var canonicalSchema *apiextlegacy.CustomResourceValidation
   221  	var canonicalSubresources *apiextlegacy.CustomResourceSubresources
   222  	var canonicalColumns []apiextlegacy.CustomResourceColumnDefinition
   223  	for i, ver := range crd.Spec.Versions {
   224  		if ver.Storage == true {
   225  			canonicalSchema = ver.Schema
   226  			canonicalSubresources = ver.Subresources
   227  			canonicalColumns = ver.AdditionalPrinterColumns
   228  		}
   229  		crd.Spec.Versions[i].Schema = nil
   230  		crd.Spec.Versions[i].Subresources = nil
   231  		crd.Spec.Versions[i].AdditionalPrinterColumns = nil
   232  	}
   233  	if canonicalSchema == nil {
   234  		return
   235  	}
   236  
   237  	crd.Spec.Validation = canonicalSchema
   238  	crd.Spec.Subresources = canonicalSubresources
   239  	crd.Spec.AdditionalPrinterColumns = canonicalColumns
   240  }
   241  
   242  // addAttribution adds attribution info to indicate controller-gen tool was used
   243  // to generate this CRD definition along with the version info.
   244  func addAttribution(crd *apiext.CustomResourceDefinition) {
   245  	if crd.ObjectMeta.Annotations == nil {
   246  		crd.ObjectMeta.Annotations = map[string]string{}
   247  	}
   248  	crd.ObjectMeta.Annotations["controller-gen.kubebuilder.io/version"] = version.Version()
   249  }
   250  
   251  // FindMetav1 locates the actual package representing metav1 amongst
   252  // the imports of the roots.
   253  func FindMetav1(roots []*loader.Package) *loader.Package {
   254  	for _, root := range roots {
   255  		pkg := root.Imports()["k8s.io/apimachinery/pkg/apis/meta/v1"]
   256  		if pkg != nil {
   257  			return pkg
   258  		}
   259  	}
   260  	return nil
   261  }
   262  
   263  // FindKubeKinds locates all types that contain TypeMeta and ObjectMeta
   264  // (and thus may be a Kubernetes object), and returns the corresponding
   265  // group-kinds.
   266  func FindKubeKinds(parser *Parser, metav1Pkg *loader.Package) map[schema.GroupKind]struct{} {
   267  	// TODO(directxman12): technically, we should be finding metav1 per-package
   268  	kubeKinds := map[schema.GroupKind]struct{}{}
   269  	for typeIdent, info := range parser.Types {
   270  		hasObjectMeta := false
   271  		hasTypeMeta := false
   272  
   273  		pkg := typeIdent.Package
   274  		pkg.NeedTypesInfo()
   275  		typesInfo := pkg.TypesInfo
   276  
   277  		for _, field := range info.Fields {
   278  			if field.Name != "" {
   279  				// type and object meta are embedded,
   280  				// so they can't be this
   281  				continue
   282  			}
   283  
   284  			fieldType := typesInfo.TypeOf(field.RawField.Type)
   285  			namedField, isNamed := fieldType.(*types.Named)
   286  			if !isNamed {
   287  				// ObjectMeta and TypeMeta are named types
   288  				continue
   289  			}
   290  			if namedField.Obj().Pkg() == nil {
   291  				// Embedded non-builtin universe type (specifically, it's probably `error`),
   292  				// so it can't be ObjectMeta or TypeMeta
   293  				continue
   294  			}
   295  			fieldPkgPath := loader.NonVendorPath(namedField.Obj().Pkg().Path())
   296  			fieldPkg := pkg.Imports()[fieldPkgPath]
   297  			if fieldPkg != metav1Pkg {
   298  				continue
   299  			}
   300  
   301  			switch namedField.Obj().Name() {
   302  			case "ObjectMeta":
   303  				hasObjectMeta = true
   304  			case "TypeMeta":
   305  				hasTypeMeta = true
   306  			}
   307  		}
   308  
   309  		if !hasObjectMeta || !hasTypeMeta {
   310  			continue
   311  		}
   312  
   313  		groupKind := schema.GroupKind{
   314  			Group: parser.GroupVersions[pkg].Group,
   315  			Kind:  typeIdent.Name,
   316  		}
   317  		kubeKinds[groupKind] = struct{}{}
   318  	}
   319  
   320  	return kubeKinds
   321  }