1/* 2Copyright 2017 The Kubernetes Authors. 3 4Licensed under the Apache License, Version 2.0 (the "License"); 5you may not use this file except in compliance with the License. 6You may obtain a copy of the License at 7 8 http://www.apache.org/licenses/LICENSE-2.0 9 10Unless required by applicable law or agreed to in writing, software 11distributed under the License is distributed on an "AS IS" BASIS, 12WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13See the License for the specific language governing permissions and 14limitations under the License. 15*/ 16 17package proto 18 19import ( 20 "fmt" 21 "sort" 22 "strings" 23 24 "github.com/googleapis/gnostic/OpenAPIv2" 25 "gopkg.in/yaml.v2" 26) 27 28func 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. 37func 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. 61type Definitions struct { 62 models map[string]Schema 63} 64 65var _ Models = &Definitions{} 66 67// NewOpenAPIData creates a new `Models` out of the openapi document. 68func 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 94func (d *Definitions) parseReference(s *openapi_v2.Schema, path *Path) (Schema, error) { 95 if len(s.GetProperties().GetAdditionalProperties()) > 0 { 96 return nil, newSchemaError(path, "unallowed embedded type definition") 97 } 98 if len(s.GetType().GetValue()) > 0 { 99 return nil, newSchemaError(path, "definition reference can't have a type") 100 } 101 102 if !strings.HasPrefix(s.GetXRef(), "#/definitions/") { 103 return nil, newSchemaError(path, "unallowed reference to non-definition %q", s.GetXRef()) 104 } 105 reference := strings.TrimPrefix(s.GetXRef(), "#/definitions/") 106 if _, ok := d.models[reference]; !ok { 107 return nil, newSchemaError(path, "unknown model in reference: %q", reference) 108 } 109 return &Ref{ 110 BaseSchema: d.parseBaseSchema(s, path), 111 reference: reference, 112 definitions: d, 113 }, nil 114} 115 116func (d *Definitions) parseBaseSchema(s *openapi_v2.Schema, path *Path) BaseSchema { 117 return BaseSchema{ 118 Description: s.GetDescription(), 119 Extensions: VendorExtensionToMap(s.GetVendorExtension()), 120 Path: *path, 121 } 122} 123 124// We believe the schema is a map, verify and return a new schema 125func (d *Definitions) parseMap(s *openapi_v2.Schema, path *Path) (Schema, error) { 126 if len(s.GetType().GetValue()) != 0 && s.GetType().GetValue()[0] != object { 127 return nil, newSchemaError(path, "invalid object type") 128 } 129 var sub Schema 130 if s.GetAdditionalProperties().GetSchema() == nil { 131 sub = &Arbitrary{ 132 BaseSchema: d.parseBaseSchema(s, path), 133 } 134 } else { 135 var err error 136 sub, err = d.ParseSchema(s.GetAdditionalProperties().GetSchema(), path) 137 if err != nil { 138 return nil, err 139 } 140 } 141 return &Map{ 142 BaseSchema: d.parseBaseSchema(s, path), 143 SubType: sub, 144 }, nil 145} 146 147func (d *Definitions) parsePrimitive(s *openapi_v2.Schema, path *Path) (Schema, error) { 148 var t string 149 if len(s.GetType().GetValue()) > 1 { 150 return nil, newSchemaError(path, "primitive can't have more than 1 type") 151 } 152 if len(s.GetType().GetValue()) == 1 { 153 t = s.GetType().GetValue()[0] 154 } 155 switch t { 156 case String: // do nothing 157 case Number: // do nothing 158 case Integer: // do nothing 159 case Boolean: // do nothing 160 default: 161 return nil, newSchemaError(path, "Unknown primitive type: %q", t) 162 } 163 return &Primitive{ 164 BaseSchema: d.parseBaseSchema(s, path), 165 Type: t, 166 Format: s.GetFormat(), 167 }, nil 168} 169 170func (d *Definitions) parseArray(s *openapi_v2.Schema, path *Path) (Schema, error) { 171 if len(s.GetType().GetValue()) != 1 { 172 return nil, newSchemaError(path, "array should have exactly one type") 173 } 174 if s.GetType().GetValue()[0] != array { 175 return nil, newSchemaError(path, `array should have type "array"`) 176 } 177 if len(s.GetItems().GetSchema()) != 1 { 178 return nil, newSchemaError(path, "array should have exactly one sub-item") 179 } 180 sub, err := d.ParseSchema(s.GetItems().GetSchema()[0], path) 181 if err != nil { 182 return nil, err 183 } 184 return &Array{ 185 BaseSchema: d.parseBaseSchema(s, path), 186 SubType: sub, 187 }, nil 188} 189 190func (d *Definitions) parseKind(s *openapi_v2.Schema, path *Path) (Schema, error) { 191 if len(s.GetType().GetValue()) != 0 && s.GetType().GetValue()[0] != object { 192 return nil, newSchemaError(path, "invalid object type") 193 } 194 if s.GetProperties() == nil { 195 return nil, newSchemaError(path, "object doesn't have properties") 196 } 197 198 fields := map[string]Schema{} 199 200 for _, namedSchema := range s.GetProperties().GetAdditionalProperties() { 201 var err error 202 path := path.FieldPath(namedSchema.GetName()) 203 fields[namedSchema.GetName()], err = d.ParseSchema(namedSchema.GetValue(), &path) 204 if err != nil { 205 return nil, err 206 } 207 } 208 209 return &Kind{ 210 BaseSchema: d.parseBaseSchema(s, path), 211 RequiredFields: s.GetRequired(), 212 Fields: fields, 213 }, nil 214} 215 216func (d *Definitions) parseArbitrary(s *openapi_v2.Schema, path *Path) (Schema, error) { 217 return &Arbitrary{ 218 BaseSchema: d.parseBaseSchema(s, path), 219 }, nil 220} 221 222// ParseSchema creates a walkable Schema from an openapi schema. While 223// this function is public, it doesn't leak through the interface. 224func (d *Definitions) ParseSchema(s *openapi_v2.Schema, path *Path) (Schema, error) { 225 if s.GetXRef() != "" { 226 return d.parseReference(s, path) 227 } 228 objectTypes := s.GetType().GetValue() 229 switch len(objectTypes) { 230 case 0: 231 // in the OpenAPI schema served by older k8s versions, object definitions created from structs did not include 232 // the type:object property (they only included the "properties" property), so we need to handle this case 233 if s.GetProperties() != nil { 234 return d.parseKind(s, path) 235 } else { 236 // Definition has no type and no properties. Treat it as an arbitrary value 237 // TODO: what if it has additionalProperties or patternProperties? 238 return d.parseArbitrary(s, path) 239 } 240 case 1: 241 t := objectTypes[0] 242 switch t { 243 case object: 244 if s.GetProperties() != nil { 245 return d.parseKind(s, path) 246 } else { 247 return d.parseMap(s, path) 248 } 249 case array: 250 return d.parseArray(s, path) 251 } 252 return d.parsePrimitive(s, path) 253 default: 254 // the OpenAPI generator never generates (nor it ever did in the past) OpenAPI type definitions with multiple types 255 return nil, newSchemaError(path, "definitions with multiple types aren't supported") 256 } 257} 258 259// LookupModel is public through the interface of Models. It 260// returns a visitable schema from the given model name. 261func (d *Definitions) LookupModel(model string) Schema { 262 return d.models[model] 263} 264 265func (d *Definitions) ListModels() []string { 266 models := []string{} 267 268 for model := range d.models { 269 models = append(models, model) 270 } 271 272 sort.Strings(models) 273 return models 274} 275 276type Ref struct { 277 BaseSchema 278 279 reference string 280 definitions *Definitions 281} 282 283var _ Reference = &Ref{} 284 285func (r *Ref) Reference() string { 286 return r.reference 287} 288 289func (r *Ref) SubSchema() Schema { 290 return r.definitions.models[r.reference] 291} 292 293func (r *Ref) Accept(v SchemaVisitor) { 294 v.VisitReference(r) 295} 296 297func (r *Ref) GetName() string { 298 return fmt.Sprintf("Reference to %q", r.reference) 299} 300