Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

new openapi package using gnostic/openapiv2 #4474

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
166 changes: 166 additions & 0 deletions kyaml/openapi/internal/openapignostic/openapignostic.go
Original file line number Diff line number Diff line change
@@ -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{}
}
145 changes: 145 additions & 0 deletions kyaml/openapi/internal/openapignostic/openapignostic_test.go
Original file line number Diff line number Diff line change
@@ -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
}
18 changes: 8 additions & 10 deletions kyaml/openapi/kustomizationapi/swagger.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 7 additions & 1 deletion kyaml/openapi/kustomizationapi/swagger.json
Original file line number Diff line number Diff line change
Expand Up @@ -126,5 +126,11 @@
}
]
}
}
},
"info": {
"title": "Kustomization",
"version": "v1beta1"
},
"swagger": "2.0",
"paths": {}
}