github.com/regadas/controller-tools@v0.5.1-0.20210408091555-18885b17ff7b/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/ast" 22 "go/types" 23 "os" 24 25 apiext "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" 26 apiextlegacy "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1" 27 "k8s.io/apimachinery/pkg/runtime/schema" 28 29 crdmarkers "github.com/regadas/controller-tools/pkg/crd/markers" 30 "github.com/regadas/controller-tools/pkg/genall" 31 "github.com/regadas/controller-tools/pkg/loader" 32 "github.com/regadas/controller-tools/pkg/markers" 33 "github.com/regadas/controller-tools/pkg/version" 34 ) 35 36 // The default CustomResourceDefinition version to generate. 37 const defaultVersion = "v1" 38 39 // +controllertools:marker:generateHelp 40 41 // Generator generates CustomResourceDefinition objects. 42 type Generator struct { 43 // TrivialVersions indicates that we should produce a single-version CRD. 44 // 45 // Single "trivial-version" CRDs are compatible with older (pre 1.13) 46 // Kubernetes API servers. The storage version's schema will be used as 47 // the CRD's schema. 48 // 49 // Only works with the v1beta1 CRD version. 50 TrivialVersions bool `marker:",optional"` 51 52 // PreserveUnknownFields indicates whether or not we should turn off pruning. 53 // 54 // Left unspecified, it'll default to true when only a v1beta1 CRD is 55 // generated (to preserve compatibility with older versions of this tool), 56 // or false otherwise. 57 // 58 // It's required to be false for v1 CRDs. 59 PreserveUnknownFields *bool `marker:",optional"` 60 61 // AllowDangerousTypes allows types which are usually omitted from CRD generation 62 // because they are not recommended. 63 // 64 // Currently the following additional types are allowed when this is true: 65 // float32 66 // float64 67 // 68 // Left unspecified, the default is false 69 AllowDangerousTypes *bool `marker:",optional"` 70 71 // MaxDescLen specifies the maximum description length for fields in CRD's OpenAPI schema. 72 // 73 // 0 indicates drop the description for all fields completely. 74 // n indicates limit the description to at most n characters and truncate the description to 75 // closest sentence boundary if it exceeds n characters. 76 MaxDescLen *int `marker:",optional"` 77 78 // CRDVersions specifies the target API versions of the CRD type itself to 79 // generate. Defaults to v1. 80 // 81 // The first version listed will be assumed to be the "default" version and 82 // will not get a version suffix in the output filename. 83 // 84 // You'll need to use "v1" to get support for features like defaulting, 85 // along with an API server that supports it (Kubernetes 1.16+). 86 CRDVersions []string `marker:"crdVersions,optional"` 87 88 // GenerateEmbeddedObjectMeta specifies if any embedded ObjectMeta in the CRD should be generated 89 GenerateEmbeddedObjectMeta *bool `marker:",optional"` 90 } 91 92 func (Generator) CheckFilter() loader.NodeFilter { 93 return filterTypesForCRDs 94 } 95 func (Generator) RegisterMarkers(into *markers.Registry) error { 96 return crdmarkers.Register(into) 97 } 98 func (g Generator) Generate(ctx *genall.GenerationContext) error { 99 parser := &Parser{ 100 Collector: ctx.Collector, 101 Checker: ctx.Checker, 102 // Perform defaulting here to avoid ambiguity later 103 AllowDangerousTypes: g.AllowDangerousTypes != nil && *g.AllowDangerousTypes == true, 104 // Indicates the parser on whether to register the ObjectMeta type or not 105 GenerateEmbeddedObjectMeta: g.GenerateEmbeddedObjectMeta != nil && *g.GenerateEmbeddedObjectMeta == true, 106 } 107 108 AddKnownTypes(parser) 109 for _, root := range ctx.Roots { 110 parser.NeedPackage(root) 111 } 112 113 metav1Pkg := FindMetav1(ctx.Roots) 114 if metav1Pkg == nil { 115 // no objects in the roots, since nothing imported metav1 116 return nil 117 } 118 119 // TODO: allow selecting a specific object 120 kubeKinds := FindKubeKinds(parser, metav1Pkg) 121 if len(kubeKinds) == 0 { 122 // no objects in the roots 123 return nil 124 } 125 126 crdVersions := g.CRDVersions 127 128 if len(crdVersions) == 0 { 129 crdVersions = []string{defaultVersion} 130 } 131 132 for groupKind := range kubeKinds { 133 parser.NeedCRDFor(groupKind, g.MaxDescLen) 134 crdRaw := parser.CustomResourceDefinitions[groupKind] 135 addAttribution(&crdRaw) 136 137 // Prevent the top level metadata for the CRD to be generate regardless of the intention in the arguments 138 FixTopLevelMetadata(crdRaw) 139 140 versionedCRDs := make([]interface{}, len(crdVersions)) 141 for i, ver := range crdVersions { 142 conv, err := AsVersion(crdRaw, schema.GroupVersion{Group: apiext.SchemeGroupVersion.Group, Version: ver}) 143 if err != nil { 144 return err 145 } 146 versionedCRDs[i] = conv 147 } 148 149 if g.TrivialVersions { 150 for i, crd := range versionedCRDs { 151 if crdVersions[i] == "v1beta1" { 152 toTrivialVersions(crd.(*apiextlegacy.CustomResourceDefinition)) 153 } 154 } 155 } 156 157 // *If* we're only generating v1beta1 CRDs, default to `preserveUnknownFields: (unset)` 158 // for compatibility purposes. In any other case, default to false, since that's 159 // the sensible default and is required for v1. 160 v1beta1Only := len(crdVersions) == 1 && crdVersions[0] == "v1beta1" 161 switch { 162 case (g.PreserveUnknownFields == nil || *g.PreserveUnknownFields) && v1beta1Only: 163 crd := versionedCRDs[0].(*apiextlegacy.CustomResourceDefinition) 164 crd.Spec.PreserveUnknownFields = nil 165 case g.PreserveUnknownFields == nil, g.PreserveUnknownFields != nil && !*g.PreserveUnknownFields: 166 // it'll be false here (coming from v1) -- leave it as such 167 default: 168 return fmt.Errorf("you may only set PreserveUnknownFields to true with v1beta1 CRDs") 169 } 170 171 for i, crd := range versionedCRDs { 172 // defaults are not allowed to be specified in v1beta1 CRDs and 173 // decriptions are not allowed on the metadata regardless of version 174 // strip them before writing to a file 175 if crdVersions[i] == "v1beta1" { 176 removeDefaultsFromSchemas(crd.(*apiextlegacy.CustomResourceDefinition)) 177 removeDescriptionFromMetadataLegacy(crd.(*apiextlegacy.CustomResourceDefinition)) 178 } else { 179 removeDescriptionFromMetadata(crd.(*apiext.CustomResourceDefinition)) 180 } 181 var fileName string 182 if i == 0 { 183 fileName = fmt.Sprintf("%s_%s.yaml", crdRaw.Spec.Group, crdRaw.Spec.Names.Plural) 184 } else { 185 fileName = fmt.Sprintf("%s_%s.%s.yaml", crdRaw.Spec.Group, crdRaw.Spec.Names.Plural, crdVersions[i]) 186 } 187 if err := ctx.WriteYAML(fileName, crd); err != nil { 188 return err 189 } 190 } 191 } 192 193 return nil 194 } 195 196 func removeDescriptionFromMetadata(crd *apiext.CustomResourceDefinition) { 197 for _, versionSpec := range crd.Spec.Versions { 198 if versionSpec.Schema != nil { 199 removeDescriptionFromMetadataProps(versionSpec.Schema.OpenAPIV3Schema) 200 } 201 } 202 } 203 204 func removeDescriptionFromMetadataProps(v *apiext.JSONSchemaProps) { 205 if m, ok := v.Properties["metadata"]; ok { 206 meta := &m 207 if meta.Description != "" { 208 meta.Description = "" 209 v.Properties["metadata"] = m 210 211 } 212 } 213 } 214 215 func removeDescriptionFromMetadataLegacy(crd *apiextlegacy.CustomResourceDefinition) { 216 if crd.Spec.Validation != nil { 217 removeDescriptionFromMetadataPropsLegacy(crd.Spec.Validation.OpenAPIV3Schema) 218 } 219 for _, versionSpec := range crd.Spec.Versions { 220 if versionSpec.Schema != nil { 221 removeDescriptionFromMetadataPropsLegacy(versionSpec.Schema.OpenAPIV3Schema) 222 } 223 } 224 } 225 226 func removeDescriptionFromMetadataPropsLegacy(v *apiextlegacy.JSONSchemaProps) { 227 if m, ok := v.Properties["metadata"]; ok { 228 meta := &m 229 if meta.Description != "" { 230 meta.Description = "" 231 v.Properties["metadata"] = m 232 233 } 234 } 235 } 236 237 // removeDefaultsFromSchemas will remove all instances of default values being 238 // specified across all defined API versions 239 func removeDefaultsFromSchemas(crd *apiextlegacy.CustomResourceDefinition) { 240 if crd.Spec.Validation != nil { 241 removeDefaultsFromSchemaProps(crd.Spec.Validation.OpenAPIV3Schema) 242 } 243 244 for _, versionSpec := range crd.Spec.Versions { 245 if versionSpec.Schema != nil { 246 removeDefaultsFromSchemaProps(versionSpec.Schema.OpenAPIV3Schema) 247 } 248 } 249 } 250 251 // removeDefaultsFromSchemaProps will recurse into JSONSchemaProps to remove 252 // all instances of default values being specified 253 func removeDefaultsFromSchemaProps(v *apiextlegacy.JSONSchemaProps) { 254 if v == nil { 255 return 256 } 257 258 if v.Default != nil { 259 fmt.Fprintln(os.Stderr, "Warning: default unsupported in CRD version v1beta1, v1 required. Removing defaults.") 260 } 261 262 // nil-out the default field 263 v.Default = nil 264 for name, prop := range v.Properties { 265 // iter var reference is fine -- we handle the persistence of the modfications on the line below 266 //nolint:gosec 267 removeDefaultsFromSchemaProps(&prop) 268 v.Properties[name] = prop 269 } 270 if v.Items != nil { 271 removeDefaultsFromSchemaProps(v.Items.Schema) 272 for i := range v.Items.JSONSchemas { 273 props := v.Items.JSONSchemas[i] 274 removeDefaultsFromSchemaProps(&props) 275 v.Items.JSONSchemas[i] = props 276 } 277 } 278 } 279 280 // FixTopLevelMetadata resets the schema for the top-level metadata field which is needed for CRD validation 281 func FixTopLevelMetadata(crd apiext.CustomResourceDefinition) { 282 for _, v := range crd.Spec.Versions { 283 if v.Schema != nil && v.Schema.OpenAPIV3Schema != nil && v.Schema.OpenAPIV3Schema.Properties != nil { 284 schemaProperties := v.Schema.OpenAPIV3Schema.Properties 285 if _, ok := schemaProperties["metadata"]; ok { 286 schemaProperties["metadata"] = apiext.JSONSchemaProps{Type: "object"} 287 } 288 } 289 290 } 291 292 } 293 294 // toTrivialVersions strips out all schemata except for the storage schema, 295 // and moves that up into the root object. This makes the CRD compatible 296 // with pre 1.13 clusters. 297 func toTrivialVersions(crd *apiextlegacy.CustomResourceDefinition) { 298 var canonicalSchema *apiextlegacy.CustomResourceValidation 299 var canonicalSubresources *apiextlegacy.CustomResourceSubresources 300 var canonicalColumns []apiextlegacy.CustomResourceColumnDefinition 301 for i, ver := range crd.Spec.Versions { 302 if ver.Storage == true { 303 canonicalSchema = ver.Schema 304 canonicalSubresources = ver.Subresources 305 canonicalColumns = ver.AdditionalPrinterColumns 306 } 307 crd.Spec.Versions[i].Schema = nil 308 crd.Spec.Versions[i].Subresources = nil 309 crd.Spec.Versions[i].AdditionalPrinterColumns = nil 310 } 311 if canonicalSchema == nil { 312 return 313 } 314 315 crd.Spec.Validation = canonicalSchema 316 crd.Spec.Subresources = canonicalSubresources 317 crd.Spec.AdditionalPrinterColumns = canonicalColumns 318 } 319 320 // addAttribution adds attribution info to indicate controller-gen tool was used 321 // to generate this CRD definition along with the version info. 322 func addAttribution(crd *apiext.CustomResourceDefinition) { 323 if crd.ObjectMeta.Annotations == nil { 324 crd.ObjectMeta.Annotations = map[string]string{} 325 } 326 crd.ObjectMeta.Annotations["controller-gen.kubebuilder.io/version"] = version.Version() 327 } 328 329 // FindMetav1 locates the actual package representing metav1 amongst 330 // the imports of the roots. 331 func FindMetav1(roots []*loader.Package) *loader.Package { 332 for _, root := range roots { 333 pkg := root.Imports()["k8s.io/apimachinery/pkg/apis/meta/v1"] 334 if pkg != nil { 335 return pkg 336 } 337 } 338 return nil 339 } 340 341 // FindKubeKinds locates all types that contain TypeMeta and ObjectMeta 342 // (and thus may be a Kubernetes object), and returns the corresponding 343 // group-kinds. 344 func FindKubeKinds(parser *Parser, metav1Pkg *loader.Package) map[schema.GroupKind]struct{} { 345 // TODO(directxman12): technically, we should be finding metav1 per-package 346 kubeKinds := map[schema.GroupKind]struct{}{} 347 for typeIdent, info := range parser.Types { 348 hasObjectMeta := false 349 hasTypeMeta := false 350 351 pkg := typeIdent.Package 352 pkg.NeedTypesInfo() 353 typesInfo := pkg.TypesInfo 354 355 for _, field := range info.Fields { 356 if field.Name != "" { 357 // type and object meta are embedded, 358 // so they can't be this 359 continue 360 } 361 362 fieldType := typesInfo.TypeOf(field.RawField.Type) 363 namedField, isNamed := fieldType.(*types.Named) 364 if !isNamed { 365 // ObjectMeta and TypeMeta are named types 366 continue 367 } 368 if namedField.Obj().Pkg() == nil { 369 // Embedded non-builtin universe type (specifically, it's probably `error`), 370 // so it can't be ObjectMeta or TypeMeta 371 continue 372 } 373 fieldPkgPath := loader.NonVendorPath(namedField.Obj().Pkg().Path()) 374 fieldPkg := pkg.Imports()[fieldPkgPath] 375 if fieldPkg != metav1Pkg { 376 continue 377 } 378 379 switch namedField.Obj().Name() { 380 case "ObjectMeta": 381 hasObjectMeta = true 382 case "TypeMeta": 383 hasTypeMeta = true 384 } 385 } 386 387 if !hasObjectMeta || !hasTypeMeta { 388 continue 389 } 390 391 groupKind := schema.GroupKind{ 392 Group: parser.GroupVersions[pkg].Group, 393 Kind: typeIdent.Name, 394 } 395 kubeKinds[groupKind] = struct{}{} 396 } 397 398 return kubeKinds 399 } 400 401 // filterTypesForCRDs filters out all nodes that aren't used in CRD generation, 402 // like interfaces and struct fields without JSON tag. 403 func filterTypesForCRDs(node ast.Node) bool { 404 switch node := node.(type) { 405 case *ast.InterfaceType: 406 // skip interfaces, we never care about references in them 407 return false 408 case *ast.StructType: 409 return true 410 case *ast.Field: 411 _, hasTag := loader.ParseAstTag(node.Tag).Lookup("json") 412 // fields without JSON tags mean we have custom serialization, 413 // so only visit fields with tags. 414 return hasTag 415 default: 416 return true 417 } 418 }