flatten.go 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445
  1. /*
  2. Copyright 2019 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. "reflect"
  17. "sort"
  18. "strings"
  19. "sync"
  20. apiext "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
  21. "sigs.k8s.io/controller-tools/pkg/loader"
  22. )
  23. // ErrorRecorder knows how to record errors. It wraps the part of
  24. // pkg/loader.Package that we need to record errors in places were it might not
  25. // make sense to have a loader.Package
  26. type ErrorRecorder interface {
  27. // AddError records that the given error occurred.
  28. // See the documentation on loader.Package.AddError for more information.
  29. AddError(error)
  30. }
  31. // isOrNil checks if val is nil if val is of a nillable type, otherwise,
  32. // it compares val to valInt (which should probably be the zero value).
  33. func isOrNil(val reflect.Value, valInt interface{}, zeroInt interface{}) bool {
  34. switch valKind := val.Kind(); valKind {
  35. case reflect.Chan, reflect.Func, reflect.Interface, reflect.Map, reflect.Ptr, reflect.Slice:
  36. return val.IsNil()
  37. default:
  38. return valInt == zeroInt
  39. }
  40. }
  41. // flattenAllOfInto copies properties from src to dst, then copies the properties
  42. // of each item in src's allOf to dst's properties as well.
  43. func flattenAllOfInto(dst *apiext.JSONSchemaProps, src apiext.JSONSchemaProps, errRec ErrorRecorder) {
  44. if len(src.AllOf) > 0 {
  45. for _, embedded := range src.AllOf {
  46. flattenAllOfInto(dst, embedded, errRec)
  47. }
  48. }
  49. dstVal := reflect.Indirect(reflect.ValueOf(dst))
  50. srcVal := reflect.ValueOf(src)
  51. typ := dstVal.Type()
  52. srcRemainder := apiext.JSONSchemaProps{}
  53. srcRemVal := reflect.Indirect(reflect.ValueOf(&srcRemainder))
  54. dstRemainder := apiext.JSONSchemaProps{}
  55. dstRemVal := reflect.Indirect(reflect.ValueOf(&dstRemainder))
  56. hoisted := false
  57. for i := 0; i < srcVal.NumField(); i++ {
  58. fieldName := typ.Field(i).Name
  59. switch fieldName {
  60. case "AllOf":
  61. // don't merge because we deal with it above
  62. continue
  63. case "Title", "Description", "Example", "ExternalDocs":
  64. // don't merge because we pre-merge to properly preserve field docs
  65. continue
  66. }
  67. srcField := srcVal.Field(i)
  68. fldTyp := srcField.Type()
  69. zeroVal := reflect.Zero(fldTyp)
  70. zeroInt := zeroVal.Interface()
  71. srcInt := srcField.Interface()
  72. if isOrNil(srcField, srcInt, zeroInt) {
  73. // nothing to copy from src, continue
  74. continue
  75. }
  76. dstField := dstVal.Field(i)
  77. dstInt := dstField.Interface()
  78. if isOrNil(dstField, dstInt, zeroInt) {
  79. // dst is empty, continue
  80. dstField.Set(srcField)
  81. continue
  82. }
  83. if fldTyp.Comparable() && srcInt == dstInt {
  84. // same value, continue
  85. continue
  86. }
  87. // resolve conflict
  88. switch fieldName {
  89. case "Properties":
  90. // merge if possible, use all of otherwise
  91. srcMap := srcInt.(map[string]apiext.JSONSchemaProps)
  92. dstMap := dstInt.(map[string]apiext.JSONSchemaProps)
  93. for k, v := range srcMap {
  94. dstProp, exists := dstMap[k]
  95. if !exists {
  96. dstMap[k] = v
  97. continue
  98. }
  99. flattenAllOfInto(&dstProp, v, errRec)
  100. dstMap[k] = dstProp
  101. }
  102. case "Required":
  103. // merge
  104. dstField.Set(reflect.AppendSlice(dstField, srcField))
  105. case "Type":
  106. if srcInt != dstInt {
  107. // TODO(directxman12): figure out how to attach this back to a useful point in the Go source or in the schema
  108. errRec.AddError(fmt.Errorf("conflicting types in allOf branches in schema: %s vs %s", dstInt, srcInt))
  109. }
  110. // keep the destination value, for now
  111. // TODO(directxman12): Default -- use field?
  112. // TODO(directxman12):
  113. // - Dependencies: if field x is present, then either schema validates or all props are present
  114. // - AdditionalItems: like AdditionalProperties
  115. // - Definitions: common named validation sets that can be references (merge, bail if duplicate)
  116. case "AdditionalProperties":
  117. // as of the time of writing, `allows: false` is not allowed, so we don't have to handle it
  118. srcProps := srcInt.(*apiext.JSONSchemaPropsOrBool)
  119. if srcProps.Schema == nil {
  120. // nothing to merge
  121. continue
  122. }
  123. dstProps := dstInt.(*apiext.JSONSchemaPropsOrBool)
  124. if dstProps.Schema == nil {
  125. dstProps.Schema = &apiext.JSONSchemaProps{}
  126. }
  127. flattenAllOfInto(dstProps.Schema, *srcProps.Schema, errRec)
  128. case "XPreserveUnknownFields":
  129. dstField.Set(srcField)
  130. case "XMapType":
  131. dstField.Set(srcField)
  132. // NB(directxman12): no need to explicitly handle nullable -- false is considered to be the zero value
  133. // TODO(directxman12): src isn't necessarily the field value -- it's just the most recent allOf entry
  134. default:
  135. // hoist into allOf...
  136. hoisted = true
  137. srcRemVal.Field(i).Set(srcField)
  138. dstRemVal.Field(i).Set(dstField)
  139. // ...and clear the original
  140. dstField.Set(zeroVal)
  141. }
  142. }
  143. if hoisted {
  144. dst.AllOf = append(dst.AllOf, dstRemainder, srcRemainder)
  145. }
  146. // dedup required
  147. if len(dst.Required) > 0 {
  148. reqUniq := make(map[string]struct{})
  149. for _, req := range dst.Required {
  150. reqUniq[req] = struct{}{}
  151. }
  152. dst.Required = make([]string, 0, len(reqUniq))
  153. for req := range reqUniq {
  154. dst.Required = append(dst.Required, req)
  155. }
  156. // be deterministic
  157. sort.Strings(dst.Required)
  158. }
  159. }
  160. // allOfVisitor recursively visits allOf fields in the schema,
  161. // merging nested allOf properties into the root schema.
  162. type allOfVisitor struct {
  163. // errRec is used to record errors while flattening (like two conflicting
  164. // field values used in an allOf)
  165. errRec ErrorRecorder
  166. }
  167. func (v *allOfVisitor) Visit(schema *apiext.JSONSchemaProps) SchemaVisitor {
  168. if schema == nil {
  169. return v
  170. }
  171. // clear this now so that we can safely preserve edits made my flattenAllOfInto
  172. origAllOf := schema.AllOf
  173. schema.AllOf = nil
  174. for _, embedded := range origAllOf {
  175. flattenAllOfInto(schema, embedded, v.errRec)
  176. }
  177. return v
  178. }
  179. // NB(directxman12): FlattenEmbedded is separate from Flattener because
  180. // some tooling wants to flatten out embedded fields, but only actually
  181. // flatten a few specific types first.
  182. // FlattenEmbedded flattens embedded fields (represented via AllOf) which have
  183. // already had their references resolved into simple properties in the containing
  184. // schema.
  185. func FlattenEmbedded(schema *apiext.JSONSchemaProps, errRec ErrorRecorder) *apiext.JSONSchemaProps {
  186. outSchema := schema.DeepCopy()
  187. EditSchema(outSchema, &allOfVisitor{errRec: errRec})
  188. return outSchema
  189. }
  190. // Flattener knows how to take a root type, and flatten all references in it
  191. // into a single, flat type. Flattened types are cached, so it's relatively
  192. // cheap to make repeated calls with the same type.
  193. type Flattener struct {
  194. // Parser is used to lookup package and type details, and parse in new packages.
  195. Parser *Parser
  196. LookupReference func(ref string, contextPkg *loader.Package) (TypeIdent, error)
  197. // flattenedTypes hold the flattened version of each seen type for later reuse.
  198. flattenedTypes map[TypeIdent]apiext.JSONSchemaProps
  199. initOnce sync.Once
  200. }
  201. func (f *Flattener) init() {
  202. f.initOnce.Do(func() {
  203. f.flattenedTypes = make(map[TypeIdent]apiext.JSONSchemaProps)
  204. if f.LookupReference == nil {
  205. f.LookupReference = identFromRef
  206. }
  207. })
  208. }
  209. // cacheType saves the flattened version of the given type for later reuse
  210. func (f *Flattener) cacheType(typ TypeIdent, schema apiext.JSONSchemaProps) {
  211. f.init()
  212. f.flattenedTypes[typ] = schema
  213. }
  214. // loadUnflattenedSchema fetches a fresh, unflattened schema from the parser.
  215. func (f *Flattener) loadUnflattenedSchema(typ TypeIdent) (*apiext.JSONSchemaProps, error) {
  216. f.Parser.NeedSchemaFor(typ)
  217. baseSchema, found := f.Parser.Schemata[typ]
  218. if !found {
  219. return nil, fmt.Errorf("unable to locate schema for type %s", typ)
  220. }
  221. return &baseSchema, nil
  222. }
  223. // FlattenType flattens the given pre-loaded type, removing any references from it.
  224. // It deep-copies the schema first, so it won't affect the parser's version of the schema.
  225. func (f *Flattener) FlattenType(typ TypeIdent) *apiext.JSONSchemaProps {
  226. f.init()
  227. if cachedSchema, isCached := f.flattenedTypes[typ]; isCached {
  228. return &cachedSchema
  229. }
  230. baseSchema, err := f.loadUnflattenedSchema(typ)
  231. if err != nil {
  232. typ.Package.AddError(err)
  233. return nil
  234. }
  235. resSchema := f.FlattenSchema(*baseSchema, typ.Package)
  236. f.cacheType(typ, *resSchema)
  237. return resSchema
  238. }
  239. // FlattenSchema flattens the given schema, removing any references.
  240. // It deep-copies the schema first, so the input schema won't be affected.
  241. func (f *Flattener) FlattenSchema(baseSchema apiext.JSONSchemaProps, currentPackage *loader.Package) *apiext.JSONSchemaProps {
  242. resSchema := baseSchema.DeepCopy()
  243. EditSchema(resSchema, &flattenVisitor{
  244. Flattener: f,
  245. currentPackage: currentPackage,
  246. })
  247. return resSchema
  248. }
  249. // RefParts splits a reference produced by the schema generator into its component
  250. // type name and package name (if it's a cross-package reference). Note that
  251. // referenced packages *must* be looked up relative to the current package.
  252. func RefParts(ref string) (typ string, pkgName string, err error) {
  253. if !strings.HasPrefix(ref, defPrefix) {
  254. return "", "", fmt.Errorf("non-standard reference link %q", ref)
  255. }
  256. ref = ref[len(defPrefix):]
  257. // decode the json pointer encodings
  258. ref = strings.Replace(ref, "~1", "/", -1)
  259. ref = strings.Replace(ref, "~0", "~", -1)
  260. nameParts := strings.SplitN(ref, "~", 2)
  261. if len(nameParts) == 1 {
  262. // local reference
  263. return nameParts[0], "", nil
  264. }
  265. // cross-package reference
  266. return nameParts[1], nameParts[0], nil
  267. }
  268. // identFromRef converts the given schema ref from the given package back
  269. // into the TypeIdent that it represents.
  270. func identFromRef(ref string, contextPkg *loader.Package) (TypeIdent, error) {
  271. typ, pkgName, err := RefParts(ref)
  272. if err != nil {
  273. return TypeIdent{}, err
  274. }
  275. if pkgName == "" {
  276. // a local reference
  277. return TypeIdent{
  278. Name: typ,
  279. Package: contextPkg,
  280. }, nil
  281. }
  282. // an external reference
  283. return TypeIdent{
  284. Name: typ,
  285. Package: contextPkg.Imports()[pkgName],
  286. }, nil
  287. }
  288. // preserveFields copies documentation fields from src into dst, preserving
  289. // field-level documentation when flattening, and preserving field-level validation
  290. // as allOf entries.
  291. func preserveFields(dst *apiext.JSONSchemaProps, src apiext.JSONSchemaProps) {
  292. srcDesc := src.Description
  293. srcTitle := src.Title
  294. srcExDoc := src.ExternalDocs
  295. srcEx := src.Example
  296. src.Description, src.Title, src.ExternalDocs, src.Example = "", "", nil, nil
  297. src.Ref = nil
  298. *dst = apiext.JSONSchemaProps{
  299. AllOf: []apiext.JSONSchemaProps{*dst, src},
  300. // keep these, in case the source field doesn't specify anything useful
  301. Description: dst.Description,
  302. Title: dst.Title,
  303. ExternalDocs: dst.ExternalDocs,
  304. Example: dst.Example,
  305. }
  306. if srcDesc != "" {
  307. dst.Description = srcDesc
  308. }
  309. if srcTitle != "" {
  310. dst.Title = srcTitle
  311. }
  312. if srcExDoc != nil {
  313. dst.ExternalDocs = srcExDoc
  314. }
  315. if srcEx != nil {
  316. dst.Example = srcEx
  317. }
  318. }
  319. // flattenVisitor visits each node in the schema, recursively flattening references.
  320. type flattenVisitor struct {
  321. *Flattener
  322. currentPackage *loader.Package
  323. currentType *TypeIdent
  324. currentSchema *apiext.JSONSchemaProps
  325. originalField apiext.JSONSchemaProps
  326. }
  327. func (f *flattenVisitor) Visit(baseSchema *apiext.JSONSchemaProps) SchemaVisitor {
  328. if baseSchema == nil {
  329. // end-of-node marker, cache the results
  330. if f.currentType != nil {
  331. f.cacheType(*f.currentType, *f.currentSchema)
  332. // preserve field information *after* caching so that we don't
  333. // accidentally cache field-level information onto the schema for
  334. // the type in general.
  335. preserveFields(f.currentSchema, f.originalField)
  336. }
  337. return f
  338. }
  339. // if we get a type that's just a ref, resolve it
  340. if baseSchema.Ref != nil && len(*baseSchema.Ref) > 0 {
  341. // resolve this ref
  342. refIdent, err := f.LookupReference(*baseSchema.Ref, f.currentPackage)
  343. if err != nil {
  344. f.currentPackage.AddError(err)
  345. return nil
  346. }
  347. // load and potentially flatten the schema
  348. // check the cache first...
  349. if refSchemaCached, isCached := f.flattenedTypes[refIdent]; isCached {
  350. // shallow copy is fine, it's just to avoid overwriting the doc fields
  351. preserveFields(&refSchemaCached, *baseSchema)
  352. *baseSchema = refSchemaCached
  353. return nil // don't recurse, we're done
  354. }
  355. // ...otherwise, we need to flatten
  356. refSchema, err := f.loadUnflattenedSchema(refIdent)
  357. if err != nil {
  358. f.currentPackage.AddError(err)
  359. return nil
  360. }
  361. refSchema = refSchema.DeepCopy()
  362. // keep field around to preserve field-level validation, docs, etc
  363. origField := *baseSchema
  364. *baseSchema = *refSchema
  365. // avoid loops (which shouldn't exist, but just in case)
  366. // by marking a nil cached pointer before we start recursing
  367. f.cacheType(refIdent, apiext.JSONSchemaProps{})
  368. return &flattenVisitor{
  369. Flattener: f.Flattener,
  370. currentPackage: refIdent.Package,
  371. currentType: &refIdent,
  372. currentSchema: baseSchema,
  373. originalField: origField,
  374. }
  375. }
  376. // otherwise, continue recursing...
  377. if f.currentType != nil {
  378. // ...but don't accidentally end this node early (for caching purposes)
  379. return &flattenVisitor{
  380. Flattener: f.Flattener,
  381. currentPackage: f.currentPackage,
  382. }
  383. }
  384. return f
  385. }