From 2e143969e5cdf3871766ec878da4e85b1d841631 Mon Sep 17 00:00:00 2001 From: natasha41575 Date: Mon, 14 Feb 2022 12:40:47 -0800 Subject: [PATCH] start a new openapi library using the gnostic package --- .../internal/openapignostic/openapignostic.go | 166 ++++++++++++++++++ .../openapignostic/openapignostic_test.go | 145 +++++++++++++++ kyaml/openapi/kustomizationapi/swagger.go | 18 +- kyaml/openapi/kustomizationapi/swagger.json | 8 +- 4 files changed, 326 insertions(+), 11 deletions(-) create mode 100644 kyaml/openapi/internal/openapignostic/openapignostic.go create mode 100644 kyaml/openapi/internal/openapignostic/openapignostic_test.go diff --git a/kyaml/openapi/internal/openapignostic/openapignostic.go b/kyaml/openapi/internal/openapignostic/openapignostic.go new file mode 100644 index 0000000000..ea6c4e99bb --- /dev/null +++ b/kyaml/openapi/internal/openapignostic/openapignostic.go @@ -0,0 +1,166 @@ +// Copyright 2022 The Kubernetes Authors. +// SPDX-License-Identifier: Apache-2.0 + +// TODO: This should eventually replace the openapi package when it is complete. +package openapignostic + +import ( + "fmt" + "path/filepath" + "strings" + + openapi_v2 "github.com/googleapis/gnostic/openapiv2" + "sigs.k8s.io/kustomize/kyaml/errors" + "sigs.k8s.io/kustomize/kyaml/openapi/kubernetesapi" + "sigs.k8s.io/kustomize/kyaml/openapi/kustomizationapi" + "sigs.k8s.io/kustomize/kyaml/yaml" +) + +// openApiSchema 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 openApiSchema struct { + // schema holds the OpenAPI schema data + schema openapi_v2.Document + + // schemaForResourceType is a map of Resource types to their schemas + schemaByResourceType map[yaml.TypeMeta]*openapi_v2.NamedSchema + + // namespaceabilityByResourceType stores whether a given Resource type + // is namespaceable or not + namespaceabilityByResourceType map[yaml.TypeMeta]bool + + // noUseBuiltInSchema stores whether we want to prevent using the built-n + // Kubernetes schema as part of the global schema + noUseBuiltInSchema bool + + // schemaInit stores whether or not we've parsed the schema already, + // so that we only reparse the schema when necessary (to speed up performance) + schemaInit bool +} + +var globalOpenApiSchema openApiSchema + +// parseBuiltin calls parseDocument to parse the json schemas +func parseBuiltin(version string) { + if globalOpenApiSchema.noUseBuiltInSchema { + // don't parse the built in schema + return + } + + // parse the swagger, this should never fail + assetName := filepath.Join( + "kubernetesapi", + version, + "swagger.json") + + if err := parseDocument(kubernetesapi.OpenAPIMustAsset[version](assetName)); err != nil { + // this should never happen + panic(err) + } + + if err := parseDocument(kustomizationapi.MustAsset("kustomizationapi/swagger.json")); err != nil { + // this should never happen + panic(err) + } +} + +// parseDocument parses and indexes a single json schema +func parseDocument(b []byte) error { + doc, err := openapi_v2.ParseDocument(b) + if err != nil { + return errors.Wrap(fmt.Errorf("parsing document error: %w", err)) + } + AddOpenApiDefinitions(doc.Definitions) + findNamespaceabilityFromOpenApi(doc.Paths) + + return nil +} + +// SchemaForResourceType returns the schema for a provided GVK +func SchemaForResourceType(meta yaml.TypeMeta) *openapi_v2.NamedSchema{ + return globalOpenApiSchema.schemaByResourceType[meta] +} + +// AddDefinitions adds the definitions to the global schema. +func AddOpenApiDefinitions(definitions *openapi_v2.Definitions) { + // initialize values if they have not yet been set + if globalOpenApiSchema.schemaByResourceType == nil { + globalOpenApiSchema.schemaByResourceType = map[yaml.TypeMeta]*openapi_v2.NamedSchema{} + } + if globalOpenApiSchema.schema.Definitions == nil { + globalOpenApiSchema.schema.Definitions = &openapi_v2.Definitions{} + } + + props := definitions.AdditionalProperties + // index the schema definitions so we can lookup them up for Resources + for k := range props { + // index by GVK, if no GVK is found then it is the schema for a subfield + // of a Resource + d := props[k] + + // copy definitions to the schema + globalOpenApiSchema.schema.Definitions.AdditionalProperties = append(globalOpenApiSchema.schema.Definitions.AdditionalProperties, d) + + for _, e := range d.GetValue().GetVendorExtension() { + if e.Name == "x-kubernetes-group-version-kind" { + var exts []map[string]string + if err := yaml.Unmarshal([]byte(e.GetValue().GetYaml()), &exts); err != nil { + continue + } + for _, gvk := range exts { + typeMeta := yaml.TypeMeta{ + APIVersion: strings.Trim(strings.Join([]string{gvk["group"], gvk["version"]}, "/"), "/"), + Kind: gvk["kind"], + } + globalOpenApiSchema.schemaByResourceType[typeMeta] = d + } + } + } + } +} + +// findNamespaceability looks at the api paths for the resource to determine +// if it is cluster-scoped or namespace-scoped. The gvk of the resource +// for each path is found by looking at the x-kubernetes-group-version-kind +// extension. If a path exists for the resource that contains a namespace path +// parameter, the resource is namespace-scoped. +func findNamespaceabilityFromOpenApi(paths *openapi_v2.Paths) { + if globalOpenApiSchema.namespaceabilityByResourceType == nil { + globalOpenApiSchema.namespaceabilityByResourceType = make(map[yaml.TypeMeta]bool) + } + if paths == nil { + return + } + for _, p := range paths.Path { + path, pathInfo := p.GetName(), p.GetValue() + if pathInfo.GetGet() == nil { + continue + } + for _, e := range pathInfo.GetGet().GetVendorExtension() { + if e.Name == "x-kubernetes-group-version-kind" { + var gvk map[string]string + if err := yaml.Unmarshal([]byte(e.GetValue().GetYaml()), &gvk); err != nil { + continue + } + typeMeta := yaml.TypeMeta{ + APIVersion: strings.Trim(strings.Join([]string{gvk["group"], gvk["version"]}, "/"), "/"), + Kind: gvk["kind"], + } + if strings.Contains(path, "namespaces/{namespace}") { + // if we find a namespace path parameter, we just update the map + // directly + globalOpenApiSchema.namespaceabilityByResourceType[typeMeta] = true + } else if _, found := globalOpenApiSchema.namespaceabilityByResourceType[typeMeta]; !found { + // if the resource doesn't have the namespace path parameter, we + // only add it to the map if it doesn't already exist. + globalOpenApiSchema.namespaceabilityByResourceType[typeMeta] = false + } + } + } + } +} + +// ResetOpenAPI resets the openapi data to empty +func ResetSchema() { + globalOpenApiSchema = openApiSchema{} +} diff --git a/kyaml/openapi/internal/openapignostic/openapignostic_test.go b/kyaml/openapi/internal/openapignostic/openapignostic_test.go new file mode 100644 index 0000000000..5ec267c589 --- /dev/null +++ b/kyaml/openapi/internal/openapignostic/openapignostic_test.go @@ -0,0 +1,145 @@ +// Copyright 2022 The Kubernetes Authors. +// SPDX-License-Identifier: Apache-2.0 + +package openapignostic + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "sigs.k8s.io/kustomize/kyaml/yaml" +) + +func TestAddOpenApiDefinitions(t *testing.T) { + parseBuiltin("v1212") + // The length here may change when the builtin openapi schema is updated. + assert.Equal(t, len(globalOpenApiSchema.schema.Definitions.AdditionalProperties), 623) + namedSchema0 := globalOpenApiSchema.schema.Definitions.AdditionalProperties[0] + assert.Equal(t, namedSchema0.GetName(), "io.k8s.api.admissionregistration.v1.MutatingWebhook") +} + +func TestSchemaForResourceType(t *testing.T) { + ResetSchema() + parseBuiltin("v1212") + + s := SchemaForResourceType( + yaml.TypeMeta{APIVersion: "apps/v1", Kind: "Deployment"}) + if !assert.NotNil(t, s) { + t.FailNow() + } + assert.Equal(t, s.GetName(), "io.k8s.api.apps.v1.Deployment") + assert.Contains(t, s.GetValue().String(), "description:\"Deployment enables declarative updates for Pods and ReplicaSets.\"") +} + + +func TestFindNamespaceability_builtin(t *testing.T) { + testCases := []struct { + name string + typeMeta yaml.TypeMeta + expectIsFound bool + expectIsNamespaced bool + }{ + { + name: "namespacescoped resource", + typeMeta: yaml.TypeMeta{ + APIVersion: "apps/v1", + Kind: "Deployment", + }, + expectIsFound: true, + expectIsNamespaced: true, + }, + { + name: "clusterscoped resource", + typeMeta: yaml.TypeMeta{ + APIVersion: "v1", + Kind: "Namespace", + }, + expectIsFound: true, + expectIsNamespaced: false, + }, + { + name: "unknown resource", + typeMeta: yaml.TypeMeta{ + APIVersion: "custom.io/v1", + Kind: "Custom", + }, + expectIsFound: false, + }, + } + + for i := range testCases { + test := testCases[i] + t.Run(test.name, func(t *testing.T) { + ResetSchema() + parseBuiltin("v1212") + + isNamespaceable, isFound := namespaceabilityFromSchema(test.typeMeta) + + if !test.expectIsFound { + assert.False(t, isFound) + return + } + assert.True(t, isFound) + assert.Equal(t, test.expectIsNamespaced, isNamespaceable) + }) + } +} + +func TestFindNamespaceability_custom(t *testing.T) { + ResetSchema() + err := parseDocument([]byte(` +{ + "definitions": {}, + "paths": { + "/apis/custom.io/v1/namespaces/{namespace}/customs/{name}": { + "get": { + "x-kubernetes-action": "get", + "x-kubernetes-group-version-kind": { + "group": "custom.io", + "kind": "Custom", + "version": "v1" + }, + "responses": [] + } + }, + "/apis/custom.io/v1/clustercustoms": { + "get": { + "x-kubernetes-action": "get", + "x-kubernetes-group-version-kind": { + "group": "custom.io", + "kind": "ClusterCustom", + "version": "v1" + }, + "responses": [] + } + } + }, + "info": { + "title": "Kustomization", + "version": "v1beta1" + }, + "swagger": "2.0", + "paths": {} +} +`)) + assert.NoError(t, err) + + isNamespaceable, isFound := namespaceabilityFromSchema(yaml.TypeMeta{ + APIVersion: "custom.io/v1", + Kind: "ClusterCustom", + }) + assert.True(t, isFound) + assert.False(t, isNamespaceable) + + isNamespaceable, isFound = namespaceabilityFromSchema(yaml.TypeMeta{ + APIVersion: "custom.io/v1", + Kind: "Custom", + }) + assert.True(t, isFound) + assert.True(t, isNamespaceable) +} + +func namespaceabilityFromSchema(typeMeta yaml.TypeMeta) (bool, bool) { + isNamespaceScoped, found := globalOpenApiSchema.namespaceabilityByResourceType[typeMeta] + return isNamespaceScoped, found +} diff --git a/kyaml/openapi/kustomizationapi/swagger.go b/kyaml/openapi/kustomizationapi/swagger.go index 4f4ec3e4ac..4d329001be 100644 --- a/kyaml/openapi/kustomizationapi/swagger.go +++ b/kyaml/openapi/kustomizationapi/swagger.go @@ -1,8 +1,6 @@ -// Copyright 2019 The Kubernetes Authors. -// SPDX-License-Identifier: Apache-2.0 +// Code generated by go-bindata. (@generated) DO NOT EDIT. -// Code generated for package kustomizationapi by go-bindata DO NOT EDIT. (@generated) -// sources: +//Package kustomizationapi generated by go-bindata.// sources: // kustomizationapi/swagger.json package kustomizationapi @@ -21,7 +19,7 @@ import ( func bindataRead(data []byte, name string) ([]byte, error) { gz, err := gzip.NewReader(bytes.NewBuffer(data)) if err != nil { - return nil, fmt.Errorf("Read %q: %v", name, err) + return nil, fmt.Errorf("read %q: %v", name, err) } var buf bytes.Buffer @@ -29,7 +27,7 @@ func bindataRead(data []byte, name string) ([]byte, error) { clErr := gz.Close() if err != nil { - return nil, fmt.Errorf("Read %q: %v", name, err) + return nil, fmt.Errorf("read %q: %v", name, err) } if clErr != nil { return nil, err @@ -65,7 +63,7 @@ func (fi bindataFileInfo) Mode() os.FileMode { return fi.mode } -// Mode return file modify time +// ModTime return file modify time func (fi bindataFileInfo) ModTime() time.Time { return fi.modTime } @@ -80,7 +78,7 @@ func (fi bindataFileInfo) Sys() interface{} { return nil } -var _kustomizationapiSwaggerJson = []byte("\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\xe4\x56\xc1\x6e\xdb\x30\x0c\xbd\xe7\x2b\x04\x6d\xc7\xd8\x45\x6e\x43\x6e\xc3\x0e\x3b\x14\x05\x0a\x74\xb7\xa1\x07\xc6\xa1\x5d\xce\x8e\xa4\x51\xb4\xb1\x6c\xc8\xbf\x0f\xd6\x62\xd7\x4a\xec\x75\x0b\x1a\xac\x4b\x0f\x06\x0c\x99\x7c\x4f\xe4\x7b\x24\xfc\x63\xa6\x94\x5e\x63\x4e\x86\x84\xac\xf1\x7a\xa9\xda\x23\xa5\x34\xd9\xb4\x7c\xe7\x53\x70\x94\x82\x73\x3e\x6d\x16\xe9\x07\x6b\x72\x2a\x6e\xc0\xbd\xe7\xe2\x31\x52\x29\xed\xd8\x3a\x64\x21\x1c\x9e\x2a\xa5\x3f\xa2\x41\x06\xb1\x7c\x90\x10\x3e\xbe\x65\xcc\xf5\x52\xe9\x37\x57\x03\xfe\xab\x11\xda\x18\xa5\x87\xd8\xed\xdf\x76\xf3\xee\x1a\xb0\x5e\x07\x14\xa8\x6e\x87\x17\xca\xa1\xf2\xd8\x07\xc9\xd6\x61\x4b\x6b\x57\x5f\x30\x13\xdd\x9f\x7f\x4b\xca\x7a\x85\x6c\x50\xd0\x27\x05\xdb\xda\x25\x0d\xb2\x27\x6b\x92\x92\xcc\x5a\x2f\xd5\xe7\x9e\x3a\xaa\x23\xc4\xb6\x88\x65\xed\xc5\x6e\xe8\x3b\xa6\x59\x68\x54\x28\x84\x6c\x4f\x11\xa2\xf7\x58\x3a\xee\x65\x14\xb2\xa7\x6d\xa3\x9a\xc5\x0a\x05\x16\xc7\x45\xdf\xcf\x06\xa5\x8f\x69\x75\x87\x19\xa3\xbc\x0c\xa1\x1e\xab\xeb\xba\x1f\xe1\x77\x8a\x78\x61\x32\xc5\xa5\x08\x3c\x10\xe0\xf9\xd5\x9d\xd2\x6b\x52\x60\x03\x1b\xf4\x0e\xb2\x3f\x6f\xfe\x3c\x4e\x3e\x25\x6f\x85\x0f\xd0\x90\xe5\x53\x72\xaf\x9b\x5b\x20\xbe\xb3\x35\x67\x78\xba\x23\x63\x94\x0b\x71\x56\x2c\xfe\xf3\x9b\xeb\x7a\x7f\x19\x90\x5f\x50\xbd\xb9\x18\xbf\xd6\xc4\x18\x17\xa4\x3f\x6d\x1d\xde\xa0\x40\xc7\x74\x3f\x7f\xca\x8c\x59\xb7\xfb\xfa\x4a\x0e\x05\x26\xc1\xcd\xa1\xea\x7f\xa3\x7b\xbc\x5d\x07\x20\xbb\xf9\x98\x11\x81\x19\xb6\x71\x27\x23\x4d\x1d\x48\xf6\x90\x6c\x90\x0b\x4c\x4a\xdc\xb6\x29\x61\x26\x9e\xca\xf0\xc2\x20\x58\x84\x84\x90\x3d\xee\x75\x1f\x56\xc5\xd9\x9a\x31\xd8\x44\x2f\xb2\x13\xff\xf5\x30\xc6\xc3\x72\x86\x61\x9c\xd8\x83\x93\xc3\x55\x91\x20\x43\x75\xb4\x33\x27\x5c\x34\xb5\x8b\x7f\x6f\x90\x51\x1b\xe7\x54\x1d\xaf\xea\xf3\xd3\xa2\x69\xfe\x0d\xeb\xeb\xf8\x8f\x89\x0d\x78\xaa\xc1\x67\xed\xb3\xfb\x19\x00\x00\xff\xff\x2f\x39\x79\xd0\x6e\x0c\x00\x00") +var _kustomizationapiSwaggerJson = []byte("\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\xe4\x57\x41\x6f\xd3\x4c\x10\xbd\xe7\x57\xac\xf6\xfb\x8e\xb1\x4b\x38\xa1\xdc\x10\x07\x0e\x55\xa5\x4a\xe5\x86\x7a\x98\x38\x63\x67\x88\xb3\xbb\xcc\x4e\x0c\x01\xe5\xbf\x23\x6f\x6d\xc7\x9b\xd8\x14\xa2\x46\x94\x72\xb3\xd6\x33\x6f\x76\xe6\xbd\x79\xb2\xbf\x4f\x94\xd2\x4b\xcc\xc9\x90\x90\x35\x5e\xcf\x55\x7d\xa4\x94\x26\x9b\xae\xdf\xf8\x14\x1c\xa5\xe0\x9c\x4f\xab\x59\xfa\xce\x9a\x9c\x8a\x1b\x70\x6f\xb9\x38\x44\x2a\xa5\x1d\x5b\x87\x2c\x84\xfd\x53\xa5\xf4\x7b\x34\xc8\x20\x96\x8f\x12\xc2\xcb\xff\x19\x73\x3d\x57\xfa\xbf\xab\x5e\xfd\xab\x81\xb2\x31\x4a\x07\xb1\x6f\x9e\xf6\xd3\xf6\x1a\xb0\x5c\x06\x14\x28\x6f\xfb\x17\xca\xa1\xf4\xd8\x05\xc9\xce\x61\x5d\xd6\x2e\x3e\x61\x26\xba\x3b\xff\x9a\xac\xb7\x0b\x64\x83\x82\x3e\x29\xd8\x6e\x5d\x52\x21\x7b\xb2\x26\x59\x93\x59\xea\xb9\xfa\xd8\x95\x8e\xfa\x08\xb1\x35\xe2\x7a\xeb\xc5\x6e\xe8\x1b\xa6\x59\x18\x54\x68\x84\x6c\x57\x22\x44\x37\x58\x3a\x9e\x65\x14\xd2\x94\xad\xa3\xaa\xd9\x02\x05\x66\xa7\x4d\xdf\x4f\x7a\xad\x0f\x71\x75\x87\x19\xa3\x3c\x0f\xa2\x0e\xdd\xb5\xd3\x8f\xf0\x5b\x46\xbc\x30\x99\xe2\xa5\x10\xdc\x23\xe0\xe9\xd9\x1d\xe3\x6b\x94\x60\x03\x1b\xf4\x0e\xb2\x5f\x1f\xfe\x34\x4e\x3e\x27\x6f\x81\x2b\xa8\xc8\xf2\x39\xb9\xd7\xd5\x2d\x10\xdf\xd9\x2d\x67\x78\xbe\x22\x63\x94\x17\xa2\xac\x98\xfc\xa7\x17\xd7\x75\x73\x19\x90\x07\xa8\x4e\x5c\x8c\x9f\xb7\xc4\x18\x37\xa4\x3f\xec\x1c\xde\xa0\x40\x5b\xe9\x7e\xfa\x98\x18\xb3\xd6\xfb\xba\x4e\x8e\x09\x26\xc1\xcd\x31\xeb\xbf\xc3\x7b\xec\xae\x3d\x90\xfd\x74\x48\x88\xc0\x0c\xbb\x78\x92\x11\xa7\x0e\x24\x5b\x25\x1b\xe4\x02\x93\x35\xee\xea\x94\xb0\x13\x8f\x65\x78\x61\x10\x2c\x42\x42\xc8\x1e\xd6\xba\x0f\x56\x71\xb1\x61\xf4\x9c\xe8\x59\x4e\xe2\xaf\x5e\xc6\x78\x59\x2e\xb0\x8c\x23\x3e\x38\xba\x5c\x25\x09\x32\x94\x27\x9e\x39\xa2\xa2\x31\x2f\xfe\xb9\x40\x06\x65\x9c\x53\x79\x6a\xd5\x97\x2f\x8b\xa6\xfa\x33\x55\xff\x8d\xef\x98\x58\x80\xe7\x0a\x7c\xd2\xb4\xaf\xc9\xe4\xf6\xf0\x93\x21\x24\x25\x8e\xad\xd1\x08\xfe\x03\x8e\xff\x02\x45\x81\xb5\x57\xea\xd7\xe9\xab\x90\xa1\x1d\xc8\x2a\xb0\xbe\x9f\xec\x7f\x04\x00\x00\xff\xff\x60\xef\x30\x23\xd9\x0c\x00\x00") func kustomizationapiSwaggerJsonBytes() ([]byte, error) { return bindataRead( @@ -95,7 +93,7 @@ func kustomizationapiSwaggerJson() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "kustomizationapi/swagger.json", size: 3182, mode: os.FileMode(420), modTime: time.Unix(1615228558, 0)} + info := bindataFileInfo{name: "kustomizationapi/swagger.json", size: 3289, mode: os.FileMode(420), modTime: time.Unix(1643411805, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -246,4 +244,4 @@ func RestoreAssets(dir, name string) error { func _filePath(dir, name string) string { cannonicalName := strings.Replace(name, "\\", "/", -1) return filepath.Join(append([]string{dir}, strings.Split(cannonicalName, "/")...)...) -} +} \ No newline at end of file diff --git a/kyaml/openapi/kustomizationapi/swagger.json b/kyaml/openapi/kustomizationapi/swagger.json index 7441a5ee64..e7d8cadd05 100644 --- a/kyaml/openapi/kustomizationapi/swagger.json +++ b/kyaml/openapi/kustomizationapi/swagger.json @@ -126,5 +126,11 @@ } ] } - } + }, + "info": { + "title": "Kustomization", + "version": "v1beta1" + }, + "swagger": "2.0", + "paths": {} } \ No newline at end of file