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  }