diff --git a/kyaml/openapi/openapi.go b/kyaml/openapi/openapi.go index 9cb8bfb19..7587968f1 100644 --- a/kyaml/openapi/openapi.go +++ b/kyaml/openapi/openapi.go @@ -4,15 +4,26 @@ package openapi import ( + "encoding/json" + "fmt" "sync" "github.com/go-openapi/spec" + "sigs.k8s.io/kustomize/kyaml/errors" "sigs.k8s.io/kustomize/kyaml/yaml" ) -var setup sync.Once -var schema spec.Schema -var schemaByResourceType map[yaml.TypeMeta]*spec.Schema +// 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 { @@ -26,13 +37,95 @@ type ResourceSchema struct { // as metadata, replicas and spec.template.spec func SchemaForResourceType(t yaml.TypeMeta) *ResourceSchema { initSchema() - rs, found := schemaByResourceType[t] + rs, found := globalSchema.schemaByResourceType[t] if !found { return nil } return &ResourceSchema{Schema: rs} } +// AddSchema parses s, and adds definitions from s to the global schema. +func AddSchema(s []byte) (*spec.Schema, error) { + return parse(s) +} + +// 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 = false +} + // Elements returns the Schema for the elements of an array. func (r *ResourceSchema) Elements() *ResourceSchema { // load the schema from swagger.json @@ -44,7 +137,7 @@ func (r *ResourceSchema) Elements() *ResourceSchema { } s := *r.Schema.Items.Schema for s.Ref.String() != "" { - sc, e := spec.ResolveRef(rootSchema(), &s.Ref) + sc, e := Resolve(&s.Ref) if e != nil { return nil } @@ -96,7 +189,7 @@ func (r *ResourceSchema) Field(field string) *ResourceSchema { // resolve the reference to the Schema if the Schema has one for s.Ref.String() != "" { - sc, e := spec.ResolveRef(rootSchema(), &s.Ref) + sc, e := Resolve(&s.Ref) if e != nil { return nil } @@ -148,53 +241,57 @@ const ( // initSchema parses the json schema func initSchema() { - setup.Do(func() { - // initialize the map - schemaByResourceType = map[yaml.TypeMeta]*spec.Schema{} + globalSchema.setup.Do(func() { + if globalSchema.noUseBuiltInSchema { + // don't parse the built in schema + return + } // parse the swagger, this should never fail - parse(MustAsset(openAPIAssetName)) - - // TODO(pwittrock): add support for parsing additional schemas from - // environment variables, files or other sources + 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) { - if err := schema.UnmarshalJSON(b); err != nil { - panic(err) - } - // index the schema definitions so we can lookup them up for Resources - for k := range schema.Definitions { - // index by GVK, if no GVK is found then it is the schema for a subfield - // of a Resource - d := schema.Definitions[k] - 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 - } +func parse(b []byte) (*spec.Schema, error) { + var sc spec.Schema - // build the index key and save it - g := m[groupKey].(string) - apiVersion := m[versionKey].(string) - if g != "" { - apiVersion = g + "/" + apiVersion + 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 } - schemaByResourceType[yaml.TypeMeta{Kind: m[kindKey].(string), APIVersion: apiVersion}] = &d + 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 &schema + return &globalSchema.schema } diff --git a/kyaml/openapi/openapi_test.go b/kyaml/openapi/openapi_test.go index 8318dbc7e..04bf26f0b 100644 --- a/kyaml/openapi/openapi_test.go +++ b/kyaml/openapi/openapi_test.go @@ -1,18 +1,73 @@ // Copyright 2019 The Kubernetes Authors. // SPDX-License-Identifier: Apache-2.0 -package openapi_test +package openapi import ( + "fmt" "testing" "github.com/stretchr/testify/assert" - "sigs.k8s.io/kustomize/kyaml/openapi" "sigs.k8s.io/kustomize/kyaml/yaml" ) +func TestAddSchema(t *testing.T) { + // reset package vars + globalSchema = openapiData{} + + _, err := AddSchema(additionalSchema) + if !assert.NoError(t, err) { + t.FailNow() + } + s, err := GetSchema(`{"$ref": "#/definitions/io.k8s.config.setters.replicas"}`) + if !assert.Greater(t, len(globalSchema.schema.Definitions), 200) { + t.FailNow() + } + if !assert.NoError(t, err) { + t.FailNow() + } + assert.Equal(t, `map[x-kustomize:map[setBy:Jane setter:map[name:replicas value:5]]]`, + fmt.Sprintf("%v", s.Schema.Extensions)) +} + +func TestNoUseBuiltInSchema_AddSchema(t *testing.T) { + // reset package vars + globalSchema = openapiData{} + + SuppressBuiltInSchemaUse() + _, err := AddSchema(additionalSchema) + if !assert.NoError(t, err) { + t.FailNow() + } + s, err := GetSchema(`{"$ref": "#/definitions/io.k8s.config.setters.replicas"}`) + if !assert.Greater(t, len(globalSchema.schema.Definitions), 1) { + t.FailNow() + } + if !assert.NoError(t, err) { + t.FailNow() + } + assert.Equal(t, `map[x-kustomize:map[setBy:Jane setter:map[name:replicas value:5]]]`, + fmt.Sprintf("%v", s.Schema.Extensions)) +} + +var additionalSchema = []byte(` +{ + "definitions": { + "io.k8s.config.setters.replicas": { + "description": "replicas description.", + "type": "integer", + "x-kustomize": {"setBy":"Jane","setter": {"name":"replicas","value":"5"}} + } + }, + "invalid": "field" +} +`) + func TestSchemaForResourceType(t *testing.T) { - s := openapi.SchemaForResourceType( + // reset package vars + globalSchema = openapiData{} + + s := SchemaForResourceType( yaml.TypeMeta{APIVersion: "apps/v1", Kind: "Deployment"}) if !assert.NotNil(t, s) { t.FailNow()