gen.go 8.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280
  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. apiext "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
  19. "k8s.io/apimachinery/pkg/runtime/schema"
  20. crdmarkers "sigs.k8s.io/controller-tools/pkg/crd/markers"
  21. "sigs.k8s.io/controller-tools/pkg/genall"
  22. "sigs.k8s.io/controller-tools/pkg/loader"
  23. "sigs.k8s.io/controller-tools/pkg/markers"
  24. "sigs.k8s.io/controller-tools/pkg/version"
  25. )
  26. // The identifier for v1 CustomResourceDefinitions.
  27. const v1 = "v1"
  28. // The default CustomResourceDefinition version to generate.
  29. const defaultVersion = v1
  30. // +controllertools:marker:generateHelp
  31. // Generator generates CustomResourceDefinition objects.
  32. type Generator struct {
  33. // AllowDangerousTypes allows types which are usually omitted from CRD generation
  34. // because they are not recommended.
  35. //
  36. // Currently the following additional types are allowed when this is true:
  37. // float32
  38. // float64
  39. //
  40. // Left unspecified, the default is false
  41. AllowDangerousTypes *bool `marker:",optional"`
  42. // MaxDescLen specifies the maximum description length for fields in CRD's OpenAPI schema.
  43. //
  44. // 0 indicates drop the description for all fields completely.
  45. // n indicates limit the description to at most n characters and truncate the description to
  46. // closest sentence boundary if it exceeds n characters.
  47. MaxDescLen *int `marker:",optional"`
  48. // CRDVersions specifies the target API versions of the CRD type itself to
  49. // generate. Defaults to v1.
  50. //
  51. // Currently, the only supported value is v1.
  52. //
  53. // The first version listed will be assumed to be the "default" version and
  54. // will not get a version suffix in the output filename.
  55. //
  56. // You'll need to use "v1" to get support for features like defaulting,
  57. // along with an API server that supports it (Kubernetes 1.16+).
  58. CRDVersions []string `marker:"crdVersions,optional"`
  59. // GenerateEmbeddedObjectMeta specifies if any embedded ObjectMeta in the CRD should be generated
  60. GenerateEmbeddedObjectMeta *bool `marker:",optional"`
  61. }
  62. func (Generator) CheckFilter() loader.NodeFilter {
  63. return filterTypesForCRDs
  64. }
  65. func (Generator) RegisterMarkers(into *markers.Registry) error {
  66. return crdmarkers.Register(into)
  67. }
  68. func (g Generator) Generate(ctx *genall.GenerationContext) error {
  69. parser := &Parser{
  70. Collector: ctx.Collector,
  71. Checker: ctx.Checker,
  72. // Perform defaulting here to avoid ambiguity later
  73. AllowDangerousTypes: g.AllowDangerousTypes != nil && *g.AllowDangerousTypes == true,
  74. // Indicates the parser on whether to register the ObjectMeta type or not
  75. GenerateEmbeddedObjectMeta: g.GenerateEmbeddedObjectMeta != nil && *g.GenerateEmbeddedObjectMeta == true,
  76. }
  77. AddKnownTypes(parser)
  78. for _, root := range ctx.Roots {
  79. parser.NeedPackage(root)
  80. }
  81. metav1Pkg := FindMetav1(ctx.Roots)
  82. if metav1Pkg == nil {
  83. // no objects in the roots, since nothing imported metav1
  84. return nil
  85. }
  86. // TODO: allow selecting a specific object
  87. kubeKinds := FindKubeKinds(parser, metav1Pkg)
  88. if len(kubeKinds) == 0 {
  89. // no objects in the roots
  90. return nil
  91. }
  92. crdVersions := g.CRDVersions
  93. if len(crdVersions) == 0 {
  94. crdVersions = []string{defaultVersion}
  95. }
  96. for groupKind := range kubeKinds {
  97. parser.NeedCRDFor(groupKind, g.MaxDescLen)
  98. crdRaw := parser.CustomResourceDefinitions[groupKind]
  99. addAttribution(&crdRaw)
  100. // Prevent the top level metadata for the CRD to be generate regardless of the intention in the arguments
  101. FixTopLevelMetadata(crdRaw)
  102. versionedCRDs := make([]interface{}, len(crdVersions))
  103. for i, ver := range crdVersions {
  104. conv, err := AsVersion(crdRaw, schema.GroupVersion{Group: apiext.SchemeGroupVersion.Group, Version: ver})
  105. if err != nil {
  106. return err
  107. }
  108. versionedCRDs[i] = conv
  109. }
  110. for i, crd := range versionedCRDs {
  111. removeDescriptionFromMetadata(crd.(*apiext.CustomResourceDefinition))
  112. var fileName string
  113. if i == 0 {
  114. fileName = fmt.Sprintf("%s_%s.yaml", crdRaw.Spec.Group, crdRaw.Spec.Names.Plural)
  115. } else {
  116. fileName = fmt.Sprintf("%s_%s.%s.yaml", crdRaw.Spec.Group, crdRaw.Spec.Names.Plural, crdVersions[i])
  117. }
  118. if err := ctx.WriteYAML(fileName, crd); err != nil {
  119. return err
  120. }
  121. }
  122. }
  123. return nil
  124. }
  125. func removeDescriptionFromMetadata(crd *apiext.CustomResourceDefinition) {
  126. for _, versionSpec := range crd.Spec.Versions {
  127. if versionSpec.Schema != nil {
  128. removeDescriptionFromMetadataProps(versionSpec.Schema.OpenAPIV3Schema)
  129. }
  130. }
  131. }
  132. func removeDescriptionFromMetadataProps(v *apiext.JSONSchemaProps) {
  133. if m, ok := v.Properties["metadata"]; ok {
  134. meta := &m
  135. if meta.Description != "" {
  136. meta.Description = ""
  137. v.Properties["metadata"] = m
  138. }
  139. }
  140. }
  141. // FixTopLevelMetadata resets the schema for the top-level metadata field which is needed for CRD validation
  142. func FixTopLevelMetadata(crd apiext.CustomResourceDefinition) {
  143. for _, v := range crd.Spec.Versions {
  144. if v.Schema != nil && v.Schema.OpenAPIV3Schema != nil && v.Schema.OpenAPIV3Schema.Properties != nil {
  145. schemaProperties := v.Schema.OpenAPIV3Schema.Properties
  146. if _, ok := schemaProperties["metadata"]; ok {
  147. schemaProperties["metadata"] = apiext.JSONSchemaProps{Type: "object"}
  148. }
  149. }
  150. }
  151. }
  152. // addAttribution adds attribution info to indicate controller-gen tool was used
  153. // to generate this CRD definition along with the version info.
  154. func addAttribution(crd *apiext.CustomResourceDefinition) {
  155. if crd.ObjectMeta.Annotations == nil {
  156. crd.ObjectMeta.Annotations = map[string]string{}
  157. }
  158. crd.ObjectMeta.Annotations["controller-gen.kubebuilder.io/version"] = version.Version()
  159. }
  160. // FindMetav1 locates the actual package representing metav1 amongst
  161. // the imports of the roots.
  162. func FindMetav1(roots []*loader.Package) *loader.Package {
  163. for _, root := range roots {
  164. pkg := root.Imports()["k8s.io/apimachinery/pkg/apis/meta/v1"]
  165. if pkg != nil {
  166. return pkg
  167. }
  168. }
  169. return nil
  170. }
  171. // FindKubeKinds locates all types that contain TypeMeta and ObjectMeta
  172. // (and thus may be a Kubernetes object), and returns the corresponding
  173. // group-kinds.
  174. func FindKubeKinds(parser *Parser, metav1Pkg *loader.Package) map[schema.GroupKind]struct{} {
  175. // TODO(directxman12): technically, we should be finding metav1 per-package
  176. kubeKinds := map[schema.GroupKind]struct{}{}
  177. for typeIdent, info := range parser.Types {
  178. hasObjectMeta := false
  179. hasTypeMeta := false
  180. pkg := typeIdent.Package
  181. pkg.NeedTypesInfo()
  182. typesInfo := pkg.TypesInfo
  183. for _, field := range info.Fields {
  184. if field.Name != "" {
  185. // type and object meta are embedded,
  186. // so they can't be this
  187. continue
  188. }
  189. fieldType := typesInfo.TypeOf(field.RawField.Type)
  190. namedField, isNamed := fieldType.(*types.Named)
  191. if !isNamed {
  192. // ObjectMeta and TypeMeta are named types
  193. continue
  194. }
  195. if namedField.Obj().Pkg() == nil {
  196. // Embedded non-builtin universe type (specifically, it's probably `error`),
  197. // so it can't be ObjectMeta or TypeMeta
  198. continue
  199. }
  200. fieldPkgPath := loader.NonVendorPath(namedField.Obj().Pkg().Path())
  201. fieldPkg := pkg.Imports()[fieldPkgPath]
  202. if fieldPkg != metav1Pkg {
  203. continue
  204. }
  205. switch namedField.Obj().Name() {
  206. case "ObjectMeta":
  207. hasObjectMeta = true
  208. case "TypeMeta":
  209. hasTypeMeta = true
  210. }
  211. }
  212. if !hasObjectMeta || !hasTypeMeta {
  213. continue
  214. }
  215. groupKind := schema.GroupKind{
  216. Group: parser.GroupVersions[pkg].Group,
  217. Kind: typeIdent.Name,
  218. }
  219. kubeKinds[groupKind] = struct{}{}
  220. }
  221. return kubeKinds
  222. }
  223. // filterTypesForCRDs filters out all nodes that aren't used in CRD generation,
  224. // like interfaces and struct fields without JSON tag.
  225. func filterTypesForCRDs(node ast.Node) bool {
  226. switch node := node.(type) {
  227. case *ast.InterfaceType:
  228. // skip interfaces, we never care about references in them
  229. return false
  230. case *ast.StructType:
  231. return true
  232. case *ast.Field:
  233. _, hasTag := loader.ParseAstTag(node.Tag).Lookup("json")
  234. // fields without JSON tags mean we have custom serialization,
  235. // so only visit fields with tags.
  236. return hasTag
  237. default:
  238. return true
  239. }
  240. }