k8s.io/kube-openapi@v0.0.0-20240228011516-70dd3763d340/pkg/schemaconv/openapi.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 schemaconv
    18  
    19  import (
    20  	"errors"
    21  	"path"
    22  	"strings"
    23  
    24  	"k8s.io/kube-openapi/pkg/validation/spec"
    25  	"sigs.k8s.io/structured-merge-diff/v4/schema"
    26  )
    27  
    28  // ToSchemaFromOpenAPI converts a directory of OpenAPI schemas to an smd Schema.
    29  //   - models: a map from definition name to OpenAPI V3 structural schema for each definition.
    30  //     Key in map is used to resolve references in the schema.
    31  //   - preserveUnknownFields: flag indicating whether unknown fields in all schemas should be preserved.
    32  //   - returns: nil and an error if there is a parse error, or if schema does not satisfy a
    33  //     required structural schema invariant for conversion. If no error, returns
    34  //     a new smd schema.
    35  //
    36  // Schema should be validated as structural before using with this function, or
    37  // there may be information lost.
    38  func ToSchemaFromOpenAPI(models map[string]*spec.Schema, preserveUnknownFields bool) (*schema.Schema, error) {
    39  	c := convert{
    40  		preserveUnknownFields: preserveUnknownFields,
    41  		output:                &schema.Schema{},
    42  	}
    43  
    44  	for name, spec := range models {
    45  		// Skip/Ignore top-level references
    46  		if len(spec.Ref.String()) > 0 {
    47  			continue
    48  		}
    49  
    50  		var a schema.Atom
    51  
    52  		// Hard-coded schemas for now as proto_models implementation functions.
    53  		// https://github.com/kubernetes/kube-openapi/issues/364
    54  		if name == quantityResource {
    55  			a = schema.Atom{
    56  				Scalar: untypedDef.Atom.Scalar,
    57  			}
    58  		} else if name == rawExtensionResource {
    59  			a = untypedDef.Atom
    60  		} else {
    61  			c2 := c.push(name, &a)
    62  			c2.visitSpec(spec)
    63  			c.pop(c2)
    64  		}
    65  
    66  		c.insertTypeDef(name, a)
    67  	}
    68  
    69  	if len(c.errorMessages) > 0 {
    70  		return nil, errors.New(strings.Join(c.errorMessages, "\n"))
    71  	}
    72  
    73  	c.addCommonTypes()
    74  	return c.output, nil
    75  }
    76  
    77  func (c *convert) visitSpec(m *spec.Schema) {
    78  	// Check if this schema opts its descendants into preserve-unknown-fields
    79  	if p, ok := m.Extensions["x-kubernetes-preserve-unknown-fields"]; ok && p == true {
    80  		c.preserveUnknownFields = true
    81  	}
    82  	a := c.top()
    83  	*a = c.parseSchema(m)
    84  }
    85  
    86  func (c *convert) parseSchema(m *spec.Schema) schema.Atom {
    87  	// k8s-generated OpenAPI specs have historically used only one value for
    88  	// type and starting with OpenAPIV3 it is only allowed to be
    89  	// a single string.
    90  	typ := ""
    91  	if len(m.Type) > 0 {
    92  		typ = m.Type[0]
    93  	}
    94  
    95  	// Structural Schemas produced by kubernetes follow very specific rules which
    96  	// we can use to infer the SMD type:
    97  	switch typ {
    98  	case "":
    99  		// According to Swagger docs:
   100  		// https://swagger.io/docs/specification/data-models/data-types/#any
   101  		//
   102  		// If no type is specified, it is equivalent to accepting any type.
   103  		return schema.Atom{
   104  			Scalar: ptr(schema.Scalar("untyped")),
   105  			List:   c.parseList(m),
   106  			Map:    c.parseObject(m),
   107  		}
   108  
   109  	case "object":
   110  		return schema.Atom{
   111  			Map: c.parseObject(m),
   112  		}
   113  	case "array":
   114  		return schema.Atom{
   115  			List: c.parseList(m),
   116  		}
   117  	case "integer", "boolean", "number", "string":
   118  		return convertPrimitive(typ, m.Format)
   119  	default:
   120  		c.reportError("unrecognized type: '%v'", typ)
   121  		return schema.Atom{
   122  			Scalar: ptr(schema.Scalar("untyped")),
   123  		}
   124  	}
   125  }
   126  
   127  func (c *convert) makeOpenAPIRef(specSchema *spec.Schema) schema.TypeRef {
   128  	refString := specSchema.Ref.String()
   129  
   130  	// Special-case handling for $ref stored inside a single-element allOf
   131  	if len(refString) == 0 && len(specSchema.AllOf) == 1 && len(specSchema.AllOf[0].Ref.String()) > 0 {
   132  		refString = specSchema.AllOf[0].Ref.String()
   133  	}
   134  
   135  	if _, n := path.Split(refString); len(n) > 0 {
   136  		//!TODO: Refactor the field ElementRelationship override
   137  		// we can generate the types with overrides ahead of time rather than
   138  		// requiring the hacky runtime support
   139  		// (could just create a normalized key struct containing all customizations
   140  		// 	to deduplicate)
   141  		mapRelationship, err := getMapElementRelationship(specSchema.Extensions)
   142  		if err != nil {
   143  			c.reportError(err.Error())
   144  		}
   145  
   146  		if len(mapRelationship) > 0 {
   147  			return schema.TypeRef{
   148  				NamedType:           &n,
   149  				ElementRelationship: &mapRelationship,
   150  			}
   151  		}
   152  
   153  		return schema.TypeRef{
   154  			NamedType: &n,
   155  		}
   156  
   157  	}
   158  	var inlined schema.Atom
   159  
   160  	// compute the type inline
   161  	c2 := c.push("inlined in "+c.currentName, &inlined)
   162  	c2.preserveUnknownFields = c.preserveUnknownFields
   163  	c2.visitSpec(specSchema)
   164  	c.pop(c2)
   165  
   166  	return schema.TypeRef{
   167  		Inlined: inlined,
   168  	}
   169  }
   170  
   171  func (c *convert) parseObject(s *spec.Schema) *schema.Map {
   172  	var fields []schema.StructField
   173  	for name, member := range s.Properties {
   174  		fields = append(fields, schema.StructField{
   175  			Name:    name,
   176  			Type:    c.makeOpenAPIRef(&member),
   177  			Default: member.Default,
   178  		})
   179  	}
   180  
   181  	// AdditionalProperties informs the schema of any "unknown" keys
   182  	// Unknown keys are enforced by the ElementType field.
   183  	elementType := func() schema.TypeRef {
   184  		if s.AdditionalProperties == nil {
   185  			// According to openAPI spec, an object without properties and without
   186  			// additionalProperties is assumed to be a free-form object.
   187  			if c.preserveUnknownFields || len(s.Properties) == 0 {
   188  				return schema.TypeRef{
   189  					NamedType: &deducedName,
   190  				}
   191  			}
   192  
   193  			// If properties are specified, do not implicitly allow unknown
   194  			// fields
   195  			return schema.TypeRef{}
   196  		} else if s.AdditionalProperties.Schema != nil {
   197  			// Unknown fields use the referred schema
   198  			return c.makeOpenAPIRef(s.AdditionalProperties.Schema)
   199  
   200  		} else if s.AdditionalProperties.Allows {
   201  			// A boolean instead of a schema was provided. Deduce the
   202  			// type from the value provided at runtime.
   203  			return schema.TypeRef{
   204  				NamedType: &deducedName,
   205  			}
   206  		} else {
   207  			// Additional Properties are explicitly disallowed by the user.
   208  			// Ensure element type is empty.
   209  			return schema.TypeRef{}
   210  		}
   211  	}()
   212  
   213  	relationship, err := getMapElementRelationship(s.Extensions)
   214  	if err != nil {
   215  		c.reportError(err.Error())
   216  	}
   217  
   218  	return &schema.Map{
   219  		Fields:              fields,
   220  		ElementRelationship: relationship,
   221  		ElementType:         elementType,
   222  	}
   223  }
   224  
   225  func (c *convert) parseList(s *spec.Schema) *schema.List {
   226  	relationship, mapKeys, err := getListElementRelationship(s.Extensions)
   227  	if err != nil {
   228  		c.reportError(err.Error())
   229  	}
   230  	elementType := func() schema.TypeRef {
   231  		if s.Items != nil {
   232  			if s.Items.Schema == nil || s.Items.Len() != 1 {
   233  				c.reportError("structural schema arrays must have exactly one member subtype")
   234  				return schema.TypeRef{
   235  					NamedType: &deducedName,
   236  				}
   237  			}
   238  
   239  			subSchema := s.Items.Schema
   240  			if subSchema == nil {
   241  				subSchema = &s.Items.Schemas[0]
   242  			}
   243  			return c.makeOpenAPIRef(subSchema)
   244  		} else if len(s.Type) > 0 && len(s.Type[0]) > 0 {
   245  			c.reportError("`items` must be specified on arrays")
   246  		}
   247  
   248  		// A list with no items specified is treated as "untyped".
   249  		return schema.TypeRef{
   250  			NamedType: &untypedName,
   251  		}
   252  
   253  	}()
   254  
   255  	return &schema.List{
   256  		ElementRelationship: relationship,
   257  		Keys:                mapKeys,
   258  		ElementType:         elementType,
   259  	}
   260  }