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	openapi_v2 "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	// 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
123func 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
134func (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
148func (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
179func (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
207func (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
233func (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
267func (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.
279func (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.
324func (d *Definitions) LookupModel(model string) Schema {
325	return d.models[model]
326}
327
328func (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
339type Ref struct {
340	BaseSchema
341
342	reference   string
343	definitions *Definitions
344}
345
346var _ Reference = &Ref{}
347
348func (r *Ref) Reference() string {
349	return r.reference
350}
351
352func (r *Ref) SubSchema() Schema {
353	return r.definitions.models[r.reference]
354}
355
356func (r *Ref) Accept(v SchemaVisitor) {
357	v.VisitReference(r)
358}
359
360func (r *Ref) GetName() string {
361	return fmt.Sprintf("Reference to %q", r.reference)
362}
363