// Copyright 2019 The Kubernetes Authors. // SPDX-License-Identifier: Apache-2.0 package openapi import ( "encoding/json" "fmt" "io/ioutil" "sync" "github.com/go-openapi/spec" "sigs.k8s.io/kustomize/kyaml/errors" "sigs.k8s.io/kustomize/kyaml/yaml" ) // globalSchema contains global state information about the openapi var globalSchema openapiData // openapiData contains the parsed openapi state. this is in a struct rather than // a list of vars so that it can be reset from tests. type openapiData struct { setup sync.Once schema spec.Schema schemaByResourceType map[yaml.TypeMeta]*spec.Schema noUseBuiltInSchema bool } // ResourceSchema wraps the OpenAPI Schema. type ResourceSchema struct { // Schema is the OpenAPI schema for a Resource or field Schema *spec.Schema } // SchemaForResourceType returns the Schema for the given Resource // TODO(pwittrock): create a version of this function that will return a schema // which can be used for duck-typed Resources -- e.g. contains common fields such // as metadata, replicas and spec.template.spec func SchemaForResourceType(t yaml.TypeMeta) *ResourceSchema { initSchema() rs, found := globalSchema.schemaByResourceType[t] if !found { return nil } return &ResourceSchema{Schema: rs} } // SupplementaryOpenAPIFieldName is the conventional field name (JSON/YAML) containing // supplementary OpenAPI definitions. const SupplementaryOpenAPIFieldName = "openAPI" // AddSchemaFromFile reads the file at path and parses the OpenAPI definitions // from the field "openAPI" func AddSchemaFromFile(path string) error { return AddSchemaFromFileUsingField(path, SupplementaryOpenAPIFieldName) } // AddSchemaFromFileUsingField reads the file at path and parses the OpenAPI definitions // from the specified field. If field is the empty string, use the whole document as // OpenAPI. func AddSchemaFromFileUsingField(path, field string) error { b, err := ioutil.ReadFile(path) if err != nil { return err } // parse the yaml file (json is a subset of yaml, so will also parse) y, err := yaml.Parse(string(b)) if err != nil { return err } if field != "" { // get the field containing the openAPI m := y.Field(field) if yaml.IsFieldEmpty(m) { // doesn't contain openAPI definitions return nil } y = m.Value } oAPI, err := y.String() if err != nil { return err } // convert the yaml openAPI to a JSON string by unmarshalling it to an // interface{} and the marshalling it to a string var o interface{} err = yaml.Unmarshal([]byte(oAPI), &o) if err != nil { return err } j, err := json.Marshal(o) if err != nil { return err } // add the json schema to the global schema _, err = AddSchema(j) if err != nil { return err } return nil } // AddSchema parses s, and adds definitions from s to the global schema. func AddSchema(s []byte) (*spec.Schema, error) { return parse(s) } // ResetOpenAPI resets the openapi data to empty func ResetOpenAPI() { globalSchema = openapiData{} } // AddDefinitions adds the definitions to the global schema. func AddDefinitions(definitions spec.Definitions) { // initialize values if they have not yet been set if globalSchema.schemaByResourceType == nil { globalSchema.schemaByResourceType = map[yaml.TypeMeta]*spec.Schema{} } if globalSchema.schema.Definitions == nil { globalSchema.schema.Definitions = spec.Definitions{} } // index the schema definitions so we can lookup them up for Resources for k := range definitions { // index by GVK, if no GVK is found then it is the schema for a subfield // of a Resource d := definitions[k] // copy definitions to the schema globalSchema.schema.Definitions[k] = d gvk, found := d.VendorExtensible.Extensions[kubernetesGVKExtensionKey] if !found { continue } // cast the extension to a []map[string]string exts, ok := gvk.([]interface{}) if !ok || len(exts) != 1 { continue } m, ok := exts[0].(map[string]interface{}) if !ok { continue } // build the index key and save it g := m[groupKey].(string) apiVersion := m[versionKey].(string) if g != "" { apiVersion = g + "/" + apiVersion } globalSchema.schemaByResourceType[yaml.TypeMeta{Kind: m[kindKey].(string), APIVersion: apiVersion}] = &d } } // Resolve resolves the reference against the global schema func Resolve(ref *spec.Ref) (*spec.Schema, error) { return resolve(Schema(), ref) } // Schema returns the global schema func Schema() *spec.Schema { return rootSchema() } // GetSchema parses s into a ResourceSchema, resolving References within the // global schema. func GetSchema(s string) (*ResourceSchema, error) { var sc spec.Schema if err := sc.UnmarshalJSON([]byte(s)); err != nil { return nil, errors.Wrap(err) } if sc.Ref.String() != "" { r, err := Resolve(&sc.Ref) if err != nil { return nil, errors.Wrap(err) } sc = *r } return &ResourceSchema{Schema: &sc}, nil } // SuppressBuiltInSchemaUse can be called to prevent using the built-in Kubernetes // schema as part of the global schema. // Must be called before the schema is used. func SuppressBuiltInSchemaUse() { globalSchema.noUseBuiltInSchema = true } // Elements returns the Schema for the elements of an array. func (r *ResourceSchema) Elements() *ResourceSchema { // load the schema from swagger.json initSchema() if len(r.Schema.Type) != 1 || r.Schema.Type[0] != "array" { // either not an array, or array has multiple types return nil } s := *r.Schema.Items.Schema for s.Ref.String() != "" { sc, e := Resolve(&s.Ref) if e != nil { return nil } s = *sc } return &ResourceSchema{Schema: &s} } const Elements = "[]" // Lookup calls either Field or Elements for each item in the path. // If the path item is "[]", then Elements is called, otherwise // Field is called. // If any Field or Elements call returns nil, then Lookup returns // nil immediately. func (r *ResourceSchema) Lookup(path ...string) *ResourceSchema { s := r for _, p := range path { if s == nil { break } if p == Elements { s = s.Elements() continue } s = s.Field(p) } return s } // Field returns the Schema for a field. func (r *ResourceSchema) Field(field string) *ResourceSchema { // load the schema from swagger.json initSchema() // locate the Schema s, found := r.Schema.Properties[field] switch { case found: // no-op, continue with s as the schema case r.Schema.AdditionalProperties != nil && r.Schema.AdditionalProperties.Schema != nil: // map field type -- use Schema of the value // (the key doesn't matter, they all have the same value type) s = *r.Schema.AdditionalProperties.Schema default: // no Schema found from either swagger.json or line comments return nil } // resolve the reference to the Schema if the Schema has one for s.Ref.String() != "" { sc, e := Resolve(&s.Ref) if e != nil { return nil } s = *sc } // return the merged Schema return &ResourceSchema{Schema: &s} } // PatchStrategyAndKey returns the patch strategy and merge key extensions func (r *ResourceSchema) PatchStrategyAndKey() (string, string) { ps, found := r.Schema.Extensions[kubernetesPatchStrategyExtensionKey] if !found { // merge key and patch strategy must appear together return "", "" } mk, found := r.Schema.Extensions[kubernetesMergeKeyExtensionKey] if !found { // merge key and patch strategy must appear together return "", "" } return ps.(string), mk.(string) } const ( // openAPIAssetName is the name of the asset containing the statically compiled in // OpenAPI definitions for Kubernetes built-in types openAPIAssetName = "openapi/swagger.json" // kubernetesGVKExtensionKey is the key to lookup the kubernetes group version kind extension // -- the extension is an array of objects containing a gvk kubernetesGVKExtensionKey = "x-kubernetes-group-version-kind" // kubernetesMergeKeyExtensionKey is the key to lookup the kubernetes merge key extension // -- the extension is a string kubernetesMergeKeyExtensionKey = "x-kubernetes-patch-merge-key" // kubernetesPatchStrategyExtensionKey is the key to lookup the kubernetes patch strategy // extension -- the extension is a string kubernetesPatchStrategyExtensionKey = "x-kubernetes-patch-strategy" // groupKey is the key to lookup the group from the GVK extension groupKey = "group" // versionKey is the key to lookup the version from the GVK extension versionKey = "version" // kindKey is the the to lookup the kind from the GVK extension kindKey = "kind" ) // initSchema parses the json schema func initSchema() { globalSchema.setup.Do(func() { if globalSchema.noUseBuiltInSchema { // don't parse the built in schema return } // parse the swagger, this should never fail if _, err := parse(MustAsset(openAPIAssetName)); err != nil { // this should never happen panic(err) } }) } // parse parses and indexes a single json schema func parse(b []byte) (*spec.Schema, error) { var sc spec.Schema if err := sc.UnmarshalJSON(b); err != nil { return nil, errors.Wrap(err) } AddDefinitions(sc.Definitions) return &sc, nil } func resolve(root interface{}, ref *spec.Ref) (*spec.Schema, error) { res, _, err := ref.GetPointer().Get(root) if err != nil { return nil, errors.Wrap(err) } switch sch := res.(type) { case spec.Schema: return &sch, nil case *spec.Schema: return sch, nil case map[string]interface{}: b, err := json.Marshal(sch) if err != nil { return nil, err } newSch := new(spec.Schema) if err = json.Unmarshal(b, newSch); err != nil { return nil, err } return newSch, nil default: return nil, errors.Wrap(fmt.Errorf("unknown type for the resolved reference")) } } func rootSchema() *spec.Schema { initSchema() return &globalSchema.schema }