flatten.go 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441
  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. // NB(directxman12): no need to explicitly handle nullable -- false is considered to be the zero value
  129. // TODO(directxman12): src isn't necessarily the field value -- it's just the most recent allOf entry
  130. default:
  131. // hoist into allOf...
  132. hoisted = true
  133. srcRemVal.Field(i).Set(srcField)
  134. dstRemVal.Field(i).Set(dstField)
  135. // ...and clear the original
  136. dstField.Set(zeroVal)
  137. }
  138. }
  139. if hoisted {
  140. dst.AllOf = append(dst.AllOf, dstRemainder, srcRemainder)
  141. }
  142. // dedup required
  143. if len(dst.Required) > 0 {
  144. reqUniq := make(map[string]struct{})
  145. for _, req := range dst.Required {
  146. reqUniq[req] = struct{}{}
  147. }
  148. dst.Required = make([]string, 0, len(reqUniq))
  149. for req := range reqUniq {
  150. dst.Required = append(dst.Required, req)
  151. }
  152. // be deterministic
  153. sort.Strings(dst.Required)
  154. }
  155. }
  156. // allOfVisitor recursively visits allOf fields in the schema,
  157. // merging nested allOf properties into the root schema.
  158. type allOfVisitor struct {
  159. // errRec is used to record errors while flattening (like two conflicting
  160. // field values used in an allOf)
  161. errRec ErrorRecorder
  162. }
  163. func (v *allOfVisitor) Visit(schema *apiext.JSONSchemaProps) SchemaVisitor {
  164. if schema == nil {
  165. return v
  166. }
  167. // clear this now so that we can safely preserve edits made my flattenAllOfInto
  168. origAllOf := schema.AllOf
  169. schema.AllOf = nil
  170. for _, embedded := range origAllOf {
  171. flattenAllOfInto(schema, embedded, v.errRec)
  172. }
  173. return v
  174. }
  175. // NB(directxman12): FlattenEmbedded is separate from Flattener because
  176. // some tooling wants to flatten out embedded fields, but only actually
  177. // flatten a few specific types first.
  178. // FlattenEmbedded flattens embedded fields (represented via AllOf) which have
  179. // already had their references resolved into simple properties in the containing
  180. // schema.
  181. func FlattenEmbedded(schema *apiext.JSONSchemaProps, errRec ErrorRecorder) *apiext.JSONSchemaProps {
  182. outSchema := schema.DeepCopy()
  183. EditSchema(outSchema, &allOfVisitor{errRec: errRec})
  184. return outSchema
  185. }
  186. // Flattener knows how to take a root type, and flatten all references in it
  187. // into a single, flat type. Flattened types are cached, so it's relatively
  188. // cheap to make repeated calls with the same type.
  189. type Flattener struct {
  190. // Parser is used to lookup package and type details, and parse in new packages.
  191. Parser *Parser
  192. LookupReference func(ref string, contextPkg *loader.Package) (TypeIdent, error)
  193. // flattenedTypes hold the flattened version of each seen type for later reuse.
  194. flattenedTypes map[TypeIdent]apiext.JSONSchemaProps
  195. initOnce sync.Once
  196. }
  197. func (f *Flattener) init() {
  198. f.initOnce.Do(func() {
  199. f.flattenedTypes = make(map[TypeIdent]apiext.JSONSchemaProps)
  200. if f.LookupReference == nil {
  201. f.LookupReference = identFromRef
  202. }
  203. })
  204. }
  205. // cacheType saves the flattened version of the given type for later reuse
  206. func (f *Flattener) cacheType(typ TypeIdent, schema apiext.JSONSchemaProps) {
  207. f.init()
  208. f.flattenedTypes[typ] = schema
  209. }
  210. // loadUnflattenedSchema fetches a fresh, unflattened schema from the parser.
  211. func (f *Flattener) loadUnflattenedSchema(typ TypeIdent) (*apiext.JSONSchemaProps, error) {
  212. f.Parser.NeedSchemaFor(typ)
  213. baseSchema, found := f.Parser.Schemata[typ]
  214. if !found {
  215. return nil, fmt.Errorf("unable to locate schema for type %s", typ)
  216. }
  217. return &baseSchema, nil
  218. }
  219. // FlattenType flattens the given pre-loaded type, removing any references from it.
  220. // It deep-copies the schema first, so it won't affect the parser's version of the schema.
  221. func (f *Flattener) FlattenType(typ TypeIdent) *apiext.JSONSchemaProps {
  222. f.init()
  223. if cachedSchema, isCached := f.flattenedTypes[typ]; isCached {
  224. return &cachedSchema
  225. }
  226. baseSchema, err := f.loadUnflattenedSchema(typ)
  227. if err != nil {
  228. typ.Package.AddError(err)
  229. return nil
  230. }
  231. resSchema := f.FlattenSchema(*baseSchema, typ.Package)
  232. f.cacheType(typ, *resSchema)
  233. return resSchema
  234. }
  235. // FlattenSchema flattens the given schema, removing any references.
  236. // It deep-copies the schema first, so the input schema won't be affected.
  237. func (f *Flattener) FlattenSchema(baseSchema apiext.JSONSchemaProps, currentPackage *loader.Package) *apiext.JSONSchemaProps {
  238. resSchema := baseSchema.DeepCopy()
  239. EditSchema(resSchema, &flattenVisitor{
  240. Flattener: f,
  241. currentPackage: currentPackage,
  242. })
  243. return resSchema
  244. }
  245. // RefParts splits a reference produced by the schema generator into its component
  246. // type name and package name (if it's a cross-package reference). Note that
  247. // referenced packages *must* be looked up relative to the current package.
  248. func RefParts(ref string) (typ string, pkgName string, err error) {
  249. if !strings.HasPrefix(ref, defPrefix) {
  250. return "", "", fmt.Errorf("non-standard reference link %q", ref)
  251. }
  252. ref = ref[len(defPrefix):]
  253. // decode the json pointer encodings
  254. ref = strings.Replace(ref, "~1", "/", -1)
  255. ref = strings.Replace(ref, "~0", "~", -1)
  256. nameParts := strings.SplitN(ref, "~", 2)
  257. if len(nameParts) == 1 {
  258. // local reference
  259. return nameParts[0], "", nil
  260. }
  261. // cross-package reference
  262. return nameParts[1], nameParts[0], nil
  263. }
  264. // identFromRef converts the given schema ref from the given package back
  265. // into the TypeIdent that it represents.
  266. func identFromRef(ref string, contextPkg *loader.Package) (TypeIdent, error) {
  267. typ, pkgName, err := RefParts(ref)
  268. if err != nil {
  269. return TypeIdent{}, err
  270. }
  271. if pkgName == "" {
  272. // a local reference
  273. return TypeIdent{
  274. Name: typ,
  275. Package: contextPkg,
  276. }, nil
  277. }
  278. // an external reference
  279. return TypeIdent{
  280. Name: typ,
  281. Package: contextPkg.Imports()[pkgName],
  282. }, nil
  283. }
  284. // preserveFields copies documentation fields from src into dst, preserving
  285. // field-level documentation when flattening, and preserving field-level validation
  286. // as allOf entries.
  287. func preserveFields(dst *apiext.JSONSchemaProps, src apiext.JSONSchemaProps) {
  288. srcDesc := src.Description
  289. srcTitle := src.Title
  290. srcExDoc := src.ExternalDocs
  291. srcEx := src.Example
  292. src.Description, src.Title, src.ExternalDocs, src.Example = "", "", nil, nil
  293. src.Ref = nil
  294. *dst = apiext.JSONSchemaProps{
  295. AllOf: []apiext.JSONSchemaProps{*dst, src},
  296. // keep these, in case the source field doesn't specify anything useful
  297. Description: dst.Description,
  298. Title: dst.Title,
  299. ExternalDocs: dst.ExternalDocs,
  300. Example: dst.Example,
  301. }
  302. if srcDesc != "" {
  303. dst.Description = srcDesc
  304. }
  305. if srcTitle != "" {
  306. dst.Title = srcTitle
  307. }
  308. if srcExDoc != nil {
  309. dst.ExternalDocs = srcExDoc
  310. }
  311. if srcEx != nil {
  312. dst.Example = srcEx
  313. }
  314. }
  315. // flattenVisitor visits each node in the schema, recursively flattening references.
  316. type flattenVisitor struct {
  317. *Flattener
  318. currentPackage *loader.Package
  319. currentType *TypeIdent
  320. currentSchema *apiext.JSONSchemaProps
  321. originalField apiext.JSONSchemaProps
  322. }
  323. func (f *flattenVisitor) Visit(baseSchema *apiext.JSONSchemaProps) SchemaVisitor {
  324. if baseSchema == nil {
  325. // end-of-node marker, cache the results
  326. if f.currentType != nil {
  327. f.cacheType(*f.currentType, *f.currentSchema)
  328. // preserve field information *after* caching so that we don't
  329. // accidentally cache field-level information onto the schema for
  330. // the type in general.
  331. preserveFields(f.currentSchema, f.originalField)
  332. }
  333. return f
  334. }
  335. // if we get a type that's just a ref, resolve it
  336. if baseSchema.Ref != nil && len(*baseSchema.Ref) > 0 {
  337. // resolve this ref
  338. refIdent, err := f.LookupReference(*baseSchema.Ref, f.currentPackage)
  339. if err != nil {
  340. f.currentPackage.AddError(err)
  341. return nil
  342. }
  343. // load and potentially flatten the schema
  344. // check the cache first...
  345. if refSchemaCached, isCached := f.flattenedTypes[refIdent]; isCached {
  346. // shallow copy is fine, it's just to avoid overwriting the doc fields
  347. preserveFields(&refSchemaCached, *baseSchema)
  348. *baseSchema = refSchemaCached
  349. return nil // don't recurse, we're done
  350. }
  351. // ...otherwise, we need to flatten
  352. refSchema, err := f.loadUnflattenedSchema(refIdent)
  353. if err != nil {
  354. f.currentPackage.AddError(err)
  355. return nil
  356. }
  357. refSchema = refSchema.DeepCopy()
  358. // keep field around to preserve field-level validation, docs, etc
  359. origField := *baseSchema
  360. *baseSchema = *refSchema
  361. // avoid loops (which shouldn't exist, but just in case)
  362. // by marking a nil cached pointer before we start recursing
  363. f.cacheType(refIdent, apiext.JSONSchemaProps{})
  364. return &flattenVisitor{
  365. Flattener: f.Flattener,
  366. currentPackage: refIdent.Package,
  367. currentType: &refIdent,
  368. currentSchema: baseSchema,
  369. originalField: origField,
  370. }
  371. }
  372. // otherwise, continue recursing...
  373. if f.currentType != nil {
  374. // ...but don't accidentally end this node early (for caching purposes)
  375. return &flattenVisitor{
  376. Flattener: f.Flattener,
  377. currentPackage: f.currentPackage,
  378. }
  379. }
  380. return f
  381. }