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 }