openapi.go 7.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260
  1. /*
  2. Copyright 2022 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. "errors"
  16. "path"
  17. "strings"
  18. "k8s.io/kube-openapi/pkg/validation/spec"
  19. "sigs.k8s.io/structured-merge-diff/v4/schema"
  20. )
  21. // ToSchemaFromOpenAPI converts a directory of OpenAPI schemas to an smd Schema.
  22. // - models: a map from definition name to OpenAPI V3 structural schema for each definition.
  23. // Key in map is used to resolve references in the schema.
  24. // - preserveUnknownFields: flag indicating whether unknown fields in all schemas should be preserved.
  25. // - returns: nil and an error if there is a parse error, or if schema does not satisfy a
  26. // required structural schema invariant for conversion. If no error, returns
  27. // a new smd schema.
  28. //
  29. // Schema should be validated as structural before using with this function, or
  30. // there may be information lost.
  31. func ToSchemaFromOpenAPI(models map[string]*spec.Schema, preserveUnknownFields bool) (*schema.Schema, error) {
  32. c := convert{
  33. preserveUnknownFields: preserveUnknownFields,
  34. output: &schema.Schema{},
  35. }
  36. for name, spec := range models {
  37. // Skip/Ignore top-level references
  38. if len(spec.Ref.String()) > 0 {
  39. continue
  40. }
  41. var a schema.Atom
  42. // Hard-coded schemas for now as proto_models implementation functions.
  43. // https://github.com/kubernetes/kube-openapi/issues/364
  44. if name == quantityResource {
  45. a = schema.Atom{
  46. Scalar: untypedDef.Atom.Scalar,
  47. }
  48. } else if name == rawExtensionResource {
  49. a = untypedDef.Atom
  50. } else {
  51. c2 := c.push(name, &a)
  52. c2.visitSpec(spec)
  53. c.pop(c2)
  54. }
  55. c.insertTypeDef(name, a)
  56. }
  57. if len(c.errorMessages) > 0 {
  58. return nil, errors.New(strings.Join(c.errorMessages, "\n"))
  59. }
  60. c.addCommonTypes()
  61. return c.output, nil
  62. }
  63. func (c *convert) visitSpec(m *spec.Schema) {
  64. // Check if this schema opts its descendants into preserve-unknown-fields
  65. if p, ok := m.Extensions["x-kubernetes-preserve-unknown-fields"]; ok && p == true {
  66. c.preserveUnknownFields = true
  67. }
  68. a := c.top()
  69. *a = c.parseSchema(m)
  70. }
  71. func (c *convert) parseSchema(m *spec.Schema) schema.Atom {
  72. // k8s-generated OpenAPI specs have historically used only one value for
  73. // type and starting with OpenAPIV3 it is only allowed to be
  74. // a single string.
  75. typ := ""
  76. if len(m.Type) > 0 {
  77. typ = m.Type[0]
  78. }
  79. // Structural Schemas produced by kubernetes follow very specific rules which
  80. // we can use to infer the SMD type:
  81. switch typ {
  82. case "":
  83. // According to Swagger docs:
  84. // https://swagger.io/docs/specification/data-models/data-types/#any
  85. //
  86. // If no type is specified, it is equivalent to accepting any type.
  87. return schema.Atom{
  88. Scalar: ptr(schema.Scalar("untyped")),
  89. List: c.parseList(m),
  90. Map: c.parseObject(m),
  91. }
  92. case "object":
  93. return schema.Atom{
  94. Map: c.parseObject(m),
  95. }
  96. case "array":
  97. return schema.Atom{
  98. List: c.parseList(m),
  99. }
  100. case "integer", "boolean", "number", "string":
  101. return convertPrimitive(typ, m.Format)
  102. default:
  103. c.reportError("unrecognized type: '%v'", typ)
  104. return schema.Atom{
  105. Scalar: ptr(schema.Scalar("untyped")),
  106. }
  107. }
  108. }
  109. func (c *convert) makeOpenAPIRef(specSchema *spec.Schema) schema.TypeRef {
  110. refString := specSchema.Ref.String()
  111. // Special-case handling for $ref stored inside a single-element allOf
  112. if len(refString) == 0 && len(specSchema.AllOf) == 1 && len(specSchema.AllOf[0].Ref.String()) > 0 {
  113. refString = specSchema.AllOf[0].Ref.String()
  114. }
  115. if _, n := path.Split(refString); len(n) > 0 {
  116. //!TODO: Refactor the field ElementRelationship override
  117. // we can generate the types with overrides ahead of time rather than
  118. // requiring the hacky runtime support
  119. // (could just create a normalized key struct containing all customizations
  120. // to deduplicate)
  121. mapRelationship, err := getMapElementRelationship(specSchema.Extensions)
  122. if err != nil {
  123. c.reportError(err.Error())
  124. }
  125. if len(mapRelationship) > 0 {
  126. return schema.TypeRef{
  127. NamedType: &n,
  128. ElementRelationship: &mapRelationship,
  129. }
  130. }
  131. return schema.TypeRef{
  132. NamedType: &n,
  133. }
  134. }
  135. var inlined schema.Atom
  136. // compute the type inline
  137. c2 := c.push("inlined in "+c.currentName, &inlined)
  138. c2.preserveUnknownFields = c.preserveUnknownFields
  139. c2.visitSpec(specSchema)
  140. c.pop(c2)
  141. return schema.TypeRef{
  142. Inlined: inlined,
  143. }
  144. }
  145. func (c *convert) parseObject(s *spec.Schema) *schema.Map {
  146. var fields []schema.StructField
  147. for name, member := range s.Properties {
  148. fields = append(fields, schema.StructField{
  149. Name: name,
  150. Type: c.makeOpenAPIRef(&member),
  151. Default: member.Default,
  152. })
  153. }
  154. // AdditionalProperties informs the schema of any "unknown" keys
  155. // Unknown keys are enforced by the ElementType field.
  156. elementType := func() schema.TypeRef {
  157. if s.AdditionalProperties == nil {
  158. // According to openAPI spec, an object without properties and without
  159. // additionalProperties is assumed to be a free-form object.
  160. if c.preserveUnknownFields || len(s.Properties) == 0 {
  161. return schema.TypeRef{
  162. NamedType: &deducedName,
  163. }
  164. }
  165. // If properties are specified, do not implicitly allow unknown
  166. // fields
  167. return schema.TypeRef{}
  168. } else if s.AdditionalProperties.Schema != nil {
  169. // Unknown fields use the referred schema
  170. return c.makeOpenAPIRef(s.AdditionalProperties.Schema)
  171. } else if s.AdditionalProperties.Allows {
  172. // A boolean instead of a schema was provided. Deduce the
  173. // type from the value provided at runtime.
  174. return schema.TypeRef{
  175. NamedType: &deducedName,
  176. }
  177. } else {
  178. // Additional Properties are explicitly disallowed by the user.
  179. // Ensure element type is empty.
  180. return schema.TypeRef{}
  181. }
  182. }()
  183. relationship, err := getMapElementRelationship(s.Extensions)
  184. if err != nil {
  185. c.reportError(err.Error())
  186. }
  187. return &schema.Map{
  188. Fields: fields,
  189. ElementRelationship: relationship,
  190. ElementType: elementType,
  191. }
  192. }
  193. func (c *convert) parseList(s *spec.Schema) *schema.List {
  194. relationship, mapKeys, err := getListElementRelationship(s.Extensions)
  195. if err != nil {
  196. c.reportError(err.Error())
  197. }
  198. elementType := func() schema.TypeRef {
  199. if s.Items != nil {
  200. if s.Items.Schema == nil || s.Items.Len() != 1 {
  201. c.reportError("structural schema arrays must have exactly one member subtype")
  202. return schema.TypeRef{
  203. NamedType: &deducedName,
  204. }
  205. }
  206. subSchema := s.Items.Schema
  207. if subSchema == nil {
  208. subSchema = &s.Items.Schemas[0]
  209. }
  210. return c.makeOpenAPIRef(subSchema)
  211. } else if len(s.Type) > 0 && len(s.Type[0]) > 0 {
  212. c.reportError("`items` must be specified on arrays")
  213. }
  214. // A list with no items specified is treated as "untyped".
  215. return schema.TypeRef{
  216. NamedType: &untypedName,
  217. }
  218. }()
  219. return &schema.List{
  220. ElementRelationship: relationship,
  221. Keys: mapKeys,
  222. ElementType: elementType,
  223. }
  224. }