github.com/TheSpiritXIII/controller-tools@v0.14.1/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 "github.com/TheSpiritXIII/controller-tools/pkg/crd/markers" 30 "github.com/TheSpiritXIII/controller-tools/pkg/genall" 31 "github.com/TheSpiritXIII/controller-tools/pkg/loader" 32 "github.com/TheSpiritXIII/controller-tools/pkg/markers" 33 "github.com/TheSpiritXIII/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 90 func (Generator) CheckFilter() loader.NodeFilter { 91 return filterTypesForCRDs 92 } 93 func (Generator) RegisterMarkers(into *markers.Registry) error { 94 return crdmarkers.Register(into) 95 } 96 97 // transformRemoveCRDStatus ensures we do not write the CRD status field. 98 func transformRemoveCRDStatus(obj map[string]interface{}) error { 99 delete(obj, "status") 100 return nil 101 } 102 103 func (g Generator) Generate(ctx *genall.GenerationContext) error { 104 parser := &Parser{ 105 Collector: ctx.Collector, 106 Checker: ctx.Checker, 107 // Perform defaulting here to avoid ambiguity later 108 IgnoreUnexportedFields: g.IgnoreUnexportedFields != nil && *g.IgnoreUnexportedFields == true, 109 AllowDangerousTypes: g.AllowDangerousTypes != nil && *g.AllowDangerousTypes == true, 110 // Indicates the parser on whether to register the ObjectMeta type or not 111 GenerateEmbeddedObjectMeta: g.GenerateEmbeddedObjectMeta != nil && *g.GenerateEmbeddedObjectMeta == true, 112 } 113 114 AddKnownTypes(parser) 115 for _, root := range ctx.Roots { 116 parser.NeedPackage(root) 117 } 118 119 metav1Pkg := FindMetav1(ctx.Roots) 120 if metav1Pkg == nil { 121 // no objects in the roots, since nothing imported metav1 122 return nil 123 } 124 125 // TODO: allow selecting a specific object 126 kubeKinds := FindKubeKinds(parser, metav1Pkg) 127 if len(kubeKinds) == 0 { 128 // no objects in the roots 129 return nil 130 } 131 132 crdVersions := g.CRDVersions 133 134 if len(crdVersions) == 0 { 135 crdVersions = []string{defaultVersion} 136 } 137 138 var headerText string 139 140 if g.HeaderFile != "" { 141 headerBytes, err := ctx.ReadFile(g.HeaderFile) 142 if err != nil { 143 return err 144 } 145 headerText = string(headerBytes) 146 } 147 headerText = strings.ReplaceAll(headerText, " YEAR", " "+g.Year) 148 149 for _, groupKind := range kubeKinds { 150 parser.NeedCRDFor(groupKind, g.MaxDescLen) 151 crdRaw := parser.CustomResourceDefinitions[groupKind] 152 addAttribution(&crdRaw) 153 154 // Prevent the top level metadata for the CRD to be generate regardless of the intention in the arguments 155 FixTopLevelMetadata(crdRaw) 156 157 versionedCRDs := make([]interface{}, len(crdVersions)) 158 for i, ver := range crdVersions { 159 conv, err := AsVersion(crdRaw, schema.GroupVersion{Group: apiext.SchemeGroupVersion.Group, Version: ver}) 160 if err != nil { 161 return err 162 } 163 versionedCRDs[i] = conv 164 } 165 166 for i, crd := range versionedCRDs { 167 removeDescriptionFromMetadata(crd.(*apiext.CustomResourceDefinition)) 168 var fileName string 169 if i == 0 { 170 fileName = fmt.Sprintf("%s_%s.yaml", crdRaw.Spec.Group, crdRaw.Spec.Names.Plural) 171 } else { 172 fileName = fmt.Sprintf("%s_%s.%s.yaml", crdRaw.Spec.Group, crdRaw.Spec.Names.Plural, crdVersions[i]) 173 } 174 if err := ctx.WriteYAML(fileName, headerText, []interface{}{crd}, genall.WithTransform(transformRemoveCRDStatus), genall.WithTransform(genall.TransformRemoveCreationTimestamp)); err != nil { 175 return err 176 } 177 } 178 } 179 180 return nil 181 } 182 183 func removeDescriptionFromMetadata(crd *apiext.CustomResourceDefinition) { 184 for _, versionSpec := range crd.Spec.Versions { 185 if versionSpec.Schema != nil { 186 removeDescriptionFromMetadataProps(versionSpec.Schema.OpenAPIV3Schema) 187 } 188 } 189 } 190 191 func removeDescriptionFromMetadataProps(v *apiext.JSONSchemaProps) { 192 if m, ok := v.Properties["metadata"]; ok { 193 meta := &m 194 if meta.Description != "" { 195 meta.Description = "" 196 v.Properties["metadata"] = m 197 198 } 199 } 200 } 201 202 // FixTopLevelMetadata resets the schema for the top-level metadata field which is needed for CRD validation 203 func FixTopLevelMetadata(crd apiext.CustomResourceDefinition) { 204 for _, v := range crd.Spec.Versions { 205 if v.Schema != nil && v.Schema.OpenAPIV3Schema != nil && v.Schema.OpenAPIV3Schema.Properties != nil { 206 schemaProperties := v.Schema.OpenAPIV3Schema.Properties 207 if _, ok := schemaProperties["metadata"]; ok { 208 schemaProperties["metadata"] = apiext.JSONSchemaProps{Type: "object"} 209 } 210 } 211 } 212 } 213 214 // addAttribution adds attribution info to indicate controller-gen tool was used 215 // to generate this CRD definition along with the version info. 216 func addAttribution(crd *apiext.CustomResourceDefinition) { 217 if crd.ObjectMeta.Annotations == nil { 218 crd.ObjectMeta.Annotations = map[string]string{} 219 } 220 crd.ObjectMeta.Annotations["controller-gen.kubebuilder.io/version"] = version.Version() 221 } 222 223 // FindMetav1 locates the actual package representing metav1 amongst 224 // the imports of the roots. 225 func FindMetav1(roots []*loader.Package) *loader.Package { 226 for _, root := range roots { 227 pkg := root.Imports()["k8s.io/apimachinery/pkg/apis/meta/v1"] 228 if pkg != nil { 229 return pkg 230 } 231 } 232 return nil 233 } 234 235 // FindKubeKinds locates all types that contain TypeMeta and ObjectMeta 236 // (and thus may be a Kubernetes object), and returns the corresponding 237 // group-kinds. 238 func FindKubeKinds(parser *Parser, metav1Pkg *loader.Package) []schema.GroupKind { 239 // TODO(directxman12): technically, we should be finding metav1 per-package 240 kubeKinds := map[schema.GroupKind]struct{}{} 241 for typeIdent, info := range parser.Types { 242 hasObjectMeta := false 243 hasTypeMeta := false 244 245 pkg := typeIdent.Package 246 pkg.NeedTypesInfo() 247 typesInfo := pkg.TypesInfo 248 249 for _, field := range info.Fields { 250 if field.Name != "" { 251 // type and object meta are embedded, 252 // so they can't be this 253 continue 254 } 255 256 fieldType := typesInfo.TypeOf(field.RawField.Type) 257 namedField, isNamed := fieldType.(*types.Named) 258 if !isNamed { 259 // ObjectMeta and TypeMeta are named types 260 continue 261 } 262 if namedField.Obj().Pkg() == nil { 263 // Embedded non-builtin universe type (specifically, it's probably `error`), 264 // so it can't be ObjectMeta or TypeMeta 265 continue 266 } 267 fieldPkgPath := loader.NonVendorPath(namedField.Obj().Pkg().Path()) 268 fieldPkg := pkg.Imports()[fieldPkgPath] 269 270 // Compare the metav1 package by ID and not by the actual instance 271 // of the object. The objects in memory could be different due to 272 // loading from different root paths, even when they both refer to 273 // the same metav1 package. 274 if fieldPkg == nil || fieldPkg.ID != metav1Pkg.ID { 275 continue 276 } 277 278 switch namedField.Obj().Name() { 279 case "ObjectMeta": 280 hasObjectMeta = true 281 case "TypeMeta": 282 hasTypeMeta = true 283 } 284 } 285 286 if !hasObjectMeta || !hasTypeMeta { 287 continue 288 } 289 290 groupKind := schema.GroupKind{ 291 Group: parser.GroupVersions[pkg].Group, 292 Kind: typeIdent.Name, 293 } 294 kubeKinds[groupKind] = struct{}{} 295 } 296 297 groupKindList := make([]schema.GroupKind, 0, len(kubeKinds)) 298 for groupKind := range kubeKinds { 299 groupKindList = append(groupKindList, groupKind) 300 } 301 sort.Slice(groupKindList, func(i, j int) bool { 302 return groupKindList[i].String() < groupKindList[j].String() 303 }) 304 305 return groupKindList 306 } 307 308 // filterTypesForCRDs filters out all nodes that aren't used in CRD generation, 309 // like interfaces and struct fields without JSON tag. 310 func filterTypesForCRDs(node ast.Node) bool { 311 switch node := node.(type) { 312 case *ast.InterfaceType: 313 // skip interfaces, we never care about references in them 314 return false 315 case *ast.StructType: 316 return true 317 case *ast.Field: 318 _, hasTag := loader.ParseAstTag(node.Tag).Lookup("json") 319 // fields without JSON tags mean we have custom serialization, 320 // so only visit fields with tags. 321 return hasTag 322 default: 323 return true 324 } 325 }