2
0

gen.go 13 KB

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