github.com/regadas/controller-tools@v0.5.1-0.20210408091555-18885b17ff7b/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  	"sort"
    21  	"strings"
    22  
    23  	"github.com/gobuffalo/flect"
    24  
    25  	apiext "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
    26  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    27  	"k8s.io/apimachinery/pkg/runtime/schema"
    28  
    29  	"github.com/regadas/controller-tools/pkg/loader"
    30  )
    31  
    32  // SpecMarker is a marker that knows how to apply itself to a particular
    33  // version in a CRD.
    34  type SpecMarker interface {
    35  	// ApplyToCRD applies this marker to the given CRD, in the given version
    36  	// within that CRD.  It's called after everything else in the CRD is populated.
    37  	ApplyToCRD(crd *apiext.CustomResourceDefinitionSpec, version string) error
    38  }
    39  
    40  // NeedCRDFor requests the full CRD for the given group-kind.  It requires
    41  // that the packages containing the Go structs for that CRD have already
    42  // been loaded with NeedPackage.
    43  func (p *Parser) NeedCRDFor(groupKind schema.GroupKind, maxDescLen *int) {
    44  	p.init()
    45  
    46  	if _, exists := p.CustomResourceDefinitions[groupKind]; exists {
    47  		return
    48  	}
    49  
    50  	var packages []*loader.Package
    51  	for pkg, gv := range p.GroupVersions {
    52  		if gv.Group != groupKind.Group {
    53  			continue
    54  		}
    55  		packages = append(packages, pkg)
    56  	}
    57  
    58  	defaultPlural := strings.ToLower(flect.Pluralize(groupKind.Kind))
    59  	crd := apiext.CustomResourceDefinition{
    60  		TypeMeta: metav1.TypeMeta{
    61  			APIVersion: apiext.SchemeGroupVersion.String(),
    62  			Kind:       "CustomResourceDefinition",
    63  		},
    64  		ObjectMeta: metav1.ObjectMeta{
    65  			Name: defaultPlural + "." + groupKind.Group,
    66  		},
    67  		Spec: apiext.CustomResourceDefinitionSpec{
    68  			Group: groupKind.Group,
    69  			Names: apiext.CustomResourceDefinitionNames{
    70  				Kind:     groupKind.Kind,
    71  				ListKind: groupKind.Kind + "List",
    72  				Plural:   defaultPlural,
    73  				Singular: strings.ToLower(groupKind.Kind),
    74  			},
    75  			Scope: apiext.NamespaceScoped,
    76  		},
    77  	}
    78  
    79  	for _, pkg := range packages {
    80  		typeIdent := TypeIdent{Package: pkg, Name: groupKind.Kind}
    81  		typeInfo := p.Types[typeIdent]
    82  		if typeInfo == nil {
    83  			continue
    84  		}
    85  		p.NeedFlattenedSchemaFor(typeIdent)
    86  		fullSchema := p.FlattenedSchemata[typeIdent]
    87  		fullSchema = *fullSchema.DeepCopy() // don't mutate the cache (we might be truncating description, etc)
    88  		if maxDescLen != nil {
    89  			TruncateDescription(&fullSchema, *maxDescLen)
    90  		}
    91  		ver := apiext.CustomResourceDefinitionVersion{
    92  			Name:   p.GroupVersions[pkg].Version,
    93  			Served: true,
    94  			Schema: &apiext.CustomResourceValidation{
    95  				OpenAPIV3Schema: &fullSchema, // fine to take a reference since we deepcopy above
    96  			},
    97  		}
    98  		crd.Spec.Versions = append(crd.Spec.Versions, ver)
    99  	}
   100  
   101  	// markers are applied *after* initial generation of objects
   102  	for _, pkg := range packages {
   103  		typeIdent := TypeIdent{Package: pkg, Name: groupKind.Kind}
   104  		typeInfo := p.Types[typeIdent]
   105  		if typeInfo == nil {
   106  			continue
   107  		}
   108  		ver := p.GroupVersions[pkg].Version
   109  
   110  		for _, markerVals := range typeInfo.Markers {
   111  			for _, val := range markerVals {
   112  				crdMarker, isCrdMarker := val.(SpecMarker)
   113  				if !isCrdMarker {
   114  					continue
   115  				}
   116  				if err := crdMarker.ApplyToCRD(&crd.Spec, ver); err != nil {
   117  					pkg.AddError(loader.ErrFromNode(err /* an okay guess */, typeInfo.RawSpec))
   118  				}
   119  			}
   120  		}
   121  	}
   122  
   123  	// fix the name if the plural was changed (this is the form the name *has* to take, so no harm in changing it).
   124  	crd.Name = crd.Spec.Names.Plural + "." + groupKind.Group
   125  
   126  	// nothing to actually write
   127  	if len(crd.Spec.Versions) == 0 {
   128  		return
   129  	}
   130  
   131  	// it is necessary to make sure the order of CRD versions in crd.Spec.Versions is stable and explicitly set crd.Spec.Version.
   132  	// Otherwise, crd.Spec.Version may point to different CRD versions across different runs.
   133  	sort.Slice(crd.Spec.Versions, func(i, j int) bool { return crd.Spec.Versions[i].Name < crd.Spec.Versions[j].Name })
   134  
   135  	// make sure we have *a* storage version
   136  	// (default it if we only have one, otherwise, bail)
   137  	if len(crd.Spec.Versions) == 1 {
   138  		crd.Spec.Versions[0].Storage = true
   139  	}
   140  
   141  	hasStorage := false
   142  	for _, ver := range crd.Spec.Versions {
   143  		if ver.Storage {
   144  			hasStorage = true
   145  			break
   146  		}
   147  	}
   148  	if !hasStorage {
   149  		// just add the error to the first relevant package for this CRD,
   150  		// since there's no specific error location
   151  		packages[0].AddError(fmt.Errorf("CRD for %s has no storage version", groupKind))
   152  	}
   153  
   154  	served := false
   155  	for _, ver := range crd.Spec.Versions {
   156  		if ver.Served {
   157  			served = true
   158  			break
   159  		}
   160  	}
   161  	if !served {
   162  		// just add the error to the first relevant package for this CRD,
   163  		// since there's no specific error location
   164  		packages[0].AddError(fmt.Errorf("CRD for %s with version(s) %v does not serve any version", groupKind, crd.Spec.Versions))
   165  	}
   166  
   167  	// NB(directxman12): CRD's status doesn't have omitempty markers, which means things
   168  	// get serialized as null, which causes the validator to freak out.  Manually set
   169  	// these to empty till we get a better solution.
   170  	crd.Status.Conditions = []apiext.CustomResourceDefinitionCondition{}
   171  	crd.Status.StoredVersions = []string{}
   172  
   173  	p.CustomResourceDefinitions[groupKind] = crd
   174  }