k8s.io/kube-openapi@v0.0.0-20240228011516-70dd3763d340/pkg/util/proto/document.go (about) 1 /* 2 Copyright 2017 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 proto 18 19 import ( 20 "fmt" 21 "sort" 22 "strings" 23 24 openapi_v2 "github.com/google/gnostic-models/openapiv2" 25 "gopkg.in/yaml.v2" 26 ) 27 28 func newSchemaError(path *Path, format string, a ...interface{}) error { 29 err := fmt.Sprintf(format, a...) 30 if path.Len() == 0 { 31 return fmt.Errorf("SchemaError: %v", err) 32 } 33 return fmt.Errorf("SchemaError(%v): %v", path, err) 34 } 35 36 // VendorExtensionToMap converts openapi VendorExtension to a map. 37 func VendorExtensionToMap(e []*openapi_v2.NamedAny) map[string]interface{} { 38 values := map[string]interface{}{} 39 40 for _, na := range e { 41 if na.GetName() == "" || na.GetValue() == nil { 42 continue 43 } 44 if na.GetValue().GetYaml() == "" { 45 continue 46 } 47 var value interface{} 48 err := yaml.Unmarshal([]byte(na.GetValue().GetYaml()), &value) 49 if err != nil { 50 continue 51 } 52 53 values[na.GetName()] = value 54 } 55 56 return values 57 } 58 59 // Definitions is an implementation of `Models`. It looks for 60 // models in an openapi Schema. 61 type Definitions struct { 62 models map[string]Schema 63 } 64 65 var _ Models = &Definitions{} 66 67 // NewOpenAPIData creates a new `Models` out of the openapi document. 68 func NewOpenAPIData(doc *openapi_v2.Document) (Models, error) { 69 definitions := Definitions{ 70 models: map[string]Schema{}, 71 } 72 73 // Save the list of all models first. This will allow us to 74 // validate that we don't have any dangling reference. 75 for _, namedSchema := range doc.GetDefinitions().GetAdditionalProperties() { 76 definitions.models[namedSchema.GetName()] = nil 77 } 78 79 // Now, parse each model. We can validate that references exists. 80 for _, namedSchema := range doc.GetDefinitions().GetAdditionalProperties() { 81 path := NewPath(namedSchema.GetName()) 82 schema, err := definitions.ParseSchema(namedSchema.GetValue(), &path) 83 if err != nil { 84 return nil, err 85 } 86 definitions.models[namedSchema.GetName()] = schema 87 } 88 89 return &definitions, nil 90 } 91 92 // We believe the schema is a reference, verify that and returns a new 93 // Schema 94 func (d *Definitions) parseReference(s *openapi_v2.Schema, path *Path) (Schema, error) { 95 // TODO(wrong): a schema with a $ref can have properties. We can ignore them (would be incomplete), but we cannot return an error. 96 if len(s.GetProperties().GetAdditionalProperties()) > 0 { 97 return nil, newSchemaError(path, "unallowed embedded type definition") 98 } 99 // TODO(wrong): a schema with a $ref can have a type. We can ignore it (would be incomplete), but we cannot return an error. 100 if len(s.GetType().GetValue()) > 0 { 101 return nil, newSchemaError(path, "definition reference can't have a type") 102 } 103 104 // TODO(wrong): $refs outside of the definitions are completely valid. We can ignore them (would be incomplete), but we cannot return an error. 105 if !strings.HasPrefix(s.GetXRef(), "#/definitions/") { 106 return nil, newSchemaError(path, "unallowed reference to non-definition %q", s.GetXRef()) 107 } 108 reference := strings.TrimPrefix(s.GetXRef(), "#/definitions/") 109 if _, ok := d.models[reference]; !ok { 110 return nil, newSchemaError(path, "unknown model in reference: %q", reference) 111 } 112 base, err := d.parseBaseSchema(s, path) 113 if err != nil { 114 return nil, err 115 } 116 return &Ref{ 117 BaseSchema: base, 118 reference: reference, 119 definitions: d, 120 }, nil 121 } 122 123 func parseDefault(def *openapi_v2.Any) (interface{}, error) { 124 if def == nil { 125 return nil, nil 126 } 127 var i interface{} 128 if err := yaml.Unmarshal([]byte(def.Yaml), &i); err != nil { 129 return nil, err 130 } 131 return i, nil 132 } 133 134 func (d *Definitions) parseBaseSchema(s *openapi_v2.Schema, path *Path) (BaseSchema, error) { 135 def, err := parseDefault(s.GetDefault()) 136 if err != nil { 137 return BaseSchema{}, err 138 } 139 return BaseSchema{ 140 Description: s.GetDescription(), 141 Default: def, 142 Extensions: VendorExtensionToMap(s.GetVendorExtension()), 143 Path: *path, 144 }, nil 145 } 146 147 // We believe the schema is a map, verify and return a new schema 148 func (d *Definitions) parseMap(s *openapi_v2.Schema, path *Path) (Schema, error) { 149 if len(s.GetType().GetValue()) != 0 && s.GetType().GetValue()[0] != object { 150 return nil, newSchemaError(path, "invalid object type") 151 } 152 var sub Schema 153 // TODO(incomplete): this misses the boolean case as AdditionalProperties is a bool+schema sum type. 154 if s.GetAdditionalProperties().GetSchema() == nil { 155 base, err := d.parseBaseSchema(s, path) 156 if err != nil { 157 return nil, err 158 } 159 sub = &Arbitrary{ 160 BaseSchema: base, 161 } 162 } else { 163 var err error 164 sub, err = d.ParseSchema(s.GetAdditionalProperties().GetSchema(), path) 165 if err != nil { 166 return nil, err 167 } 168 } 169 base, err := d.parseBaseSchema(s, path) 170 if err != nil { 171 return nil, err 172 } 173 return &Map{ 174 BaseSchema: base, 175 SubType: sub, 176 }, nil 177 } 178 179 func (d *Definitions) parsePrimitive(s *openapi_v2.Schema, path *Path) (Schema, error) { 180 var t string 181 if len(s.GetType().GetValue()) > 1 { 182 return nil, newSchemaError(path, "primitive can't have more than 1 type") 183 } 184 if len(s.GetType().GetValue()) == 1 { 185 t = s.GetType().GetValue()[0] 186 } 187 switch t { 188 case String: // do nothing 189 case Number: // do nothing 190 case Integer: // do nothing 191 case Boolean: // do nothing 192 // TODO(wrong): this misses "null". Would skip the null case (would be incomplete), but we cannot return an error. 193 default: 194 return nil, newSchemaError(path, "Unknown primitive type: %q", t) 195 } 196 base, err := d.parseBaseSchema(s, path) 197 if err != nil { 198 return nil, err 199 } 200 return &Primitive{ 201 BaseSchema: base, 202 Type: t, 203 Format: s.GetFormat(), 204 }, nil 205 } 206 207 func (d *Definitions) parseArray(s *openapi_v2.Schema, path *Path) (Schema, error) { 208 if len(s.GetType().GetValue()) != 1 { 209 return nil, newSchemaError(path, "array should have exactly one type") 210 } 211 if s.GetType().GetValue()[0] != array { 212 return nil, newSchemaError(path, `array should have type "array"`) 213 } 214 if len(s.GetItems().GetSchema()) != 1 { 215 // TODO(wrong): Items can have multiple elements. We can ignore Items then (would be incomplete), but we cannot return an error. 216 // TODO(wrong): "type: array" witohut any items at all is completely valid. 217 return nil, newSchemaError(path, "array should have exactly one sub-item") 218 } 219 sub, err := d.ParseSchema(s.GetItems().GetSchema()[0], path) 220 if err != nil { 221 return nil, err 222 } 223 base, err := d.parseBaseSchema(s, path) 224 if err != nil { 225 return nil, err 226 } 227 return &Array{ 228 BaseSchema: base, 229 SubType: sub, 230 }, nil 231 } 232 233 func (d *Definitions) parseKind(s *openapi_v2.Schema, path *Path) (Schema, error) { 234 if len(s.GetType().GetValue()) != 0 && s.GetType().GetValue()[0] != object { 235 return nil, newSchemaError(path, "invalid object type") 236 } 237 if s.GetProperties() == nil { 238 return nil, newSchemaError(path, "object doesn't have properties") 239 } 240 241 fields := map[string]Schema{} 242 fieldOrder := []string{} 243 244 for _, namedSchema := range s.GetProperties().GetAdditionalProperties() { 245 var err error 246 name := namedSchema.GetName() 247 path := path.FieldPath(name) 248 fields[name], err = d.ParseSchema(namedSchema.GetValue(), &path) 249 if err != nil { 250 return nil, err 251 } 252 fieldOrder = append(fieldOrder, name) 253 } 254 255 base, err := d.parseBaseSchema(s, path) 256 if err != nil { 257 return nil, err 258 } 259 return &Kind{ 260 BaseSchema: base, 261 RequiredFields: s.GetRequired(), 262 Fields: fields, 263 FieldOrder: fieldOrder, 264 }, nil 265 } 266 267 func (d *Definitions) parseArbitrary(s *openapi_v2.Schema, path *Path) (Schema, error) { 268 base, err := d.parseBaseSchema(s, path) 269 if err != nil { 270 return nil, err 271 } 272 return &Arbitrary{ 273 BaseSchema: base, 274 }, nil 275 } 276 277 // ParseSchema creates a walkable Schema from an openapi schema. While 278 // this function is public, it doesn't leak through the interface. 279 func (d *Definitions) ParseSchema(s *openapi_v2.Schema, path *Path) (Schema, error) { 280 if s.GetXRef() != "" { 281 // TODO(incomplete): ignoring the rest of s is wrong. As long as there are no conflict, everything from s must be considered 282 // Reference: https://github.com/OAI/OpenAPI-Specification/blob/master/versions/2.0.md#path-item-object 283 return d.parseReference(s, path) 284 } 285 objectTypes := s.GetType().GetValue() 286 switch len(objectTypes) { 287 case 0: 288 // in the OpenAPI schema served by older k8s versions, object definitions created from structs did not include 289 // the type:object property (they only included the "properties" property), so we need to handle this case 290 // TODO: validate that we ever published empty, non-nil properties. JSON roundtripping nils them. 291 if s.GetProperties() != nil { 292 // TODO(wrong): when verifying a non-object later against this, it will be rejected as invalid type. 293 // TODO(CRD validation schema publishing): we have to filter properties (empty or not) if type=object is not given 294 return d.parseKind(s, path) 295 } else { 296 // Definition has no type and no properties. Treat it as an arbitrary value 297 // TODO(incomplete): what if it has additionalProperties=false or patternProperties? 298 // ANSWER: parseArbitrary is less strict than it has to be with patternProperties (which is ignored). So this is correct (of course not complete). 299 return d.parseArbitrary(s, path) 300 } 301 case 1: 302 t := objectTypes[0] 303 switch t { 304 case object: 305 if s.GetProperties() != nil { 306 return d.parseKind(s, path) 307 } else { 308 return d.parseMap(s, path) 309 } 310 case array: 311 return d.parseArray(s, path) 312 } 313 return d.parsePrimitive(s, path) 314 default: 315 // the OpenAPI generator never generates (nor it ever did in the past) OpenAPI type definitions with multiple types 316 // TODO(wrong): this is rejecting a completely valid OpenAPI spec 317 // TODO(CRD validation schema publishing): filter these out 318 return nil, newSchemaError(path, "definitions with multiple types aren't supported") 319 } 320 } 321 322 // LookupModel is public through the interface of Models. It 323 // returns a visitable schema from the given model name. 324 func (d *Definitions) LookupModel(model string) Schema { 325 return d.models[model] 326 } 327 328 func (d *Definitions) ListModels() []string { 329 models := []string{} 330 331 for model := range d.models { 332 models = append(models, model) 333 } 334 335 sort.Strings(models) 336 return models 337 } 338 339 type Ref struct { 340 BaseSchema 341 342 reference string 343 definitions *Definitions 344 } 345 346 var _ Reference = &Ref{} 347 348 func (r *Ref) Reference() string { 349 return r.reference 350 } 351 352 func (r *Ref) SubSchema() Schema { 353 return r.definitions.models[r.reference] 354 } 355 356 func (r *Ref) Accept(v SchemaVisitor) { 357 v.VisitReference(r) 358 } 359 360 func (r *Ref) GetName() string { 361 return fmt.Sprintf("Reference to %q", r.reference) 362 }