smd.go 8.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337
  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/v4/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. if union.Discriminator != nil && len(union.Fields) == 0 {
  190. return schema.Union{}, fmt.Errorf("discriminator set to %v, but no fields in union", *union.Discriminator)
  191. }
  192. return union, nil
  193. }
  194. func toStringSlice(o interface{}) (out []string, ok bool) {
  195. switch t := o.(type) {
  196. case []interface{}:
  197. for _, v := range t {
  198. switch vt := v.(type) {
  199. case string:
  200. out = append(out, vt)
  201. }
  202. }
  203. return out, true
  204. case []string:
  205. return t, true
  206. }
  207. return nil, false
  208. }
  209. func ptr(s schema.Scalar) *schema.Scalar { return &s }
  210. // Basic conversion functions to convert OpenAPI schema definitions to
  211. // SMD Schema atoms
  212. func convertPrimitive(typ string, format string) (a schema.Atom) {
  213. switch typ {
  214. case "integer":
  215. a.Scalar = ptr(schema.Numeric)
  216. case "number":
  217. a.Scalar = ptr(schema.Numeric)
  218. case "string":
  219. switch format {
  220. case "":
  221. a.Scalar = ptr(schema.String)
  222. case "byte":
  223. // byte really means []byte and is encoded as a string.
  224. a.Scalar = ptr(schema.String)
  225. case "int-or-string":
  226. a.Scalar = ptr(schema.Scalar("untyped"))
  227. case "date-time":
  228. a.Scalar = ptr(schema.Scalar("untyped"))
  229. default:
  230. a.Scalar = ptr(schema.Scalar("untyped"))
  231. }
  232. case "boolean":
  233. a.Scalar = ptr(schema.Boolean)
  234. default:
  235. a.Scalar = ptr(schema.Scalar("untyped"))
  236. }
  237. return a
  238. }
  239. func getListElementRelationship(ext map[string]any) (schema.ElementRelationship, []string, error) {
  240. if val, ok := ext["x-kubernetes-list-type"]; ok {
  241. switch val {
  242. case "atomic":
  243. return schema.Atomic, nil, nil
  244. case "set":
  245. return schema.Associative, nil, nil
  246. case "map":
  247. keys, ok := ext["x-kubernetes-list-map-keys"]
  248. if !ok {
  249. return schema.Associative, nil, fmt.Errorf("missing map keys")
  250. }
  251. keyNames, ok := toStringSlice(keys)
  252. if !ok {
  253. return schema.Associative, nil, fmt.Errorf("uninterpreted map keys: %#v", keys)
  254. }
  255. return schema.Associative, keyNames, nil
  256. default:
  257. return schema.Atomic, nil, fmt.Errorf("unknown list type %v", val)
  258. }
  259. } else if val, ok := ext["x-kubernetes-patch-strategy"]; ok {
  260. switch val {
  261. case "merge", "merge,retainKeys":
  262. if key, ok := ext["x-kubernetes-patch-merge-key"]; ok {
  263. keyName, ok := key.(string)
  264. if !ok {
  265. return schema.Associative, nil, fmt.Errorf("uninterpreted merge key: %#v", key)
  266. }
  267. return schema.Associative, []string{keyName}, nil
  268. }
  269. // It's not an error for x-kubernetes-patch-merge-key to be absent,
  270. // it means it's a set
  271. return schema.Associative, nil, nil
  272. case "retainKeys":
  273. return schema.Atomic, nil, nil
  274. default:
  275. return schema.Atomic, nil, fmt.Errorf("unknown patch strategy %v", val)
  276. }
  277. }
  278. // Treat as atomic by default
  279. return schema.Atomic, nil, nil
  280. }
  281. // Returns map element relationship if specified, or empty string if unspecified
  282. func getMapElementRelationship(ext map[string]any) (schema.ElementRelationship, error) {
  283. val, ok := ext["x-kubernetes-map-type"]
  284. if !ok {
  285. // unset Map element relationship
  286. return "", nil
  287. }
  288. switch val {
  289. case "atomic":
  290. return schema.Atomic, nil
  291. case "granular":
  292. return schema.Separable, nil
  293. default:
  294. return "", fmt.Errorf("unknown map type %v", val)
  295. }
  296. }