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