github.com/alex123012/deckhouse-controller-tools@v0.0.0-20230510090815-d594daf1af8c/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  
    90  func (Generator) CheckFilter() loader.NodeFilter {
    91  	return filterTypesForCRDs
    92  }
    93  func (Generator) RegisterMarkers(into *markers.Registry) error {
    94  	return crdmarkers.Register(into)
    95  }
    96  
    97  // transformRemoveCRDStatus ensures we do not write the CRD status field.
    98  func transformRemoveCRDStatus(obj map[string]interface{}) error {
    99  	delete(obj, "status")
   100  	return nil
   101  }
   102  
   103  func (g Generator) Generate(ctx *genall.GenerationContext) error {
   104  	parser := &Parser{
   105  		Collector: ctx.Collector,
   106  		Checker:   ctx.Checker,
   107  		// Perform defaulting here to avoid ambiguity later
   108  		IgnoreUnexportedFields: g.IgnoreUnexportedFields != nil && *g.IgnoreUnexportedFields == true,
   109  		AllowDangerousTypes:    g.AllowDangerousTypes != nil && *g.AllowDangerousTypes == true,
   110  		// Indicates the parser on whether to register the ObjectMeta type or not
   111  		GenerateEmbeddedObjectMeta: g.GenerateEmbeddedObjectMeta != nil && *g.GenerateEmbeddedObjectMeta == true,
   112  	}
   113  
   114  	AddKnownTypes(parser)
   115  	for _, root := range ctx.Roots {
   116  		parser.NeedPackage(root)
   117  	}
   118  
   119  	metav1Pkg := FindMetav1(ctx.Roots)
   120  	if metav1Pkg == nil {
   121  		// no objects in the roots, since nothing imported metav1
   122  		return nil
   123  	}
   124  
   125  	// TODO: allow selecting a specific object
   126  	kubeKinds := FindKubeKinds(parser, metav1Pkg)
   127  	if len(kubeKinds) == 0 {
   128  		// no objects in the roots
   129  		return nil
   130  	}
   131  
   132  	crdVersions := g.CRDVersions
   133  
   134  	if len(crdVersions) == 0 {
   135  		crdVersions = []string{defaultVersion}
   136  	}
   137  
   138  	var headerText string
   139  
   140  	if g.HeaderFile != "" {
   141  		headerBytes, err := ctx.ReadFile(g.HeaderFile)
   142  		if err != nil {
   143  			return err
   144  		}
   145  		headerText = string(headerBytes)
   146  	}
   147  	headerText = strings.ReplaceAll(headerText, " YEAR", " "+g.Year)
   148  
   149  	for _, groupKind := range kubeKinds {
   150  		parser.NeedCRDFor(groupKind, g.MaxDescLen)
   151  		crdRaw := parser.CustomResourceDefinitions[groupKind]
   152  		addAttribution(&crdRaw)
   153  
   154  		// Prevent the top level metadata for the CRD to be generate regardless of the intention in the arguments
   155  		FixTopLevelMetadata(crdRaw)
   156  
   157  		versionedCRDs := make([]interface{}, len(crdVersions))
   158  		for i, ver := range crdVersions {
   159  			conv, err := AsVersion(crdRaw, schema.GroupVersion{Group: apiext.SchemeGroupVersion.Group, Version: ver})
   160  			if err != nil {
   161  				return err
   162  			}
   163  			versionedCRDs[i] = conv
   164  		}
   165  
   166  		for i, crd := range versionedCRDs {
   167  			removeDescriptionFromMetadata(crd.(*apiext.CustomResourceDefinition))
   168  			var fileName string
   169  			if i == 0 {
   170  				fileName = fmt.Sprintf("%s_%s.yaml", crdRaw.Spec.Group, crdRaw.Spec.Names.Plural)
   171  			} else {
   172  				fileName = fmt.Sprintf("%s_%s.%s.yaml", crdRaw.Spec.Group, crdRaw.Spec.Names.Plural, crdVersions[i])
   173  			}
   174  			if err := ctx.WriteYAML(fileName, headerText, []interface{}{crd}, genall.WithTransform(transformRemoveCRDStatus), genall.WithTransform(genall.TransformRemoveCreationTimestamp)); err != nil {
   175  				return err
   176  			}
   177  		}
   178  	}
   179  
   180  	return nil
   181  }
   182  
   183  func removeDescriptionFromMetadata(crd *apiext.CustomResourceDefinition) {
   184  	for _, versionSpec := range crd.Spec.Versions {
   185  		if versionSpec.Schema != nil {
   186  			removeDescriptionFromMetadataProps(versionSpec.Schema.OpenAPIV3Schema)
   187  		}
   188  	}
   189  }
   190  
   191  func removeDescriptionFromMetadataProps(v *apiext.JSONSchemaProps) {
   192  	if m, ok := v.Properties["metadata"]; ok {
   193  		meta := &m
   194  		if meta.Description != "" {
   195  			meta.Description = ""
   196  			v.Properties["metadata"] = m
   197  
   198  		}
   199  	}
   200  }
   201  
   202  // FixTopLevelMetadata resets the schema for the top-level metadata field which is needed for CRD validation
   203  func FixTopLevelMetadata(crd apiext.CustomResourceDefinition) {
   204  	for _, v := range crd.Spec.Versions {
   205  		if v.Schema != nil && v.Schema.OpenAPIV3Schema != nil && v.Schema.OpenAPIV3Schema.Properties != nil {
   206  			schemaProperties := v.Schema.OpenAPIV3Schema.Properties
   207  			if _, ok := schemaProperties["metadata"]; ok {
   208  				schemaProperties["metadata"] = apiext.JSONSchemaProps{Type: "object"}
   209  			}
   210  		}
   211  	}
   212  }
   213  
   214  // addAttribution adds attribution info to indicate controller-gen tool was used
   215  // to generate this CRD definition along with the version info.
   216  func addAttribution(crd *apiext.CustomResourceDefinition) {
   217  	if crd.ObjectMeta.Annotations == nil {
   218  		crd.ObjectMeta.Annotations = map[string]string{}
   219  	}
   220  	crd.ObjectMeta.Annotations["controller-gen.kubebuilder.io/version"] = version.Version()
   221  }
   222  
   223  // FindMetav1 locates the actual package representing metav1 amongst
   224  // the imports of the roots.
   225  func FindMetav1(roots []*loader.Package) *loader.Package {
   226  	for _, root := range roots {
   227  		pkg := root.Imports()["k8s.io/apimachinery/pkg/apis/meta/v1"]
   228  		if pkg != nil {
   229  			return pkg
   230  		}
   231  	}
   232  	return nil
   233  }
   234  
   235  // FindKubeKinds locates all types that contain TypeMeta and ObjectMeta
   236  // (and thus may be a Kubernetes object), and returns the corresponding
   237  // group-kinds.
   238  func FindKubeKinds(parser *Parser, metav1Pkg *loader.Package) []schema.GroupKind {
   239  	// TODO(directxman12): technically, we should be finding metav1 per-package
   240  	kubeKinds := map[schema.GroupKind]struct{}{}
   241  	for typeIdent, info := range parser.Types {
   242  		hasObjectMeta := false
   243  		hasTypeMeta := false
   244  
   245  		pkg := typeIdent.Package
   246  		pkg.NeedTypesInfo()
   247  		typesInfo := pkg.TypesInfo
   248  
   249  		for _, field := range info.Fields {
   250  			if field.Name != "" {
   251  				// type and object meta are embedded,
   252  				// so they can't be this
   253  				continue
   254  			}
   255  
   256  			fieldType := typesInfo.TypeOf(field.RawField.Type)
   257  			namedField, isNamed := fieldType.(*types.Named)
   258  			if !isNamed {
   259  				// ObjectMeta and TypeMeta are named types
   260  				continue
   261  			}
   262  			if namedField.Obj().Pkg() == nil {
   263  				// Embedded non-builtin universe type (specifically, it's probably `error`),
   264  				// so it can't be ObjectMeta or TypeMeta
   265  				continue
   266  			}
   267  			fieldPkgPath := loader.NonVendorPath(namedField.Obj().Pkg().Path())
   268  			fieldPkg := pkg.Imports()[fieldPkgPath]
   269  
   270  			// Compare the metav1 package by ID and not by the actual instance
   271  			// of the object. The objects in memory could be different due to
   272  			// loading from different root paths, even when they both refer to
   273  			// the same metav1 package.
   274  			if fieldPkg == nil || fieldPkg.ID != metav1Pkg.ID {
   275  				continue
   276  			}
   277  
   278  			switch namedField.Obj().Name() {
   279  			case "ObjectMeta":
   280  				hasObjectMeta = true
   281  			case "TypeMeta":
   282  				hasTypeMeta = true
   283  			}
   284  		}
   285  
   286  		if !hasObjectMeta || !hasTypeMeta {
   287  			continue
   288  		}
   289  
   290  		groupKind := schema.GroupKind{
   291  			Group: parser.GroupVersions[pkg].Group,
   292  			Kind:  typeIdent.Name,
   293  		}
   294  		kubeKinds[groupKind] = struct{}{}
   295  	}
   296  
   297  	groupKindList := make([]schema.GroupKind, 0, len(kubeKinds))
   298  	for groupKind := range kubeKinds {
   299  		groupKindList = append(groupKindList, groupKind)
   300  	}
   301  	sort.Slice(groupKindList, func(i, j int) bool {
   302  		return groupKindList[i].String() < groupKindList[j].String()
   303  	})
   304  
   305  	return groupKindList
   306  }
   307  
   308  // filterTypesForCRDs filters out all nodes that aren't used in CRD generation,
   309  // like interfaces and struct fields without JSON tag.
   310  func filterTypesForCRDs(node ast.Node) bool {
   311  	switch node := node.(type) {
   312  	case *ast.InterfaceType:
   313  		// skip interfaces, we never care about references in them
   314  		return false
   315  	case *ast.StructType:
   316  		return true
   317  	case *ast.Field:
   318  		_, hasTag := loader.ParseAstTag(node.Tag).Lookup("json")
   319  		// fields without JSON tags mean we have custom serialization,
   320  		// so only visit fields with tags.
   321  		return hasTag
   322  	default:
   323  		return true
   324  	}
   325  }