Merge pull request #2978 from mortent/NamespaceabilityFromSchema

Determine namespaceability of resources from openapi schema
This commit is contained in:
Kubernetes Prow Robot
2020-09-14 21:10:06 -07:00
committed by GitHub
7 changed files with 68115 additions and 4825 deletions

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

View File

@@ -95,7 +95,7 @@ func openapiKustomizationapiSwaggerJson() (*asset, error) {
return nil, err
}
info := bindataFileInfo{name: "openapi/kustomizationapi/swagger.json", size: 3127, mode: os.FileMode(420), modTime: time.Unix(1586844916, 0)}
info := bindataFileInfo{name: "openapi/kustomizationapi/swagger.json", size: 3127, mode: os.FileMode(420), modTime: time.Unix(1586909905, 0)}
a := &asset{bytes: bytes, info: info}
return a, nil
}

View File

@@ -8,6 +8,7 @@ import (
"fmt"
"io/ioutil"
"reflect"
"strings"
"sync"
"github.com/go-openapi/spec"
@@ -23,10 +24,11 @@ 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
setup sync.Once
schema spec.Schema
schemaByResourceType map[yaml.TypeMeta]*spec.Schema
namespaceabilityByResourceType map[yaml.TypeMeta]bool
noUseBuiltInSchema bool
}
// ResourceSchema wraps the OpenAPI Schema.
@@ -144,7 +146,7 @@ func AddSchemaFromFileUsingField(path, field string) error {
}
// add the json schema to the global schema
_, err = AddSchema(j)
err = AddSchema(j)
if err != nil {
return err
}
@@ -152,7 +154,7 @@ func AddSchemaFromFileUsingField(path, field string) error {
}
// AddSchema parses s, and adds definitions from s to the global schema.
func AddSchema(s []byte) (*spec.Schema, error) {
func AddSchema(s []byte) error {
return parse(s)
}
@@ -188,21 +190,29 @@ func AddDefinitions(definitions spec.Definitions) {
if !ok || len(exts) != 1 {
continue
}
m, ok := exts[0].(map[string]interface{})
typeMeta, ok := toTypeMeta(exts[0])
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
globalSchema.schemaByResourceType[typeMeta] = &d
}
}
func toTypeMeta(ext interface{}) (yaml.TypeMeta, bool) {
m, ok := ext.(map[string]interface{})
if !ok {
return yaml.TypeMeta{}, false
}
g := m[groupKey].(string)
apiVersion := m[versionKey].(string)
if g != "" {
apiVersion = g + "/" + apiVersion
}
return yaml.TypeMeta{Kind: m[kindKey].(string), APIVersion: apiVersion}, true
}
// Resolve resolves the reference against the global schema
func Resolve(ref *spec.Ref) (*spec.Schema, error) {
return resolve(Schema(), ref)
@@ -231,6 +241,19 @@ func GetSchema(s string) (*ResourceSchema, error) {
return &ResourceSchema{Schema: &sc}, nil
}
// IsNamespaceScoped determines whether a resource is namespace or
// cluster-scoped by looking at the information in the openapi schema.
// The second return value tells whether the provided type could be found
// in the openapi schema. If the value is false here, the scope of the
// resource is not known. If the type if found, the first return value will
// be true if the resource is namespace-scoped, and false if the type is
// cluster-scoped.
func IsNamespaceScoped(typeMeta yaml.TypeMeta) (bool, bool) {
initSchema()
isNamespaceScoped, found := globalSchema.namespaceabilityByResourceType[typeMeta]
return isNamespaceScoped, found
}
// 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.
@@ -368,12 +391,12 @@ func initSchema() {
}
// parse the swagger, this should never fail
if _, err := parse(kubernetesapi.MustAsset(kubernetesAPIAssetName)); err != nil {
if err := parse(kubernetesapi.MustAsset(kubernetesAPIAssetName)); err != nil {
// this should never happen
panic(err)
}
if _, err := parse(kustomizationapi.MustAsset(kustomizationAPIAssetName)); err != nil {
if err := parse(kustomizationapi.MustAsset(kustomizationAPIAssetName)); err != nil {
// this should never happen
panic(err)
}
@@ -381,14 +404,55 @@ func initSchema() {
}
// parse parses and indexes a single json schema
func parse(b []byte) (*spec.Schema, error) {
var sc spec.Schema
func parse(b []byte) error {
var swagger spec.Swagger
if err := sc.UnmarshalJSON(b); err != nil {
return nil, errors.Wrap(err)
if err := swagger.UnmarshalJSON(b); err != nil {
return errors.Wrap(err)
}
AddDefinitions(swagger.Definitions)
findNamespaceability(swagger.Paths)
return nil
}
// 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 findNamespaceability(paths *spec.Paths) {
if globalSchema.namespaceabilityByResourceType == nil {
globalSchema.namespaceabilityByResourceType = make(map[yaml.TypeMeta]bool)
}
if paths == nil {
return
}
for path, pathInfo := range paths.Paths {
if pathInfo.Get == nil {
continue
}
gvk, found := pathInfo.Get.VendorExtensible.Extensions[kubernetesGVKExtensionKey]
if !found {
continue
}
typeMeta, found := toTypeMeta(gvk)
if !found {
continue
}
if strings.Contains(path, "namespaces/{namespace}") {
// if we find a namespace path parameter, we just update the map
// directly
globalSchema.namespaceabilityByResourceType[typeMeta] = true
} else if _, found := globalSchema.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.
globalSchema.namespaceabilityByResourceType[typeMeta] = false
}
}
AddDefinitions(sc.Definitions)
return &sc, nil
}
func resolve(root interface{}, ref *spec.Ref) (*spec.Schema, error) {

View File

@@ -16,7 +16,7 @@ func TestAddSchema(t *testing.T) {
// reset package vars
globalSchema = openapiData{}
_, err := AddSchema(additionalSchema)
err := AddSchema(additionalSchema)
if !assert.NoError(t, err) {
t.FailNow()
}
@@ -36,7 +36,7 @@ func TestNoUseBuiltInSchema_AddSchema(t *testing.T) {
globalSchema = openapiData{}
SuppressBuiltInSchemaUse()
_, err := AddSchema(additionalSchema)
err := AddSchema(additionalSchema)
if !assert.NoError(t, err) {
t.FailNow()
}
@@ -313,3 +313,100 @@ kind: Example
t.FailNow()
}
}
func TestIsNamespaceScoped_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) {
ResetOpenAPI()
isNamespaceable, isFound := IsNamespaceScoped(test.typeMeta)
if !test.expectIsFound {
assert.False(t, isFound)
return
}
assert.True(t, isFound)
assert.Equal(t, test.expectIsNamespaced, isNamespaceable)
})
}
}
func TestIsNamespaceScoped_custom(t *testing.T) {
SuppressBuiltInSchemaUse()
err := AddSchema([]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"
}
}
},
"/apis/custom.io/v1/clustercustoms": {
"get": {
"x-kubernetes-action": "get",
"x-kubernetes-group-version-kind": {
"group": "custom.io",
"kind": "ClusterCustom",
"version": "v1"
}
}
}
}
}
`))
assert.NoError(t, err)
isNamespaceable, isFound := IsNamespaceScoped(yaml.TypeMeta{
APIVersion: "custom.io/v1",
Kind: "ClusterCustom",
})
assert.True(t, isFound)
assert.False(t, isNamespaceable)
isNamespaceable, isFound = IsNamespaceScoped(yaml.TypeMeta{
APIVersion: "custom.io/v1",
Kind: "Custom",
})
assert.True(t, isFound)
assert.True(t, isNamespaceable)
}

View File

@@ -38,7 +38,7 @@ metadata:
spec:
replicas: 3 # {"$ref": "#/definitions/io.k8s.cli.setters.replicas"}
`
_, err := openapi.AddSchema([]byte(schema)) // add the schema definitions
err := openapi.AddSchema([]byte(schema)) // add the schema definitions
if err != nil {
panic(err)
}
@@ -103,7 +103,7 @@ spec:
image: nginx:1.7.9 # {"$ref": "#/definitions/io.k8s.cli.substitutions.image"}
`
_, err := openapi.AddSchema([]byte(schema)) // add the schema definitions
err := openapi.AddSchema([]byte(schema)) // add the schema definitions
if err != nil {
panic(err)
}

View File

@@ -954,7 +954,7 @@ func initSchema(t *testing.T, s string) {
openapi.ResetOpenAPI()
// add the json schema to the global schema
_, err = openapi.AddSchema(j)
err = openapi.AddSchema(j)
if !assert.NoError(t, err) {
t.FailNow()
}