gen.go 10.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325
  1. /*
  2. Copyright 2018 The Kubernetes Authors.
  3. Licensed under the Apache License, Version 2.0 (the "License");
  4. you may not use this file except in compliance with the License.
  5. You may obtain a copy of the License at
  6. http://www.apache.org/licenses/LICENSE-2.0
  7. Unless required by applicable law or agreed to in writing, software
  8. distributed under the License is distributed on an "AS IS" BASIS,
  9. WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  10. See the License for the specific language governing permissions and
  11. limitations under the License.
  12. */
  13. package crd
  14. import (
  15. "fmt"
  16. "go/ast"
  17. "go/types"
  18. "sort"
  19. "strings"
  20. apiext "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
  21. "k8s.io/apimachinery/pkg/runtime/schema"
  22. crdmarkers "sigs.k8s.io/controller-tools/pkg/crd/markers"
  23. "sigs.k8s.io/controller-tools/pkg/genall"
  24. "sigs.k8s.io/controller-tools/pkg/loader"
  25. "sigs.k8s.io/controller-tools/pkg/markers"
  26. "sigs.k8s.io/controller-tools/pkg/version"
  27. )
  28. // The identifier for v1 CustomResourceDefinitions.
  29. const v1 = "v1"
  30. // The default CustomResourceDefinition version to generate.
  31. const defaultVersion = v1
  32. // +controllertools:marker:generateHelp
  33. // Generator generates CustomResourceDefinition objects.
  34. type Generator struct {
  35. // IgnoreUnexportedFields indicates that we should skip unexported fields.
  36. //
  37. // Left unspecified, the default is false.
  38. IgnoreUnexportedFields *bool `marker:",optional"`
  39. // AllowDangerousTypes allows types which are usually omitted from CRD generation
  40. // because they are not recommended.
  41. //
  42. // Currently the following additional types are allowed when this is true:
  43. // float32
  44. // float64
  45. //
  46. // Left unspecified, the default is false
  47. AllowDangerousTypes *bool `marker:",optional"`
  48. // MaxDescLen specifies the maximum description length for fields in CRD's OpenAPI schema.
  49. //
  50. // 0 indicates drop the description for all fields completely.
  51. // n indicates limit the description to at most n characters and truncate the description to
  52. // closest sentence boundary if it exceeds n characters.
  53. MaxDescLen *int `marker:",optional"`
  54. // CRDVersions specifies the target API versions of the CRD type itself to
  55. // generate. Defaults to v1.
  56. //
  57. // Currently, the only supported value is v1.
  58. //
  59. // The first version listed will be assumed to be the "default" version and
  60. // will not get a version suffix in the output filename.
  61. //
  62. // You'll need to use "v1" to get support for features like defaulting,
  63. // along with an API server that supports it (Kubernetes 1.16+).
  64. CRDVersions []string `marker:"crdVersions,optional"`
  65. // GenerateEmbeddedObjectMeta specifies if any embedded ObjectMeta in the CRD should be generated
  66. GenerateEmbeddedObjectMeta *bool `marker:",optional"`
  67. // HeaderFile specifies the header text (e.g. license) to prepend to generated files.
  68. HeaderFile string `marker:",optional"`
  69. // Year specifies the year to substitute for " YEAR" in the header file.
  70. Year string `marker:",optional"`
  71. }
  72. func (Generator) CheckFilter() loader.NodeFilter {
  73. return filterTypesForCRDs
  74. }
  75. func (Generator) RegisterMarkers(into *markers.Registry) error {
  76. return crdmarkers.Register(into)
  77. }
  78. // transformRemoveCRDStatus ensures we do not write the CRD status field.
  79. func transformRemoveCRDStatus(obj map[string]interface{}) error {
  80. delete(obj, "status")
  81. return nil
  82. }
  83. func (g Generator) Generate(ctx *genall.GenerationContext) error {
  84. parser := &Parser{
  85. Collector: ctx.Collector,
  86. Checker: ctx.Checker,
  87. // Perform defaulting here to avoid ambiguity later
  88. IgnoreUnexportedFields: g.IgnoreUnexportedFields != nil && *g.IgnoreUnexportedFields == true,
  89. AllowDangerousTypes: g.AllowDangerousTypes != nil && *g.AllowDangerousTypes == true,
  90. // Indicates the parser on whether to register the ObjectMeta type or not
  91. GenerateEmbeddedObjectMeta: g.GenerateEmbeddedObjectMeta != nil && *g.GenerateEmbeddedObjectMeta == true,
  92. }
  93. AddKnownTypes(parser)
  94. for _, root := range ctx.Roots {
  95. parser.NeedPackage(root)
  96. }
  97. metav1Pkg := FindMetav1(ctx.Roots)
  98. if metav1Pkg == nil {
  99. // no objects in the roots, since nothing imported metav1
  100. return nil
  101. }
  102. // TODO: allow selecting a specific object
  103. kubeKinds := FindKubeKinds(parser, metav1Pkg)
  104. if len(kubeKinds) == 0 {
  105. // no objects in the roots
  106. return nil
  107. }
  108. crdVersions := g.CRDVersions
  109. if len(crdVersions) == 0 {
  110. crdVersions = []string{defaultVersion}
  111. }
  112. var headerText string
  113. if g.HeaderFile != "" {
  114. headerBytes, err := ctx.ReadFile(g.HeaderFile)
  115. if err != nil {
  116. return err
  117. }
  118. headerText = string(headerBytes)
  119. }
  120. headerText = strings.ReplaceAll(headerText, " YEAR", " "+g.Year)
  121. for _, groupKind := range kubeKinds {
  122. parser.NeedCRDFor(groupKind, g.MaxDescLen)
  123. crdRaw := parser.CustomResourceDefinitions[groupKind]
  124. addAttribution(&crdRaw)
  125. // Prevent the top level metadata for the CRD to be generate regardless of the intention in the arguments
  126. FixTopLevelMetadata(crdRaw)
  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. for i, crd := range versionedCRDs {
  136. removeDescriptionFromMetadata(crd.(*apiext.CustomResourceDefinition))
  137. var fileName string
  138. if i == 0 {
  139. fileName = fmt.Sprintf("%s_%s.yaml", crdRaw.Spec.Group, crdRaw.Spec.Names.Plural)
  140. } else {
  141. fileName = fmt.Sprintf("%s_%s.%s.yaml", crdRaw.Spec.Group, crdRaw.Spec.Names.Plural, crdVersions[i])
  142. }
  143. if err := ctx.WriteYAML(fileName, headerText, []interface{}{crd}, genall.WithTransform(transformRemoveCRDStatus), genall.WithTransform(genall.TransformRemoveCreationTimestamp)); err != nil {
  144. return err
  145. }
  146. }
  147. }
  148. return nil
  149. }
  150. func removeDescriptionFromMetadata(crd *apiext.CustomResourceDefinition) {
  151. for _, versionSpec := range crd.Spec.Versions {
  152. if versionSpec.Schema != nil {
  153. removeDescriptionFromMetadataProps(versionSpec.Schema.OpenAPIV3Schema)
  154. }
  155. }
  156. }
  157. func removeDescriptionFromMetadataProps(v *apiext.JSONSchemaProps) {
  158. if m, ok := v.Properties["metadata"]; ok {
  159. meta := &m
  160. if meta.Description != "" {
  161. meta.Description = ""
  162. v.Properties["metadata"] = m
  163. }
  164. }
  165. }
  166. // FixTopLevelMetadata resets the schema for the top-level metadata field which is needed for CRD validation
  167. func FixTopLevelMetadata(crd apiext.CustomResourceDefinition) {
  168. for _, v := range crd.Spec.Versions {
  169. if v.Schema != nil && v.Schema.OpenAPIV3Schema != nil && v.Schema.OpenAPIV3Schema.Properties != nil {
  170. schemaProperties := v.Schema.OpenAPIV3Schema.Properties
  171. if _, ok := schemaProperties["metadata"]; ok {
  172. schemaProperties["metadata"] = apiext.JSONSchemaProps{Type: "object"}
  173. }
  174. }
  175. }
  176. }
  177. // addAttribution adds attribution info to indicate controller-gen tool was used
  178. // to generate this CRD definition along with the version info.
  179. func addAttribution(crd *apiext.CustomResourceDefinition) {
  180. if crd.ObjectMeta.Annotations == nil {
  181. crd.ObjectMeta.Annotations = map[string]string{}
  182. }
  183. crd.ObjectMeta.Annotations["controller-gen.kubebuilder.io/version"] = version.Version()
  184. }
  185. // FindMetav1 locates the actual package representing metav1 amongst
  186. // the imports of the roots.
  187. func FindMetav1(roots []*loader.Package) *loader.Package {
  188. for _, root := range roots {
  189. pkg := root.Imports()["k8s.io/apimachinery/pkg/apis/meta/v1"]
  190. if pkg != nil {
  191. return pkg
  192. }
  193. }
  194. return nil
  195. }
  196. // FindKubeKinds locates all types that contain TypeMeta and ObjectMeta
  197. // (and thus may be a Kubernetes object), and returns the corresponding
  198. // group-kinds.
  199. func FindKubeKinds(parser *Parser, metav1Pkg *loader.Package) []schema.GroupKind {
  200. // TODO(directxman12): technically, we should be finding metav1 per-package
  201. kubeKinds := map[schema.GroupKind]struct{}{}
  202. for typeIdent, info := range parser.Types {
  203. hasObjectMeta := false
  204. hasTypeMeta := false
  205. pkg := typeIdent.Package
  206. pkg.NeedTypesInfo()
  207. typesInfo := pkg.TypesInfo
  208. for _, field := range info.Fields {
  209. if field.Name != "" {
  210. // type and object meta are embedded,
  211. // so they can't be this
  212. continue
  213. }
  214. fieldType := typesInfo.TypeOf(field.RawField.Type)
  215. namedField, isNamed := fieldType.(*types.Named)
  216. if !isNamed {
  217. // ObjectMeta and TypeMeta are named types
  218. continue
  219. }
  220. if namedField.Obj().Pkg() == nil {
  221. // Embedded non-builtin universe type (specifically, it's probably `error`),
  222. // so it can't be ObjectMeta or TypeMeta
  223. continue
  224. }
  225. fieldPkgPath := loader.NonVendorPath(namedField.Obj().Pkg().Path())
  226. fieldPkg := pkg.Imports()[fieldPkgPath]
  227. // Compare the metav1 package by ID and not by the actual instance
  228. // of the object. The objects in memory could be different due to
  229. // loading from different root paths, even when they both refer to
  230. // the same metav1 package.
  231. if fieldPkg == nil || fieldPkg.ID != metav1Pkg.ID {
  232. continue
  233. }
  234. switch namedField.Obj().Name() {
  235. case "ObjectMeta":
  236. hasObjectMeta = true
  237. case "TypeMeta":
  238. hasTypeMeta = true
  239. }
  240. }
  241. if !hasObjectMeta || !hasTypeMeta {
  242. continue
  243. }
  244. groupKind := schema.GroupKind{
  245. Group: parser.GroupVersions[pkg].Group,
  246. Kind: typeIdent.Name,
  247. }
  248. kubeKinds[groupKind] = struct{}{}
  249. }
  250. groupKindList := make([]schema.GroupKind, 0, len(kubeKinds))
  251. for groupKind := range kubeKinds {
  252. groupKindList = append(groupKindList, groupKind)
  253. }
  254. sort.Slice(groupKindList, func(i, j int) bool {
  255. return groupKindList[i].String() < groupKindList[j].String()
  256. })
  257. return groupKindList
  258. }
  259. // filterTypesForCRDs filters out all nodes that aren't used in CRD generation,
  260. // like interfaces and struct fields without JSON tag.
  261. func filterTypesForCRDs(node ast.Node) bool {
  262. switch node := node.(type) {
  263. case *ast.InterfaceType:
  264. // skip interfaces, we never care about references in them
  265. return false
  266. case *ast.StructType:
  267. return true
  268. case *ast.Field:
  269. _, hasTag := loader.ParseAstTag(node.Tag).Lookup("json")
  270. // fields without JSON tags mean we have custom serialization,
  271. // so only visit fields with tags.
  272. return hasTag
  273. default:
  274. return true
  275. }
  276. }