github.com/cilium/controller-tools@v0.3.1-0.20230329170030-f2b7ff866fde/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/types" 22 23 apiext "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" 24 apiextlegacy "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1" 25 "k8s.io/apimachinery/pkg/runtime/schema" 26 27 crdmarkers "sigs.k8s.io/controller-tools/pkg/crd/markers" 28 "sigs.k8s.io/controller-tools/pkg/genall" 29 "sigs.k8s.io/controller-tools/pkg/loader" 30 "sigs.k8s.io/controller-tools/pkg/markers" 31 "sigs.k8s.io/controller-tools/pkg/version" 32 ) 33 34 // The default CustomResourceDefinition version to generate. 35 const defaultVersion = "v1" 36 37 // +controllertools:marker:generateHelp 38 39 // Generator generates CustomResourceDefinition objects. 40 type Generator struct { 41 // TrivialVersions indicates that we should produce a single-version CRD. 42 // 43 // Single "trivial-version" CRDs are compatible with older (pre 1.13) 44 // Kubernetes API servers. The storage version's schema will be used as 45 // the CRD's schema. 46 // 47 // Only works with the v1beta1 CRD version. 48 TrivialVersions bool `marker:",optional"` 49 50 // PreserveUnknownFields indicates whether or not we should turn off pruning. 51 // 52 // Left unspecified, it'll default to true when only a v1beta1 CRD is 53 // generated (to preserve compatibility with older versions of this tool), 54 // or false otherwise. 55 // 56 // It's required to be false for v1 CRDs. 57 PreserveUnknownFields *bool `marker:",optional"` 58 59 // AllowDangerousTypes allows types which are usually omitted from CRD generation 60 // because they are not recommended. 61 // 62 // Currently the following additional types are allowed when this is true: 63 // float32 64 // float64 65 // 66 // Left unspecified, the default is false 67 AllowDangerousTypes *bool `marker:",optional"` 68 69 // MaxDescLen specifies the maximum description length for fields in CRD's OpenAPI schema. 70 // 71 // 0 indicates drop the description for all fields completely. 72 // n indicates limit the description to at most n characters and truncate the description to 73 // closest sentence boundary if it exceeds n characters. 74 MaxDescLen *int `marker:",optional"` 75 76 // CRDVersions specifies the target API versions of the CRD type itself to 77 // generate. Defaults to v1. 78 // 79 // The first version listed will be assumed to be the "default" version and 80 // will not get a version suffix in the output filename. 81 // 82 // You'll need to use "v1" to get support for features like defaulting, 83 // along with an API server that supports it (Kubernetes 1.16+). 84 CRDVersions []string `marker:"crdVersions,optional"` 85 } 86 87 func (Generator) RegisterMarkers(into *markers.Registry) error { 88 return crdmarkers.Register(into) 89 } 90 func (g Generator) Generate(ctx *genall.GenerationContext) error { 91 parser := &Parser{ 92 Collector: ctx.Collector, 93 Checker: ctx.Checker, 94 // Perform defaulting here to avoid ambiguity later 95 AllowDangerousTypes: g.AllowDangerousTypes != nil && *g.AllowDangerousTypes == true, 96 } 97 98 AddKnownTypes(parser) 99 for _, root := range ctx.Roots { 100 parser.NeedPackage(root) 101 } 102 103 metav1Pkg := FindMetav1(ctx.Roots) 104 if metav1Pkg == nil { 105 // no objects in the roots, since nothing imported metav1 106 return nil 107 } 108 109 // TODO: allow selecting a specific object 110 kubeKinds := FindKubeKinds(parser, metav1Pkg) 111 if len(kubeKinds) == 0 { 112 // no objects in the roots 113 return nil 114 } 115 116 crdVersions := g.CRDVersions 117 118 if len(crdVersions) == 0 { 119 crdVersions = []string{defaultVersion} 120 } 121 122 for groupKind := range kubeKinds { 123 parser.NeedCRDFor(groupKind, g.MaxDescLen) 124 crdRaw := parser.CustomResourceDefinitions[groupKind] 125 addAttribution(&crdRaw) 126 127 versionedCRDs := make([]interface{}, len(crdVersions)) 128 for i, ver := range crdVersions { 129 conv, err := AsVersion(crdRaw, schema.GroupVersion{Group: apiext.SchemeGroupVersion.Group, Version: ver}) 130 if err != nil { 131 return err 132 } 133 versionedCRDs[i] = conv 134 } 135 136 if g.TrivialVersions { 137 for i, crd := range versionedCRDs { 138 if crdVersions[i] == "v1beta1" { 139 toTrivialVersions(crd.(*apiextlegacy.CustomResourceDefinition)) 140 } 141 } 142 } 143 144 // *If* we're only generating v1beta1 CRDs, default to `preserveUnknownFields: (unset)` 145 // for compatibility purposes. In any other case, default to false, since that's 146 // the sensible default and is required for v1. 147 v1beta1Only := len(crdVersions) == 1 && crdVersions[0] == "v1beta1" 148 switch { 149 case (g.PreserveUnknownFields == nil || *g.PreserveUnknownFields) && v1beta1Only: 150 crd := versionedCRDs[0].(*apiextlegacy.CustomResourceDefinition) 151 crd.Spec.PreserveUnknownFields = nil 152 case g.PreserveUnknownFields == nil, g.PreserveUnknownFields != nil && !*g.PreserveUnknownFields: 153 // it'll be false here (coming from v1) -- leave it as such 154 default: 155 return fmt.Errorf("you may only set PreserveUnknownFields to true with v1beta1 CRDs") 156 } 157 158 for i, crd := range versionedCRDs { 159 // defaults are not allowed to be specified in v1beta1 CRDs, so strip them 160 // before writing to a file 161 if crdVersions[i] == "v1beta1" { 162 removeDefaultsFromSchemas(crd.(*apiextlegacy.CustomResourceDefinition)) 163 } 164 var fileName string 165 if i == 0 { 166 fileName = fmt.Sprintf("%s_%s.yaml", crdRaw.Spec.Group, crdRaw.Spec.Names.Plural) 167 } else { 168 fileName = fmt.Sprintf("%s_%s.%s.yaml", crdRaw.Spec.Group, crdRaw.Spec.Names.Plural, crdVersions[i]) 169 } 170 if err := ctx.WriteYAML(fileName, crd); err != nil { 171 return err 172 } 173 } 174 } 175 176 return nil 177 } 178 179 // removeDefaultsFromSchemas will remove all instances of default values being 180 // specified across all defined API versions 181 func removeDefaultsFromSchemas(crd *apiextlegacy.CustomResourceDefinition) { 182 if crd.Spec.Validation != nil { 183 removeDefaultsFromSchemaProps(crd.Spec.Validation.OpenAPIV3Schema) 184 } 185 186 for _, versionSpec := range crd.Spec.Versions { 187 if versionSpec.Schema != nil { 188 removeDefaultsFromSchemaProps(versionSpec.Schema.OpenAPIV3Schema) 189 } 190 } 191 } 192 193 // removeDefaultsFromSchemaProps will recurse into JSONSchemaProps to remove 194 // all instances of default values being specified 195 func removeDefaultsFromSchemaProps(v *apiextlegacy.JSONSchemaProps) { 196 if v == nil { 197 return 198 } 199 200 // nil-out the default field 201 v.Default = nil 202 for name, prop := range v.Properties { 203 removeDefaultsFromSchemaProps(&prop) 204 v.Properties[name] = prop 205 } 206 if v.Items != nil { 207 removeDefaultsFromSchemaProps(v.Items.Schema) 208 for i := range v.Items.JSONSchemas { 209 props := v.Items.JSONSchemas[i] 210 removeDefaultsFromSchemaProps(&props) 211 v.Items.JSONSchemas[i] = props 212 } 213 } 214 } 215 216 // toTrivialVersions strips out all schemata except for the storage schema, 217 // and moves that up into the root object. This makes the CRD compatible 218 // with pre 1.13 clusters. 219 func toTrivialVersions(crd *apiextlegacy.CustomResourceDefinition) { 220 var canonicalSchema *apiextlegacy.CustomResourceValidation 221 var canonicalSubresources *apiextlegacy.CustomResourceSubresources 222 var canonicalColumns []apiextlegacy.CustomResourceColumnDefinition 223 for i, ver := range crd.Spec.Versions { 224 if ver.Storage == true { 225 canonicalSchema = ver.Schema 226 canonicalSubresources = ver.Subresources 227 canonicalColumns = ver.AdditionalPrinterColumns 228 } 229 crd.Spec.Versions[i].Schema = nil 230 crd.Spec.Versions[i].Subresources = nil 231 crd.Spec.Versions[i].AdditionalPrinterColumns = nil 232 } 233 if canonicalSchema == nil { 234 return 235 } 236 237 crd.Spec.Validation = canonicalSchema 238 crd.Spec.Subresources = canonicalSubresources 239 crd.Spec.AdditionalPrinterColumns = canonicalColumns 240 } 241 242 // addAttribution adds attribution info to indicate controller-gen tool was used 243 // to generate this CRD definition along with the version info. 244 func addAttribution(crd *apiext.CustomResourceDefinition) { 245 if crd.ObjectMeta.Annotations == nil { 246 crd.ObjectMeta.Annotations = map[string]string{} 247 } 248 crd.ObjectMeta.Annotations["controller-gen.kubebuilder.io/version"] = version.Version() 249 } 250 251 // FindMetav1 locates the actual package representing metav1 amongst 252 // the imports of the roots. 253 func FindMetav1(roots []*loader.Package) *loader.Package { 254 for _, root := range roots { 255 pkg := root.Imports()["k8s.io/apimachinery/pkg/apis/meta/v1"] 256 if pkg != nil { 257 return pkg 258 } 259 } 260 return nil 261 } 262 263 // FindKubeKinds locates all types that contain TypeMeta and ObjectMeta 264 // (and thus may be a Kubernetes object), and returns the corresponding 265 // group-kinds. 266 func FindKubeKinds(parser *Parser, metav1Pkg *loader.Package) map[schema.GroupKind]struct{} { 267 // TODO(directxman12): technically, we should be finding metav1 per-package 268 kubeKinds := map[schema.GroupKind]struct{}{} 269 for typeIdent, info := range parser.Types { 270 hasObjectMeta := false 271 hasTypeMeta := false 272 273 pkg := typeIdent.Package 274 pkg.NeedTypesInfo() 275 typesInfo := pkg.TypesInfo 276 277 for _, field := range info.Fields { 278 if field.Name != "" { 279 // type and object meta are embedded, 280 // so they can't be this 281 continue 282 } 283 284 fieldType := typesInfo.TypeOf(field.RawField.Type) 285 namedField, isNamed := fieldType.(*types.Named) 286 if !isNamed { 287 // ObjectMeta and TypeMeta are named types 288 continue 289 } 290 if namedField.Obj().Pkg() == nil { 291 // Embedded non-builtin universe type (specifically, it's probably `error`), 292 // so it can't be ObjectMeta or TypeMeta 293 continue 294 } 295 fieldPkgPath := loader.NonVendorPath(namedField.Obj().Pkg().Path()) 296 fieldPkg := pkg.Imports()[fieldPkgPath] 297 if fieldPkg != metav1Pkg { 298 continue 299 } 300 301 switch namedField.Obj().Name() { 302 case "ObjectMeta": 303 hasObjectMeta = true 304 case "TypeMeta": 305 hasTypeMeta = true 306 } 307 } 308 309 if !hasObjectMeta || !hasTypeMeta { 310 continue 311 } 312 313 groupKind := schema.GroupKind{ 314 Group: parser.GroupVersions[pkg].Group, 315 Kind: typeIdent.Name, 316 } 317 kubeKinds[groupKind] = struct{}{} 318 } 319 320 return kubeKinds 321 }