github.com/TheSpiritXIII/controller-tools@v0.14.1/pkg/crd/spec.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  package crd
    17  
    18  import (
    19  	"fmt"
    20  	"slices"
    21  	"sort"
    22  	"strings"
    23  
    24  	"github.com/gobuffalo/flect"
    25  
    26  	apiext "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
    27  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    28  	"k8s.io/apimachinery/pkg/runtime/schema"
    29  
    30  	"github.com/TheSpiritXIII/controller-tools/pkg/loader"
    31  )
    32  
    33  // SpecMarker is a marker that knows how to apply itself to a particular
    34  // version in a CRD Spec.
    35  type SpecMarker interface {
    36  	// ApplyToCRD applies this marker to the given CRD, in the given version
    37  	// within that CRD.  It's called after everything else in the CRD is populated.
    38  	ApplyToCRD(crd *apiext.CustomResourceDefinitionSpec, version string) error
    39  }
    40  
    41  // Marker is a marker that knows how to apply itself to a particular
    42  // version in a CRD.
    43  type Marker interface {
    44  	// ApplyToCRD applies this marker to the given CRD, in the given version
    45  	// within that CRD.  It's called after everything else in the CRD is populated.
    46  	ApplyToCRD(crd *apiext.CustomResourceDefinition, version string) error
    47  }
    48  
    49  // NeedCRDFor requests the full CRD for the given group-kind.  It requires
    50  // that the packages containing the Go structs for that CRD have already
    51  // been loaded with NeedPackage.
    52  func (p *Parser) NeedCRDFor(groupKind schema.GroupKind, maxDescLen *int) {
    53  	p.init()
    54  
    55  	if _, exists := p.CustomResourceDefinitions[groupKind]; exists {
    56  		return
    57  	}
    58  
    59  	var packages []*loader.Package
    60  	for pkg, gv := range p.GroupVersions {
    61  		if gv.Group != groupKind.Group {
    62  			continue
    63  		}
    64  		packages = append(packages, pkg)
    65  	}
    66  
    67  	defaultPlural := strings.ToLower(flect.Pluralize(groupKind.Kind))
    68  	crd := apiext.CustomResourceDefinition{
    69  		TypeMeta: metav1.TypeMeta{
    70  			APIVersion: apiext.SchemeGroupVersion.String(),
    71  			Kind:       "CustomResourceDefinition",
    72  		},
    73  		ObjectMeta: metav1.ObjectMeta{
    74  			Name: defaultPlural + "." + groupKind.Group,
    75  		},
    76  		Spec: apiext.CustomResourceDefinitionSpec{
    77  			Group: groupKind.Group,
    78  			Names: apiext.CustomResourceDefinitionNames{
    79  				Kind:     groupKind.Kind,
    80  				ListKind: groupKind.Kind + "List",
    81  				Plural:   defaultPlural,
    82  				Singular: strings.ToLower(groupKind.Kind),
    83  			},
    84  			Scope: apiext.NamespaceScoped,
    85  		},
    86  	}
    87  
    88  	for _, pkg := range packages {
    89  		typeIdent := TypeIdent{Package: pkg, Name: groupKind.Kind}
    90  		typeInfo := p.Types[typeIdent]
    91  		if typeInfo == nil {
    92  			continue
    93  		}
    94  		p.NeedFlattenedSchemaFor(typeIdent)
    95  		fullSchema := p.FlattenedSchemata[typeIdent]
    96  		fullSchema = *fullSchema.DeepCopy() // don't mutate the cache (we might be truncating description, etc)
    97  		if maxDescLen != nil {
    98  			TruncateDescription(&fullSchema, *maxDescLen)
    99  		}
   100  		ver := apiext.CustomResourceDefinitionVersion{
   101  			Name:   p.GroupVersions[pkg].Version,
   102  			Served: true,
   103  			Schema: &apiext.CustomResourceValidation{
   104  				OpenAPIV3Schema: &fullSchema, // fine to take a reference since we deepcopy above
   105  			},
   106  		}
   107  		crd.Spec.Versions = append(crd.Spec.Versions, ver)
   108  
   109  	}
   110  
   111  	// markers are applied *after* initial generation of objects
   112  	for _, pkg := range packages {
   113  		typeIdent := TypeIdent{Package: pkg, Name: groupKind.Kind}
   114  		typeInfo := p.Types[typeIdent]
   115  		if typeInfo == nil {
   116  			continue
   117  		}
   118  		ver := p.GroupVersions[pkg].Version
   119  
   120  		for _, markerVals := range typeInfo.Markers {
   121  			for _, val := range markerVals {
   122  				if specMarker, isSpecMarker := val.(SpecMarker); isSpecMarker {
   123  					if err := specMarker.ApplyToCRD(&crd.Spec, ver); err != nil {
   124  						pkg.AddError(loader.ErrFromNode(err /* an okay guess */, typeInfo.RawSpec))
   125  					}
   126  				} else if crdMarker, isCRDMarker := val.(Marker); isCRDMarker {
   127  					if err := crdMarker.ApplyToCRD(&crd, ver); err != nil {
   128  						pkg.AddError(loader.ErrFromNode(err /* an okay guess */, typeInfo.RawSpec))
   129  					}
   130  				}
   131  			}
   132  		}
   133  	}
   134  
   135  	// Apply field-scoped resources. The markers live on the field, not in the top-level CRD, so we
   136  	// must apply them manually here.
   137  	for versionIndex := range crd.Spec.Versions {
   138  		version := &crd.Spec.Versions[versionIndex]
   139  		if err := applyFieldScopes(version.Schema.OpenAPIV3Schema, crd.Spec.Scope); err != nil {
   140  			packages[0].AddError(fmt.Errorf("CRD for %s was unable to apply field scopes", groupKind))
   141  		}
   142  	}
   143  
   144  	// fix the name if the plural was changed (this is the form the name *has* to take, so no harm in changing it).
   145  	crd.Name = crd.Spec.Names.Plural + "." + groupKind.Group
   146  
   147  	// nothing to actually write
   148  	if len(crd.Spec.Versions) == 0 {
   149  		return
   150  	}
   151  
   152  	// it is necessary to make sure the order of CRD versions in crd.Spec.Versions is stable and explicitly set crd.Spec.Version.
   153  	// Otherwise, crd.Spec.Version may point to different CRD versions across different runs.
   154  	sort.Slice(crd.Spec.Versions, func(i, j int) bool { return crd.Spec.Versions[i].Name < crd.Spec.Versions[j].Name })
   155  
   156  	// make sure we have *a* storage version
   157  	// (default it if we only have one, otherwise, bail)
   158  	if len(crd.Spec.Versions) == 1 {
   159  		crd.Spec.Versions[0].Storage = true
   160  	}
   161  
   162  	hasStorage := false
   163  	for _, ver := range crd.Spec.Versions {
   164  		if ver.Storage {
   165  			hasStorage = true
   166  			break
   167  		}
   168  	}
   169  	if !hasStorage {
   170  		// just add the error to the first relevant package for this CRD,
   171  		// since there's no specific error location
   172  		packages[0].AddError(fmt.Errorf("CRD for %s has no storage version", groupKind))
   173  	}
   174  
   175  	served := false
   176  	for _, ver := range crd.Spec.Versions {
   177  		if ver.Served {
   178  			served = true
   179  			break
   180  		}
   181  	}
   182  	if !served {
   183  		// just add the error to the first relevant package for this CRD,
   184  		// since there's no specific error location
   185  		packages[0].AddError(fmt.Errorf("CRD for %s with version(s) %v does not serve any version", groupKind, crd.Spec.Versions))
   186  	}
   187  
   188  	p.CustomResourceDefinitions[groupKind] = crd
   189  }
   190  
   191  func applyFieldScopes(props *apiext.JSONSchemaProps, scope apiext.ResourceScope) error {
   192  	var removed string
   193  	if scope == apiext.NamespaceScoped {
   194  		removed = string(apiext.ClusterScoped)
   195  	} else if scope == apiext.ClusterScoped {
   196  		removed = string(apiext.NamespaceScoped)
   197  	}
   198  	if err := removeScope(props, removed); err != nil {
   199  		return err
   200  	}
   201  	return nil
   202  }
   203  
   204  func removeScope(props *apiext.JSONSchemaProps, scope string) error {
   205  	scopes, ok := props.Properties[fieldScopePropertyName]
   206  	if ok {
   207  		for _, item := range scopes.Properties[scope].Required {
   208  			delete(props.Properties, item)
   209  
   210  			index := slices.Index(props.Required, item)
   211  			if index == -1 {
   212  				continue
   213  			}
   214  			props.Required = slices.Delete(props.Required, index, index+1)
   215  		}
   216  	}
   217  	delete(props.Properties, fieldScopePropertyName)
   218  
   219  	for name, p := range props.Properties {
   220  		removeScope(&p, scope)
   221  		props.Properties[name] = p
   222  	}
   223  	return nil
   224  }