sigs.k8s.io/controller-tools@v0.15.1-0.20240515195456-85686cb69316/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  	"sort"
    24  	"strings"
    25  
    26  	apiext "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
    27  	"k8s.io/apimachinery/pkg/runtime/schema"
    28  
    29  	crdmarkers "sigs.k8s.io/controller-tools/pkg/crd/markers"
    30  	"sigs.k8s.io/controller-tools/pkg/genall"
    31  	"sigs.k8s.io/controller-tools/pkg/loader"
    32  	"sigs.k8s.io/controller-tools/pkg/markers"
    33  	"sigs.k8s.io/controller-tools/pkg/version"
    34  )
    35  
    36  // The identifier for v1 CustomResourceDefinitions.
    37  const v1 = "v1"
    38  
    39  // The default CustomResourceDefinition version to generate.
    40  const defaultVersion = v1
    41  
    42  // +controllertools:marker:generateHelp
    43  
    44  // Generator generates CustomResourceDefinition objects.
    45  type Generator struct {
    46  	// IgnoreUnexportedFields indicates that we should skip unexported fields.
    47  	//
    48  	// Left unspecified, the default is false.
    49  	IgnoreUnexportedFields *bool `marker:",optional"`
    50  
    51  	// AllowDangerousTypes allows types which are usually omitted from CRD generation
    52  	// because they are not recommended.
    53  	//
    54  	// Currently the following additional types are allowed when this is true:
    55  	// float32
    56  	// float64
    57  	//
    58  	// Left unspecified, the default is false
    59  	AllowDangerousTypes *bool `marker:",optional"`
    60  
    61  	// MaxDescLen specifies the maximum description length for fields in CRD's OpenAPI schema.
    62  	//
    63  	// 0 indicates drop the description for all fields completely.
    64  	// n indicates limit the description to at most n characters and truncate the description to
    65  	// closest sentence boundary if it exceeds n characters.
    66  	MaxDescLen *int `marker:",optional"`
    67  
    68  	// CRDVersions specifies the target API versions of the CRD type itself to
    69  	// generate. Defaults to v1.
    70  	//
    71  	// Currently, the only supported value is v1.
    72  	//
    73  	// The first version listed will be assumed to be the "default" version and
    74  	// will not get a version suffix in the output filename.
    75  	//
    76  	// You'll need to use "v1" to get support for features like defaulting,
    77  	// along with an API server that supports it (Kubernetes 1.16+).
    78  	CRDVersions []string `marker:"crdVersions,optional"`
    79  
    80  	// GenerateEmbeddedObjectMeta specifies if any embedded ObjectMeta in the CRD should be generated
    81  	GenerateEmbeddedObjectMeta *bool `marker:",optional"`
    82  
    83  	// HeaderFile specifies the header text (e.g. license) to prepend to generated files.
    84  	HeaderFile string `marker:",optional"`
    85  
    86  	// Year specifies the year to substitute for " YEAR" in the header file.
    87  	Year string `marker:",optional"`
    88  
    89  	// DeprecatedV1beta1CompatibilityPreserveUnknownFields indicates whether
    90  	// or not we should turn off field pruning for this resource.
    91  	//
    92  	// Specifies spec.preserveUnknownFields value that is false and omitted by default.
    93  	// This value can only be specified for CustomResourceDefinitions that were created with
    94  	// `apiextensions.k8s.io/v1beta1`.
    95  	//
    96  	// The field can be set for compatiblity reasons, although strongly discouraged, resource
    97  	// authors should move to a structural OpenAPI schema instead.
    98  	//
    99  	// See https://kubernetes.io/docs/tasks/extend-kubernetes/custom-resources/custom-resource-definitions/#field-pruning
   100  	// for more information about field pruning and v1beta1 resources compatibility.
   101  	DeprecatedV1beta1CompatibilityPreserveUnknownFields *bool `marker:",optional"`
   102  }
   103  
   104  func (Generator) CheckFilter() loader.NodeFilter {
   105  	return filterTypesForCRDs
   106  }
   107  func (Generator) RegisterMarkers(into *markers.Registry) error {
   108  	return crdmarkers.Register(into)
   109  }
   110  
   111  // transformRemoveCRDStatus ensures we do not write the CRD status field.
   112  func transformRemoveCRDStatus(obj map[string]interface{}) error {
   113  	delete(obj, "status")
   114  	return nil
   115  }
   116  
   117  // transformPreserveUnknownFields adds spec.preserveUnknownFields=value.
   118  func transformPreserveUnknownFields(value bool) func(map[string]interface{}) error {
   119  	return func(obj map[string]interface{}) error {
   120  		if spec, ok := obj["spec"].(map[interface{}]interface{}); ok {
   121  			spec["preserveUnknownFields"] = value
   122  		}
   123  		return nil
   124  	}
   125  }
   126  
   127  func (g Generator) Generate(ctx *genall.GenerationContext) error {
   128  	parser := &Parser{
   129  		Collector: ctx.Collector,
   130  		Checker:   ctx.Checker,
   131  		// Perform defaulting here to avoid ambiguity later
   132  		IgnoreUnexportedFields: g.IgnoreUnexportedFields != nil && *g.IgnoreUnexportedFields,
   133  		AllowDangerousTypes:    g.AllowDangerousTypes != nil && *g.AllowDangerousTypes,
   134  		// Indicates the parser on whether to register the ObjectMeta type or not
   135  		GenerateEmbeddedObjectMeta: g.GenerateEmbeddedObjectMeta != nil && *g.GenerateEmbeddedObjectMeta,
   136  	}
   137  
   138  	AddKnownTypes(parser)
   139  	for _, root := range ctx.Roots {
   140  		parser.NeedPackage(root)
   141  	}
   142  
   143  	metav1Pkg := FindMetav1(ctx.Roots)
   144  	if metav1Pkg == nil {
   145  		// no objects in the roots, since nothing imported metav1
   146  		return nil
   147  	}
   148  
   149  	// TODO: allow selecting a specific object
   150  	kubeKinds := FindKubeKinds(parser, metav1Pkg)
   151  	if len(kubeKinds) == 0 {
   152  		// no objects in the roots
   153  		return nil
   154  	}
   155  
   156  	crdVersions := g.CRDVersions
   157  
   158  	if len(crdVersions) == 0 {
   159  		crdVersions = []string{defaultVersion}
   160  	}
   161  
   162  	var headerText string
   163  
   164  	if g.HeaderFile != "" {
   165  		headerBytes, err := ctx.ReadFile(g.HeaderFile)
   166  		if err != nil {
   167  			return err
   168  		}
   169  		headerText = string(headerBytes)
   170  	}
   171  	headerText = strings.ReplaceAll(headerText, " YEAR", " "+g.Year)
   172  
   173  	yamlOpts := []*genall.WriteYAMLOptions{
   174  		genall.WithTransform(transformRemoveCRDStatus),
   175  		genall.WithTransform(genall.TransformRemoveCreationTimestamp),
   176  	}
   177  	if g.DeprecatedV1beta1CompatibilityPreserveUnknownFields != nil {
   178  		yamlOpts = append(yamlOpts, genall.WithTransform(transformPreserveUnknownFields(*g.DeprecatedV1beta1CompatibilityPreserveUnknownFields)))
   179  	}
   180  
   181  	for _, groupKind := range kubeKinds {
   182  		parser.NeedCRDFor(groupKind, g.MaxDescLen)
   183  		crdRaw := parser.CustomResourceDefinitions[groupKind]
   184  		addAttribution(&crdRaw)
   185  
   186  		// Prevent the top level metadata for the CRD to be generate regardless of the intention in the arguments
   187  		FixTopLevelMetadata(crdRaw)
   188  
   189  		versionedCRDs := make([]interface{}, len(crdVersions))
   190  		for i, ver := range crdVersions {
   191  			conv, err := AsVersion(crdRaw, schema.GroupVersion{Group: apiext.SchemeGroupVersion.Group, Version: ver})
   192  			if err != nil {
   193  				return err
   194  			}
   195  			versionedCRDs[i] = conv
   196  		}
   197  
   198  		for i, crd := range versionedCRDs {
   199  			removeDescriptionFromMetadata(crd.(*apiext.CustomResourceDefinition))
   200  			var fileName string
   201  			if i == 0 {
   202  				fileName = fmt.Sprintf("%s_%s.yaml", crdRaw.Spec.Group, crdRaw.Spec.Names.Plural)
   203  			} else {
   204  				fileName = fmt.Sprintf("%s_%s.%s.yaml", crdRaw.Spec.Group, crdRaw.Spec.Names.Plural, crdVersions[i])
   205  			}
   206  			if err := ctx.WriteYAML(fileName, headerText, []interface{}{crd}, yamlOpts...); err != nil {
   207  				return err
   208  			}
   209  		}
   210  	}
   211  
   212  	return nil
   213  }
   214  
   215  func removeDescriptionFromMetadata(crd *apiext.CustomResourceDefinition) {
   216  	for _, versionSpec := range crd.Spec.Versions {
   217  		if versionSpec.Schema != nil {
   218  			removeDescriptionFromMetadataProps(versionSpec.Schema.OpenAPIV3Schema)
   219  		}
   220  	}
   221  }
   222  
   223  func removeDescriptionFromMetadataProps(v *apiext.JSONSchemaProps) {
   224  	if m, ok := v.Properties["metadata"]; ok {
   225  		meta := &m
   226  		if meta.Description != "" {
   227  			meta.Description = ""
   228  			v.Properties["metadata"] = m
   229  		}
   230  	}
   231  }
   232  
   233  // FixTopLevelMetadata resets the schema for the top-level metadata field which is needed for CRD validation
   234  func FixTopLevelMetadata(crd apiext.CustomResourceDefinition) {
   235  	for _, v := range crd.Spec.Versions {
   236  		if v.Schema != nil && v.Schema.OpenAPIV3Schema != nil && v.Schema.OpenAPIV3Schema.Properties != nil {
   237  			schemaProperties := v.Schema.OpenAPIV3Schema.Properties
   238  			if _, ok := schemaProperties["metadata"]; ok {
   239  				schemaProperties["metadata"] = apiext.JSONSchemaProps{Type: "object"}
   240  			}
   241  		}
   242  	}
   243  }
   244  
   245  // addAttribution adds attribution info to indicate controller-gen tool was used
   246  // to generate this CRD definition along with the version info.
   247  func addAttribution(crd *apiext.CustomResourceDefinition) {
   248  	if crd.ObjectMeta.Annotations == nil {
   249  		crd.ObjectMeta.Annotations = map[string]string{}
   250  	}
   251  	crd.ObjectMeta.Annotations["controller-gen.kubebuilder.io/version"] = version.Version()
   252  }
   253  
   254  // FindMetav1 locates the actual package representing metav1 amongst
   255  // the imports of the roots.
   256  func FindMetav1(roots []*loader.Package) *loader.Package {
   257  	for _, root := range roots {
   258  		pkg := root.Imports()["k8s.io/apimachinery/pkg/apis/meta/v1"]
   259  		if pkg != nil {
   260  			return pkg
   261  		}
   262  	}
   263  	return nil
   264  }
   265  
   266  // FindKubeKinds locates all types that contain TypeMeta and ObjectMeta
   267  // (and thus may be a Kubernetes object), and returns the corresponding
   268  // group-kinds.
   269  func FindKubeKinds(parser *Parser, metav1Pkg *loader.Package) []schema.GroupKind {
   270  	// TODO(directxman12): technically, we should be finding metav1 per-package
   271  	kubeKinds := map[schema.GroupKind]struct{}{}
   272  	for typeIdent, info := range parser.Types {
   273  		hasObjectMeta := false
   274  		hasTypeMeta := false
   275  
   276  		pkg := typeIdent.Package
   277  		pkg.NeedTypesInfo()
   278  		typesInfo := pkg.TypesInfo
   279  
   280  		for _, field := range info.Fields {
   281  			if field.Name != "" {
   282  				// type and object meta are embedded,
   283  				// so they can't be this
   284  				continue
   285  			}
   286  
   287  			fieldType := typesInfo.TypeOf(field.RawField.Type)
   288  			namedField, isNamed := fieldType.(*types.Named)
   289  			if !isNamed {
   290  				// ObjectMeta and TypeMeta are named types
   291  				continue
   292  			}
   293  			if namedField.Obj().Pkg() == nil {
   294  				// Embedded non-builtin universe type (specifically, it's probably `error`),
   295  				// so it can't be ObjectMeta or TypeMeta
   296  				continue
   297  			}
   298  			fieldPkgPath := loader.NonVendorPath(namedField.Obj().Pkg().Path())
   299  			fieldPkg := pkg.Imports()[fieldPkgPath]
   300  
   301  			// Compare the metav1 package by ID and not by the actual instance
   302  			// of the object. The objects in memory could be different due to
   303  			// loading from different root paths, even when they both refer to
   304  			// the same metav1 package.
   305  			if fieldPkg == nil || fieldPkg.ID != metav1Pkg.ID {
   306  				continue
   307  			}
   308  
   309  			switch namedField.Obj().Name() {
   310  			case "ObjectMeta":
   311  				hasObjectMeta = true
   312  			case "TypeMeta":
   313  				hasTypeMeta = true
   314  			}
   315  		}
   316  
   317  		if !hasObjectMeta || !hasTypeMeta {
   318  			continue
   319  		}
   320  
   321  		groupKind := schema.GroupKind{
   322  			Group: parser.GroupVersions[pkg].Group,
   323  			Kind:  typeIdent.Name,
   324  		}
   325  		kubeKinds[groupKind] = struct{}{}
   326  	}
   327  
   328  	groupKindList := make([]schema.GroupKind, 0, len(kubeKinds))
   329  	for groupKind := range kubeKinds {
   330  		groupKindList = append(groupKindList, groupKind)
   331  	}
   332  	sort.Slice(groupKindList, func(i, j int) bool {
   333  		return groupKindList[i].String() < groupKindList[j].String()
   334  	})
   335  
   336  	return groupKindList
   337  }
   338  
   339  // filterTypesForCRDs filters out all nodes that aren't used in CRD generation,
   340  // like interfaces and struct fields without JSON tag.
   341  func filterTypesForCRDs(node ast.Node) bool {
   342  	switch node := node.(type) {
   343  	case *ast.InterfaceType:
   344  		// skip interfaces, we never care about references in them
   345  		return false
   346  	case *ast.StructType:
   347  		return true
   348  	case *ast.Field:
   349  		_, hasTag := loader.ParseAstTag(node.Tag).Lookup("json")
   350  		// fields without JSON tags mean we have custom serialization,
   351  		// so only visit fields with tags.
   352  		return hasTag
   353  	default:
   354  		return true
   355  	}
   356  }