sigs.k8s.io/controller-tools@v0.15.1-0.20240515195456-85686cb69316/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 "sort" 24 "strings" 25 26 apiext "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" 27 "k8s.io/apimachinery/pkg/runtime/schema" 28 29 crdmarkers "sigs.k8s.io/controller-tools/pkg/crd/markers" 30 "sigs.k8s.io/controller-tools/pkg/genall" 31 "sigs.k8s.io/controller-tools/pkg/loader" 32 "sigs.k8s.io/controller-tools/pkg/markers" 33 "sigs.k8s.io/controller-tools/pkg/version" 34 ) 35 36 // The identifier for v1 CustomResourceDefinitions. 37 const v1 = "v1" 38 39 // The default CustomResourceDefinition version to generate. 40 const defaultVersion = v1 41 42 // +controllertools:marker:generateHelp 43 44 // Generator generates CustomResourceDefinition objects. 45 type Generator struct { 46 // IgnoreUnexportedFields indicates that we should skip unexported fields. 47 // 48 // Left unspecified, the default is false. 49 IgnoreUnexportedFields *bool `marker:",optional"` 50 51 // AllowDangerousTypes allows types which are usually omitted from CRD generation 52 // because they are not recommended. 53 // 54 // Currently the following additional types are allowed when this is true: 55 // float32 56 // float64 57 // 58 // Left unspecified, the default is false 59 AllowDangerousTypes *bool `marker:",optional"` 60 61 // MaxDescLen specifies the maximum description length for fields in CRD's OpenAPI schema. 62 // 63 // 0 indicates drop the description for all fields completely. 64 // n indicates limit the description to at most n characters and truncate the description to 65 // closest sentence boundary if it exceeds n characters. 66 MaxDescLen *int `marker:",optional"` 67 68 // CRDVersions specifies the target API versions of the CRD type itself to 69 // generate. Defaults to v1. 70 // 71 // Currently, the only supported value is v1. 72 // 73 // The first version listed will be assumed to be the "default" version and 74 // will not get a version suffix in the output filename. 75 // 76 // You'll need to use "v1" to get support for features like defaulting, 77 // along with an API server that supports it (Kubernetes 1.16+). 78 CRDVersions []string `marker:"crdVersions,optional"` 79 80 // GenerateEmbeddedObjectMeta specifies if any embedded ObjectMeta in the CRD should be generated 81 GenerateEmbeddedObjectMeta *bool `marker:",optional"` 82 83 // HeaderFile specifies the header text (e.g. license) to prepend to generated files. 84 HeaderFile string `marker:",optional"` 85 86 // Year specifies the year to substitute for " YEAR" in the header file. 87 Year string `marker:",optional"` 88 89 // DeprecatedV1beta1CompatibilityPreserveUnknownFields indicates whether 90 // or not we should turn off field pruning for this resource. 91 // 92 // Specifies spec.preserveUnknownFields value that is false and omitted by default. 93 // This value can only be specified for CustomResourceDefinitions that were created with 94 // `apiextensions.k8s.io/v1beta1`. 95 // 96 // The field can be set for compatiblity reasons, although strongly discouraged, resource 97 // authors should move to a structural OpenAPI schema instead. 98 // 99 // See https://kubernetes.io/docs/tasks/extend-kubernetes/custom-resources/custom-resource-definitions/#field-pruning 100 // for more information about field pruning and v1beta1 resources compatibility. 101 DeprecatedV1beta1CompatibilityPreserveUnknownFields *bool `marker:",optional"` 102 } 103 104 func (Generator) CheckFilter() loader.NodeFilter { 105 return filterTypesForCRDs 106 } 107 func (Generator) RegisterMarkers(into *markers.Registry) error { 108 return crdmarkers.Register(into) 109 } 110 111 // transformRemoveCRDStatus ensures we do not write the CRD status field. 112 func transformRemoveCRDStatus(obj map[string]interface{}) error { 113 delete(obj, "status") 114 return nil 115 } 116 117 // transformPreserveUnknownFields adds spec.preserveUnknownFields=value. 118 func transformPreserveUnknownFields(value bool) func(map[string]interface{}) error { 119 return func(obj map[string]interface{}) error { 120 if spec, ok := obj["spec"].(map[interface{}]interface{}); ok { 121 spec["preserveUnknownFields"] = value 122 } 123 return nil 124 } 125 } 126 127 func (g Generator) Generate(ctx *genall.GenerationContext) error { 128 parser := &Parser{ 129 Collector: ctx.Collector, 130 Checker: ctx.Checker, 131 // Perform defaulting here to avoid ambiguity later 132 IgnoreUnexportedFields: g.IgnoreUnexportedFields != nil && *g.IgnoreUnexportedFields, 133 AllowDangerousTypes: g.AllowDangerousTypes != nil && *g.AllowDangerousTypes, 134 // Indicates the parser on whether to register the ObjectMeta type or not 135 GenerateEmbeddedObjectMeta: g.GenerateEmbeddedObjectMeta != nil && *g.GenerateEmbeddedObjectMeta, 136 } 137 138 AddKnownTypes(parser) 139 for _, root := range ctx.Roots { 140 parser.NeedPackage(root) 141 } 142 143 metav1Pkg := FindMetav1(ctx.Roots) 144 if metav1Pkg == nil { 145 // no objects in the roots, since nothing imported metav1 146 return nil 147 } 148 149 // TODO: allow selecting a specific object 150 kubeKinds := FindKubeKinds(parser, metav1Pkg) 151 if len(kubeKinds) == 0 { 152 // no objects in the roots 153 return nil 154 } 155 156 crdVersions := g.CRDVersions 157 158 if len(crdVersions) == 0 { 159 crdVersions = []string{defaultVersion} 160 } 161 162 var headerText string 163 164 if g.HeaderFile != "" { 165 headerBytes, err := ctx.ReadFile(g.HeaderFile) 166 if err != nil { 167 return err 168 } 169 headerText = string(headerBytes) 170 } 171 headerText = strings.ReplaceAll(headerText, " YEAR", " "+g.Year) 172 173 yamlOpts := []*genall.WriteYAMLOptions{ 174 genall.WithTransform(transformRemoveCRDStatus), 175 genall.WithTransform(genall.TransformRemoveCreationTimestamp), 176 } 177 if g.DeprecatedV1beta1CompatibilityPreserveUnknownFields != nil { 178 yamlOpts = append(yamlOpts, genall.WithTransform(transformPreserveUnknownFields(*g.DeprecatedV1beta1CompatibilityPreserveUnknownFields))) 179 } 180 181 for _, groupKind := range kubeKinds { 182 parser.NeedCRDFor(groupKind, g.MaxDescLen) 183 crdRaw := parser.CustomResourceDefinitions[groupKind] 184 addAttribution(&crdRaw) 185 186 // Prevent the top level metadata for the CRD to be generate regardless of the intention in the arguments 187 FixTopLevelMetadata(crdRaw) 188 189 versionedCRDs := make([]interface{}, len(crdVersions)) 190 for i, ver := range crdVersions { 191 conv, err := AsVersion(crdRaw, schema.GroupVersion{Group: apiext.SchemeGroupVersion.Group, Version: ver}) 192 if err != nil { 193 return err 194 } 195 versionedCRDs[i] = conv 196 } 197 198 for i, crd := range versionedCRDs { 199 removeDescriptionFromMetadata(crd.(*apiext.CustomResourceDefinition)) 200 var fileName string 201 if i == 0 { 202 fileName = fmt.Sprintf("%s_%s.yaml", crdRaw.Spec.Group, crdRaw.Spec.Names.Plural) 203 } else { 204 fileName = fmt.Sprintf("%s_%s.%s.yaml", crdRaw.Spec.Group, crdRaw.Spec.Names.Plural, crdVersions[i]) 205 } 206 if err := ctx.WriteYAML(fileName, headerText, []interface{}{crd}, yamlOpts...); err != nil { 207 return err 208 } 209 } 210 } 211 212 return nil 213 } 214 215 func removeDescriptionFromMetadata(crd *apiext.CustomResourceDefinition) { 216 for _, versionSpec := range crd.Spec.Versions { 217 if versionSpec.Schema != nil { 218 removeDescriptionFromMetadataProps(versionSpec.Schema.OpenAPIV3Schema) 219 } 220 } 221 } 222 223 func removeDescriptionFromMetadataProps(v *apiext.JSONSchemaProps) { 224 if m, ok := v.Properties["metadata"]; ok { 225 meta := &m 226 if meta.Description != "" { 227 meta.Description = "" 228 v.Properties["metadata"] = m 229 } 230 } 231 } 232 233 // FixTopLevelMetadata resets the schema for the top-level metadata field which is needed for CRD validation 234 func FixTopLevelMetadata(crd apiext.CustomResourceDefinition) { 235 for _, v := range crd.Spec.Versions { 236 if v.Schema != nil && v.Schema.OpenAPIV3Schema != nil && v.Schema.OpenAPIV3Schema.Properties != nil { 237 schemaProperties := v.Schema.OpenAPIV3Schema.Properties 238 if _, ok := schemaProperties["metadata"]; ok { 239 schemaProperties["metadata"] = apiext.JSONSchemaProps{Type: "object"} 240 } 241 } 242 } 243 } 244 245 // addAttribution adds attribution info to indicate controller-gen tool was used 246 // to generate this CRD definition along with the version info. 247 func addAttribution(crd *apiext.CustomResourceDefinition) { 248 if crd.ObjectMeta.Annotations == nil { 249 crd.ObjectMeta.Annotations = map[string]string{} 250 } 251 crd.ObjectMeta.Annotations["controller-gen.kubebuilder.io/version"] = version.Version() 252 } 253 254 // FindMetav1 locates the actual package representing metav1 amongst 255 // the imports of the roots. 256 func FindMetav1(roots []*loader.Package) *loader.Package { 257 for _, root := range roots { 258 pkg := root.Imports()["k8s.io/apimachinery/pkg/apis/meta/v1"] 259 if pkg != nil { 260 return pkg 261 } 262 } 263 return nil 264 } 265 266 // FindKubeKinds locates all types that contain TypeMeta and ObjectMeta 267 // (and thus may be a Kubernetes object), and returns the corresponding 268 // group-kinds. 269 func FindKubeKinds(parser *Parser, metav1Pkg *loader.Package) []schema.GroupKind { 270 // TODO(directxman12): technically, we should be finding metav1 per-package 271 kubeKinds := map[schema.GroupKind]struct{}{} 272 for typeIdent, info := range parser.Types { 273 hasObjectMeta := false 274 hasTypeMeta := false 275 276 pkg := typeIdent.Package 277 pkg.NeedTypesInfo() 278 typesInfo := pkg.TypesInfo 279 280 for _, field := range info.Fields { 281 if field.Name != "" { 282 // type and object meta are embedded, 283 // so they can't be this 284 continue 285 } 286 287 fieldType := typesInfo.TypeOf(field.RawField.Type) 288 namedField, isNamed := fieldType.(*types.Named) 289 if !isNamed { 290 // ObjectMeta and TypeMeta are named types 291 continue 292 } 293 if namedField.Obj().Pkg() == nil { 294 // Embedded non-builtin universe type (specifically, it's probably `error`), 295 // so it can't be ObjectMeta or TypeMeta 296 continue 297 } 298 fieldPkgPath := loader.NonVendorPath(namedField.Obj().Pkg().Path()) 299 fieldPkg := pkg.Imports()[fieldPkgPath] 300 301 // Compare the metav1 package by ID and not by the actual instance 302 // of the object. The objects in memory could be different due to 303 // loading from different root paths, even when they both refer to 304 // the same metav1 package. 305 if fieldPkg == nil || fieldPkg.ID != metav1Pkg.ID { 306 continue 307 } 308 309 switch namedField.Obj().Name() { 310 case "ObjectMeta": 311 hasObjectMeta = true 312 case "TypeMeta": 313 hasTypeMeta = true 314 } 315 } 316 317 if !hasObjectMeta || !hasTypeMeta { 318 continue 319 } 320 321 groupKind := schema.GroupKind{ 322 Group: parser.GroupVersions[pkg].Group, 323 Kind: typeIdent.Name, 324 } 325 kubeKinds[groupKind] = struct{}{} 326 } 327 328 groupKindList := make([]schema.GroupKind, 0, len(kubeKinds)) 329 for groupKind := range kubeKinds { 330 groupKindList = append(groupKindList, groupKind) 331 } 332 sort.Slice(groupKindList, func(i, j int) bool { 333 return groupKindList[i].String() < groupKindList[j].String() 334 }) 335 336 return groupKindList 337 } 338 339 // filterTypesForCRDs filters out all nodes that aren't used in CRD generation, 340 // like interfaces and struct fields without JSON tag. 341 func filterTypesForCRDs(node ast.Node) bool { 342 switch node := node.(type) { 343 case *ast.InterfaceType: 344 // skip interfaces, we never care about references in them 345 return false 346 case *ast.StructType: 347 return true 348 case *ast.Field: 349 _, hasTag := loader.ParseAstTag(node.Tag).Lookup("json") 350 // fields without JSON tags mean we have custom serialization, 351 // so only visit fields with tags. 352 return hasTag 353 default: 354 return true 355 } 356 }