smd.go 8.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334
  1. /*
  2. Copyright 2017 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 schemaconv
  14. import (
  15. "fmt"
  16. "sort"
  17. "sigs.k8s.io/structured-merge-diff/v6/schema"
  18. )
  19. const (
  20. quantityResource = "io.k8s.apimachinery.pkg.api.resource.Quantity"
  21. rawExtensionResource = "io.k8s.apimachinery.pkg.runtime.RawExtension"
  22. )
  23. type convert struct {
  24. preserveUnknownFields bool
  25. output *schema.Schema
  26. currentName string
  27. current *schema.Atom
  28. errorMessages []string
  29. }
  30. func (c *convert) push(name string, a *schema.Atom) *convert {
  31. return &convert{
  32. preserveUnknownFields: c.preserveUnknownFields,
  33. output: c.output,
  34. currentName: name,
  35. current: a,
  36. }
  37. }
  38. func (c *convert) top() *schema.Atom { return c.current }
  39. func (c *convert) pop(c2 *convert) {
  40. c.errorMessages = append(c.errorMessages, c2.errorMessages...)
  41. }
  42. func (c *convert) reportError(format string, args ...interface{}) {
  43. c.errorMessages = append(c.errorMessages,
  44. c.currentName+": "+fmt.Sprintf(format, args...),
  45. )
  46. }
  47. func (c *convert) insertTypeDef(name string, atom schema.Atom) {
  48. def := schema.TypeDef{
  49. Name: name,
  50. Atom: atom,
  51. }
  52. if def.Atom == (schema.Atom{}) {
  53. // This could happen if there were a top-level reference.
  54. return
  55. }
  56. c.output.Types = append(c.output.Types, def)
  57. }
  58. func (c *convert) addCommonTypes() {
  59. c.output.Types = append(c.output.Types, untypedDef)
  60. c.output.Types = append(c.output.Types, deducedDef)
  61. }
  62. var untypedName string = "__untyped_atomic_"
  63. var untypedDef schema.TypeDef = schema.TypeDef{
  64. Name: untypedName,
  65. Atom: schema.Atom{
  66. Scalar: ptr(schema.Scalar("untyped")),
  67. List: &schema.List{
  68. ElementType: schema.TypeRef{
  69. NamedType: &untypedName,
  70. },
  71. ElementRelationship: schema.Atomic,
  72. },
  73. Map: &schema.Map{
  74. ElementType: schema.TypeRef{
  75. NamedType: &untypedName,
  76. },
  77. ElementRelationship: schema.Atomic,
  78. },
  79. },
  80. }
  81. var deducedName string = "__untyped_deduced_"
  82. var deducedDef schema.TypeDef = schema.TypeDef{
  83. Name: deducedName,
  84. Atom: schema.Atom{
  85. Scalar: ptr(schema.Scalar("untyped")),
  86. List: &schema.List{
  87. ElementType: schema.TypeRef{
  88. NamedType: &untypedName,
  89. },
  90. ElementRelationship: schema.Atomic,
  91. },
  92. Map: &schema.Map{
  93. ElementType: schema.TypeRef{
  94. NamedType: &deducedName,
  95. },
  96. ElementRelationship: schema.Separable,
  97. },
  98. },
  99. }
  100. func makeUnions(extensions map[string]interface{}) ([]schema.Union, error) {
  101. schemaUnions := []schema.Union{}
  102. if iunions, ok := extensions["x-kubernetes-unions"]; ok {
  103. unions, ok := iunions.([]interface{})
  104. if !ok {
  105. return nil, fmt.Errorf(`"x-kubernetes-unions" should be a list, got %#v`, unions)
  106. }
  107. for _, iunion := range unions {
  108. union, ok := iunion.(map[interface{}]interface{})
  109. if !ok {
  110. return nil, fmt.Errorf(`"x-kubernetes-unions" items should be a map of string to unions, got %#v`, iunion)
  111. }
  112. unionMap := map[string]interface{}{}
  113. for k, v := range union {
  114. key, ok := k.(string)
  115. if !ok {
  116. return nil, fmt.Errorf(`"x-kubernetes-unions" has non-string key: %#v`, k)
  117. }
  118. unionMap[key] = v
  119. }
  120. schemaUnion, err := makeUnion(unionMap)
  121. if err != nil {
  122. return nil, err
  123. }
  124. schemaUnions = append(schemaUnions, schemaUnion)
  125. }
  126. }
  127. // Make sure we have no overlap between unions
  128. fs := map[string]struct{}{}
  129. for _, u := range schemaUnions {
  130. if u.Discriminator != nil {
  131. if _, ok := fs[*u.Discriminator]; ok {
  132. return nil, fmt.Errorf("%v field appears multiple times in unions", *u.Discriminator)
  133. }
  134. fs[*u.Discriminator] = struct{}{}
  135. }
  136. for _, f := range u.Fields {
  137. if _, ok := fs[f.FieldName]; ok {
  138. return nil, fmt.Errorf("%v field appears multiple times in unions", f.FieldName)
  139. }
  140. fs[f.FieldName] = struct{}{}
  141. }
  142. }
  143. return schemaUnions, nil
  144. }
  145. func makeUnion(extensions map[string]interface{}) (schema.Union, error) {
  146. union := schema.Union{
  147. Fields: []schema.UnionField{},
  148. }
  149. if idiscriminator, ok := extensions["discriminator"]; ok {
  150. discriminator, ok := idiscriminator.(string)
  151. if !ok {
  152. return schema.Union{}, fmt.Errorf(`"discriminator" must be a string, got: %#v`, idiscriminator)
  153. }
  154. union.Discriminator = &discriminator
  155. }
  156. if ifields, ok := extensions["fields-to-discriminateBy"]; ok {
  157. fields, ok := ifields.(map[interface{}]interface{})
  158. if !ok {
  159. return schema.Union{}, fmt.Errorf(`"fields-to-discriminateBy" must be a map[string]string, got: %#v`, ifields)
  160. }
  161. // Needs sorted keys by field.
  162. keys := []string{}
  163. for ifield := range fields {
  164. field, ok := ifield.(string)
  165. if !ok {
  166. return schema.Union{}, fmt.Errorf(`"fields-to-discriminateBy": field must be a string, got: %#v`, ifield)
  167. }
  168. keys = append(keys, field)
  169. }
  170. sort.Strings(keys)
  171. reverseMap := map[string]struct{}{}
  172. for _, field := range keys {
  173. value := fields[field]
  174. discriminated, ok := value.(string)
  175. if !ok {
  176. return schema.Union{}, fmt.Errorf(`"fields-to-discriminateBy"/%v: value must be a string, got: %#v`, field, value)
  177. }
  178. union.Fields = append(union.Fields, schema.UnionField{
  179. FieldName: field,
  180. DiscriminatorValue: discriminated,
  181. })
  182. // Check that we don't have the same discriminateBy multiple times.
  183. if _, ok := reverseMap[discriminated]; ok {
  184. return schema.Union{}, fmt.Errorf("Multiple fields have the same discriminated name: %v", discriminated)
  185. }
  186. reverseMap[discriminated] = struct{}{}
  187. }
  188. }
  189. return union, nil
  190. }
  191. func toStringSlice(o interface{}) (out []string, ok bool) {
  192. switch t := o.(type) {
  193. case []interface{}:
  194. for _, v := range t {
  195. switch vt := v.(type) {
  196. case string:
  197. out = append(out, vt)
  198. }
  199. }
  200. return out, true
  201. case []string:
  202. return t, true
  203. }
  204. return nil, false
  205. }
  206. func ptr(s schema.Scalar) *schema.Scalar { return &s }
  207. // Basic conversion functions to convert OpenAPI schema definitions to
  208. // SMD Schema atoms
  209. func convertPrimitive(typ string, format string) (a schema.Atom) {
  210. switch typ {
  211. case "integer":
  212. a.Scalar = ptr(schema.Numeric)
  213. case "number":
  214. a.Scalar = ptr(schema.Numeric)
  215. case "string":
  216. switch format {
  217. case "":
  218. a.Scalar = ptr(schema.String)
  219. case "byte":
  220. // byte really means []byte and is encoded as a string.
  221. a.Scalar = ptr(schema.String)
  222. case "int-or-string":
  223. a.Scalar = ptr(schema.Scalar("untyped"))
  224. case "date-time":
  225. a.Scalar = ptr(schema.Scalar("untyped"))
  226. default:
  227. a.Scalar = ptr(schema.Scalar("untyped"))
  228. }
  229. case "boolean":
  230. a.Scalar = ptr(schema.Boolean)
  231. default:
  232. a.Scalar = ptr(schema.Scalar("untyped"))
  233. }
  234. return a
  235. }
  236. func getListElementRelationship(ext map[string]any) (schema.ElementRelationship, []string, error) {
  237. if val, ok := ext["x-kubernetes-list-type"]; ok {
  238. switch val {
  239. case "atomic":
  240. return schema.Atomic, nil, nil
  241. case "set":
  242. return schema.Associative, nil, nil
  243. case "map":
  244. keys, ok := ext["x-kubernetes-list-map-keys"]
  245. if !ok {
  246. return schema.Associative, nil, fmt.Errorf("missing map keys")
  247. }
  248. keyNames, ok := toStringSlice(keys)
  249. if !ok {
  250. return schema.Associative, nil, fmt.Errorf("uninterpreted map keys: %#v", keys)
  251. }
  252. return schema.Associative, keyNames, nil
  253. default:
  254. return schema.Atomic, nil, fmt.Errorf("unknown list type %v", val)
  255. }
  256. } else if val, ok := ext["x-kubernetes-patch-strategy"]; ok {
  257. switch val {
  258. case "merge", "merge,retainKeys":
  259. if key, ok := ext["x-kubernetes-patch-merge-key"]; ok {
  260. keyName, ok := key.(string)
  261. if !ok {
  262. return schema.Associative, nil, fmt.Errorf("uninterpreted merge key: %#v", key)
  263. }
  264. return schema.Associative, []string{keyName}, nil
  265. }
  266. // It's not an error for x-kubernetes-patch-merge-key to be absent,
  267. // it means it's a set
  268. return schema.Associative, nil, nil
  269. case "retainKeys":
  270. return schema.Atomic, nil, nil
  271. default:
  272. return schema.Atomic, nil, fmt.Errorf("unknown patch strategy %v", val)
  273. }
  274. }
  275. // Treat as atomic by default
  276. return schema.Atomic, nil, nil
  277. }
  278. // Returns map element relationship if specified, or empty string if unspecified
  279. func getMapElementRelationship(ext map[string]any) (schema.ElementRelationship, error) {
  280. val, ok := ext["x-kubernetes-map-type"]
  281. if !ok {
  282. // unset Map element relationship
  283. return "", nil
  284. }
  285. switch val {
  286. case "atomic":
  287. return schema.Atomic, nil
  288. case "granular":
  289. return schema.Separable, nil
  290. default:
  291. return "", fmt.Errorf("unknown map type %v", val)
  292. }
  293. }