github.com/cilium/controller-tools@v0.3.1-0.20230329170030-f2b7ff866fde/pkg/crd/parser.go (about)

     1  /*
     2  Copyright 2019 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  
    23  	apiext "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
    24  	"k8s.io/apimachinery/pkg/runtime/schema"
    25  
    26  	"sigs.k8s.io/controller-tools/pkg/loader"
    27  	"sigs.k8s.io/controller-tools/pkg/markers"
    28  )
    29  
    30  // TypeIdent represents some type in a Package.
    31  type TypeIdent struct {
    32  	Package *loader.Package
    33  	Name    string
    34  }
    35  
    36  func (t TypeIdent) String() string {
    37  	return fmt.Sprintf("%q.%s", t.Package.ID, t.Name)
    38  }
    39  
    40  // PackageOverride overrides the loading of some package
    41  // (potentially setting custom schemata, etc).  It must
    42  // call AddPackage if it wants to continue with the default
    43  // loading behavior.
    44  type PackageOverride func(p *Parser, pkg *loader.Package)
    45  
    46  // Parser knows how to parse out CRD information and generate
    47  // OpenAPI schemata from some collection of types and markers.
    48  // Most methods on Parser cache their results automatically,
    49  // and thus may be called any number of times.
    50  type Parser struct {
    51  	Collector *markers.Collector
    52  
    53  	// Types contains the known TypeInfo for this parser.
    54  	Types map[TypeIdent]*markers.TypeInfo
    55  	// Schemata contains the known OpenAPI JSONSchemata for this parser.
    56  	Schemata map[TypeIdent]apiext.JSONSchemaProps
    57  	// GroupVersions contains the known group-versions of each package in this parser.
    58  	GroupVersions map[*loader.Package]schema.GroupVersion
    59  	// CustomResourceDefinitions contains the known CustomResourceDefinitions for types in this parser.
    60  	CustomResourceDefinitions map[schema.GroupKind]apiext.CustomResourceDefinition
    61  	// FlattenedSchemata contains fully flattened schemata for use in building
    62  	// CustomResourceDefinition validation.  Each schema has been flattened by the flattener,
    63  	// and then embedded fields have been flattened with FlattenEmbedded.
    64  	FlattenedSchemata map[TypeIdent]apiext.JSONSchemaProps
    65  
    66  	// PackageOverrides indicates that the loading of any package with
    67  	// the given path should be handled by the given overrider.
    68  	PackageOverrides map[string]PackageOverride
    69  
    70  	// checker stores persistent partial type-checking/reference-traversal information.
    71  	Checker *loader.TypeChecker
    72  	// packages marks packages as loaded, to avoid re-loading them.
    73  	packages map[*loader.Package]struct{}
    74  
    75  	flattener *Flattener
    76  
    77  	// AllowDangerousTypes controls the handling of non-recommended types such as float. If
    78  	// false (the default), these types are not supported.
    79  	// There is a continuum here:
    80  	//    1. Types that are always supported.
    81  	//    2. Types that are allowed by default, but not recommended (warning emitted when they are encountered as per PR #443).
    82  	//       Possibly they are allowed by default for historical reasons and may even be "on their way out" at some point in the future.
    83  	//    3. Types that are not allowed by default, not recommended, but there are some legitimate reasons to need them in certain corner cases.
    84  	//       Possibly these types should also emit a warning as per PR #443 even when they are "switched on" (an integration point between
    85  	//       this feature and #443 if desired). This is the category that this flag deals with.
    86  	//    4. Types that are not allowed and will not be allowed, possibly because it just "doesn't make sense" or possibly
    87  	//       because the implementation is too difficult/clunky to promote them to category 3.
    88  	// TODO: Should we have a more formal mechanism for putting "type patterns" in each of the above categories?
    89  	AllowDangerousTypes bool
    90  }
    91  
    92  func (p *Parser) init() {
    93  	if p.packages == nil {
    94  		p.packages = make(map[*loader.Package]struct{})
    95  	}
    96  	if p.flattener == nil {
    97  		p.flattener = &Flattener{
    98  			Parser: p,
    99  		}
   100  	}
   101  	if p.Schemata == nil {
   102  		p.Schemata = make(map[TypeIdent]apiext.JSONSchemaProps)
   103  	}
   104  	if p.Types == nil {
   105  		p.Types = make(map[TypeIdent]*markers.TypeInfo)
   106  	}
   107  	if p.PackageOverrides == nil {
   108  		p.PackageOverrides = make(map[string]PackageOverride)
   109  	}
   110  	if p.GroupVersions == nil {
   111  		p.GroupVersions = make(map[*loader.Package]schema.GroupVersion)
   112  	}
   113  	if p.CustomResourceDefinitions == nil {
   114  		p.CustomResourceDefinitions = make(map[schema.GroupKind]apiext.CustomResourceDefinition)
   115  	}
   116  	if p.FlattenedSchemata == nil {
   117  		p.FlattenedSchemata = make(map[TypeIdent]apiext.JSONSchemaProps)
   118  	}
   119  }
   120  
   121  // indexTypes loads all types in the package into Types.
   122  func (p *Parser) indexTypes(pkg *loader.Package) {
   123  	// autodetect
   124  	pkgMarkers, err := markers.PackageMarkers(p.Collector, pkg)
   125  	if err != nil {
   126  		pkg.AddError(err)
   127  	} else {
   128  		if skipPkg := pkgMarkers.Get("kubebuilder:skip"); skipPkg != nil {
   129  			return
   130  		}
   131  		if nameVal := pkgMarkers.Get("groupName"); nameVal != nil {
   132  			versionVal := pkg.Name // a reasonable guess
   133  			if versionMarker := pkgMarkers.Get("versionName"); versionMarker != nil {
   134  				versionVal = versionMarker.(string)
   135  			}
   136  
   137  			p.GroupVersions[pkg] = schema.GroupVersion{
   138  				Version: versionVal,
   139  				Group:   nameVal.(string),
   140  			}
   141  		}
   142  	}
   143  
   144  	if err := markers.EachType(p.Collector, pkg, func(info *markers.TypeInfo) {
   145  		ident := TypeIdent{
   146  			Package: pkg,
   147  			Name:    info.Name,
   148  		}
   149  
   150  		p.Types[ident] = info
   151  	}); err != nil {
   152  		pkg.AddError(err)
   153  	}
   154  }
   155  
   156  // LookupType fetches type info from Types.
   157  func (p *Parser) LookupType(pkg *loader.Package, name string) *markers.TypeInfo {
   158  	return p.Types[TypeIdent{Package: pkg, Name: name}]
   159  }
   160  
   161  // NeedSchemaFor indicates that a schema should be generated for the given type.
   162  func (p *Parser) NeedSchemaFor(typ TypeIdent) {
   163  	p.init()
   164  
   165  	p.NeedPackage(typ.Package)
   166  	if _, knownSchema := p.Schemata[typ]; knownSchema {
   167  		return
   168  	}
   169  
   170  	info, knownInfo := p.Types[typ]
   171  	if !knownInfo {
   172  		typ.Package.AddError(fmt.Errorf("unknown type %s", typ))
   173  		return
   174  	}
   175  
   176  	// avoid tripping recursive schemata, like ManagedFields, by adding an empty WIP schema
   177  	p.Schemata[typ] = apiext.JSONSchemaProps{}
   178  
   179  	schemaCtx := newSchemaContext(typ.Package, p, p.AllowDangerousTypes)
   180  	ctxForInfo := schemaCtx.ForInfo(info)
   181  
   182  	pkgMarkers, err := markers.PackageMarkers(p.Collector, typ.Package)
   183  	if err != nil {
   184  		typ.Package.AddError(err)
   185  	}
   186  	ctxForInfo.PackageMarkers = pkgMarkers
   187  
   188  	schema := infoToSchema(ctxForInfo)
   189  
   190  	p.Schemata[typ] = *schema
   191  }
   192  
   193  func (p *Parser) NeedFlattenedSchemaFor(typ TypeIdent) {
   194  	p.init()
   195  
   196  	if _, knownSchema := p.FlattenedSchemata[typ]; knownSchema {
   197  		return
   198  	}
   199  
   200  	p.NeedSchemaFor(typ)
   201  	partialFlattened := p.flattener.FlattenType(typ)
   202  	fullyFlattened := FlattenEmbedded(partialFlattened, typ.Package)
   203  
   204  	p.FlattenedSchemata[typ] = *fullyFlattened
   205  }
   206  
   207  // NeedCRDFor lives off in spec.go
   208  
   209  // AddPackage indicates that types and type-checking information is needed
   210  // for the the given package, *ignoring* overrides.
   211  // Generally, consumers should call NeedPackage, while PackageOverrides should
   212  // call AddPackage to continue with the normal loading procedure.
   213  func (p *Parser) AddPackage(pkg *loader.Package) {
   214  	p.init()
   215  	if _, checked := p.packages[pkg]; checked {
   216  		return
   217  	}
   218  	p.indexTypes(pkg)
   219  	p.Checker.Check(pkg, filterTypesForCRDs)
   220  	p.packages[pkg] = struct{}{}
   221  }
   222  
   223  // NeedPackage indicates that types and type-checking information
   224  // is needed for the given package.
   225  func (p *Parser) NeedPackage(pkg *loader.Package) {
   226  	p.init()
   227  	if _, checked := p.packages[pkg]; checked {
   228  		return
   229  	}
   230  	// overrides are going to be written without vendor.  This is why we index by the actual
   231  	// object when we can.
   232  	if override, overridden := p.PackageOverrides[loader.NonVendorPath(pkg.PkgPath)]; overridden {
   233  		override(p, pkg)
   234  		p.packages[pkg] = struct{}{}
   235  		return
   236  	}
   237  	p.AddPackage(pkg)
   238  }
   239  
   240  // filterTypesForCRDs filters out all nodes that aren't used in CRD generation,
   241  // like interfaces and struct fields without JSON tag.
   242  func filterTypesForCRDs(node ast.Node) bool {
   243  	switch node := node.(type) {
   244  	case *ast.InterfaceType:
   245  		// skip interfaces, we never care about references in them
   246  		return false
   247  	case *ast.StructType:
   248  		return true
   249  	case *ast.Field:
   250  		_, hasTag := loader.ParseAstTag(node.Tag).Lookup("json")
   251  		// fields without JSON tags mean we have custom serialization,
   252  		// so only visit fields with tags.
   253  		return hasTag
   254  	default:
   255  		return true
   256  	}
   257  }