k8s.io/kube-openapi@v0.0.0-20240228011516-70dd3763d340/pkg/util/proto/document_v3.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 proto
    18  
    19  import (
    20  	"fmt"
    21  	"reflect"
    22  	"strings"
    23  
    24  	openapi_v3 "github.com/google/gnostic-models/openapiv3"
    25  	"gopkg.in/yaml.v3"
    26  )
    27  
    28  // Temporary parse implementation to be used until gnostic->kube-openapi conversion
    29  // is possible.
    30  func NewOpenAPIV3Data(doc *openapi_v3.Document) (Models, error) {
    31  	definitions := Definitions{
    32  		models: map[string]Schema{},
    33  	}
    34  
    35  	schemas := doc.GetComponents().GetSchemas()
    36  	if schemas == nil {
    37  		return &definitions, nil
    38  	}
    39  
    40  	// Save the list of all models first. This will allow us to
    41  	// validate that we don't have any dangling reference.
    42  	for _, namedSchema := range schemas.GetAdditionalProperties() {
    43  		definitions.models[namedSchema.GetName()] = nil
    44  	}
    45  
    46  	// Now, parse each model. We can validate that references exists.
    47  	for _, namedSchema := range schemas.GetAdditionalProperties() {
    48  		path := NewPath(namedSchema.GetName())
    49  		val := namedSchema.GetValue()
    50  
    51  		if val == nil {
    52  			continue
    53  		}
    54  
    55  		if schema, err := definitions.ParseV3SchemaOrReference(namedSchema.GetValue(), &path); err != nil {
    56  			return nil, err
    57  		} else if schema != nil {
    58  			// Schema may be nil if we hit incompleteness in the conversion,
    59  			// but not a fatal error
    60  			definitions.models[namedSchema.GetName()] = schema
    61  		}
    62  	}
    63  
    64  	return &definitions, nil
    65  }
    66  
    67  func (d *Definitions) ParseV3SchemaReference(s *openapi_v3.Reference, path *Path) (Schema, error) {
    68  	base := &BaseSchema{
    69  		Description: s.Description,
    70  	}
    71  
    72  	if !strings.HasPrefix(s.GetXRef(), "#/components/schemas") {
    73  		// Only resolve references to components/schemas. We may add support
    74  		// later for other in-spec paths, but otherwise treat unrecognized
    75  		// refs as arbitrary/unknown values.
    76  		return &Arbitrary{
    77  			BaseSchema: *base,
    78  		}, nil
    79  	}
    80  
    81  	reference := strings.TrimPrefix(s.GetXRef(), "#/components/schemas/")
    82  	if _, ok := d.models[reference]; !ok {
    83  		return nil, newSchemaError(path, "unknown model in reference: %q", reference)
    84  	}
    85  
    86  	return &Ref{
    87  		BaseSchema: BaseSchema{
    88  			Description: s.Description,
    89  		},
    90  		reference:   reference,
    91  		definitions: d,
    92  	}, nil
    93  }
    94  
    95  func (d *Definitions) ParseV3SchemaOrReference(s *openapi_v3.SchemaOrReference, path *Path) (Schema, error) {
    96  	var schema Schema
    97  	var err error
    98  
    99  	switch v := s.GetOneof().(type) {
   100  	case *openapi_v3.SchemaOrReference_Reference:
   101  		// Any references stored in #!/components/... are bound to refer
   102  		// to external documents. This API does not support such a
   103  		// feature.
   104  		//
   105  		// In the weird case that this is a reference to a schema that is
   106  		// not external, we attempt to parse anyway
   107  		schema, err = d.ParseV3SchemaReference(v.Reference, path)
   108  	case *openapi_v3.SchemaOrReference_Schema:
   109  		schema, err = d.ParseSchemaV3(v.Schema, path)
   110  	default:
   111  		panic("unexpected type")
   112  	}
   113  
   114  	return schema, err
   115  }
   116  
   117  // ParseSchema creates a walkable Schema from an openapi v3 schema. While
   118  // this function is public, it doesn't leak through the interface.
   119  func (d *Definitions) ParseSchemaV3(s *openapi_v3.Schema, path *Path) (Schema, error) {
   120  	switch s.GetType() {
   121  	case object:
   122  		for _, extension := range s.GetSpecificationExtension() {
   123  			if extension.Name == "x-kubernetes-group-version-kind" {
   124  				// Objects with x-kubernetes-group-version-kind are always top
   125  				// level types.
   126  				return d.parseV3Kind(s, path)
   127  			}
   128  		}
   129  
   130  		if len(s.GetProperties().GetAdditionalProperties()) > 0 {
   131  			return d.parseV3Kind(s, path)
   132  		}
   133  		return d.parseV3Map(s, path)
   134  	case array:
   135  		return d.parseV3Array(s, path)
   136  	case String, Number, Integer, Boolean:
   137  		return d.parseV3Primitive(s, path)
   138  	default:
   139  		return d.parseV3Arbitrary(s, path)
   140  	}
   141  }
   142  
   143  func (d *Definitions) parseV3Kind(s *openapi_v3.Schema, path *Path) (Schema, error) {
   144  	if s.GetType() != object {
   145  		return nil, newSchemaError(path, "invalid object type")
   146  	} else if s.GetProperties() == nil {
   147  		return nil, newSchemaError(path, "object doesn't have properties")
   148  	}
   149  
   150  	fields := map[string]Schema{}
   151  	fieldOrder := []string{}
   152  
   153  	for _, namedSchema := range s.GetProperties().GetAdditionalProperties() {
   154  		var err error
   155  		name := namedSchema.GetName()
   156  		path := path.FieldPath(name)
   157  		fields[name], err = d.ParseV3SchemaOrReference(namedSchema.GetValue(), &path)
   158  		if err != nil {
   159  			return nil, err
   160  		}
   161  		fieldOrder = append(fieldOrder, name)
   162  	}
   163  
   164  	base, err := d.parseV3BaseSchema(s, path)
   165  	if err != nil {
   166  		return nil, err
   167  	}
   168  
   169  	return &Kind{
   170  		BaseSchema:     *base,
   171  		RequiredFields: s.GetRequired(),
   172  		Fields:         fields,
   173  		FieldOrder:     fieldOrder,
   174  	}, nil
   175  }
   176  
   177  func (d *Definitions) parseV3Arbitrary(s *openapi_v3.Schema, path *Path) (Schema, error) {
   178  	base, err := d.parseV3BaseSchema(s, path)
   179  	if err != nil {
   180  		return nil, err
   181  	}
   182  	return &Arbitrary{
   183  		BaseSchema: *base,
   184  	}, nil
   185  }
   186  
   187  func (d *Definitions) parseV3Primitive(s *openapi_v3.Schema, path *Path) (Schema, error) {
   188  	switch s.GetType() {
   189  	case String: // do nothing
   190  	case Number: // do nothing
   191  	case Integer: // do nothing
   192  	case Boolean: // do nothing
   193  	default:
   194  		// Unsupported primitive type. Treat as arbitrary type
   195  		return d.parseV3Arbitrary(s, path)
   196  	}
   197  
   198  	base, err := d.parseV3BaseSchema(s, path)
   199  	if err != nil {
   200  		return nil, err
   201  	}
   202  
   203  	return &Primitive{
   204  		BaseSchema: *base,
   205  		Type:       s.GetType(),
   206  		Format:     s.GetFormat(),
   207  	}, nil
   208  }
   209  
   210  func (d *Definitions) parseV3Array(s *openapi_v3.Schema, path *Path) (Schema, error) {
   211  	if s.GetType() != array {
   212  		return nil, newSchemaError(path, `array should have type "array"`)
   213  	} else if len(s.GetItems().GetSchemaOrReference()) != 1 {
   214  		// This array can have multiple types in it (or no types at all)
   215  		// This is not supported by this conversion.
   216  		// Just return an arbitrary type
   217  		return d.parseV3Arbitrary(s, path)
   218  	}
   219  
   220  	sub, err := d.ParseV3SchemaOrReference(s.GetItems().GetSchemaOrReference()[0], path)
   221  	if err != nil {
   222  		return nil, err
   223  	}
   224  
   225  	base, err := d.parseV3BaseSchema(s, path)
   226  	if err != nil {
   227  		return nil, err
   228  	}
   229  	return &Array{
   230  		BaseSchema: *base,
   231  		SubType:    sub,
   232  	}, nil
   233  }
   234  
   235  // We believe the schema is a map, verify and return a new schema
   236  func (d *Definitions) parseV3Map(s *openapi_v3.Schema, path *Path) (Schema, error) {
   237  	if s.GetType() != object {
   238  		return nil, newSchemaError(path, "invalid object type")
   239  	}
   240  	var sub Schema
   241  
   242  	switch p := s.GetAdditionalProperties().GetOneof().(type) {
   243  	case *openapi_v3.AdditionalPropertiesItem_Boolean:
   244  		// What does this boolean even mean?
   245  		base, err := d.parseV3BaseSchema(s, path)
   246  		if err != nil {
   247  			return nil, err
   248  		}
   249  		sub = &Arbitrary{
   250  			BaseSchema: *base,
   251  		}
   252  	case *openapi_v3.AdditionalPropertiesItem_SchemaOrReference:
   253  		if schema, err := d.ParseV3SchemaOrReference(p.SchemaOrReference, path); err != nil {
   254  			return nil, err
   255  		} else {
   256  			sub = schema
   257  		}
   258  	case nil:
   259  		// no subtype?
   260  		sub = &Arbitrary{}
   261  	default:
   262  		panic("unrecognized type " + reflect.TypeOf(p).Name())
   263  	}
   264  
   265  	base, err := d.parseV3BaseSchema(s, path)
   266  	if err != nil {
   267  		return nil, err
   268  	}
   269  	return &Map{
   270  		BaseSchema: *base,
   271  		SubType:    sub,
   272  	}, nil
   273  }
   274  
   275  func parseV3Interface(def *yaml.Node) (interface{}, error) {
   276  	if def == nil {
   277  		return nil, nil
   278  	}
   279  	var i interface{}
   280  	if err := def.Decode(&i); err != nil {
   281  		return nil, err
   282  	}
   283  	return i, nil
   284  }
   285  
   286  func (d *Definitions) parseV3BaseSchema(s *openapi_v3.Schema, path *Path) (*BaseSchema, error) {
   287  	if s == nil {
   288  		return nil, fmt.Errorf("cannot initialize BaseSchema from nil")
   289  	}
   290  
   291  	def, err := parseV3Interface(s.GetDefault().ToRawInfo())
   292  	if err != nil {
   293  		return nil, err
   294  	}
   295  
   296  	return &BaseSchema{
   297  		Description: s.GetDescription(),
   298  		Default:     def,
   299  		Extensions:  SpecificationExtensionToMap(s.GetSpecificationExtension()),
   300  		Path:        *path,
   301  	}, nil
   302  }
   303  
   304  func SpecificationExtensionToMap(e []*openapi_v3.NamedAny) map[string]interface{} {
   305  	values := map[string]interface{}{}
   306  
   307  	for _, na := range e {
   308  		if na.GetName() == "" || na.GetValue() == nil {
   309  			continue
   310  		}
   311  		if na.GetValue().GetYaml() == "" {
   312  			continue
   313  		}
   314  		var value interface{}
   315  		err := yaml.Unmarshal([]byte(na.GetValue().GetYaml()), &value)
   316  		if err != nil {
   317  			continue
   318  		}
   319  
   320  		values[na.GetName()] = value
   321  	}
   322  
   323  	return values
   324  }