mirror of
https://github.com/kubernetes-sigs/kustomize.git
synced 2026-06-13 18:10:59 +00:00
Determine namespaceability of resources from openapi schema
This commit is contained in:
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large
Load Diff
@@ -95,7 +95,7 @@ func openapiKustomizationapiSwaggerJson() (*asset, error) {
|
|||||||
return nil, err
|
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}
|
a := &asset{bytes: bytes, info: info}
|
||||||
return a, nil
|
return a, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"reflect"
|
"reflect"
|
||||||
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
"github.com/go-openapi/spec"
|
"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
|
// 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.
|
// a list of vars so that it can be reset from tests.
|
||||||
type openapiData struct {
|
type openapiData struct {
|
||||||
setup sync.Once
|
setup sync.Once
|
||||||
schema spec.Schema
|
schema spec.Schema
|
||||||
schemaByResourceType map[yaml.TypeMeta]*spec.Schema
|
schemaByResourceType map[yaml.TypeMeta]*spec.Schema
|
||||||
noUseBuiltInSchema bool
|
namespaceabilityByResourceType map[yaml.TypeMeta]bool
|
||||||
|
noUseBuiltInSchema bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// ResourceSchema wraps the OpenAPI Schema.
|
// ResourceSchema wraps the OpenAPI Schema.
|
||||||
@@ -144,7 +146,7 @@ func AddSchemaFromFileUsingField(path, field string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// add the json schema to the global schema
|
// add the json schema to the global schema
|
||||||
_, err = AddSchema(j)
|
err = AddSchema(j)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -152,7 +154,7 @@ func AddSchemaFromFileUsingField(path, field string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// AddSchema parses s, and adds definitions from s to the global schema.
|
// 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)
|
return parse(s)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -188,21 +190,29 @@ func AddDefinitions(definitions spec.Definitions) {
|
|||||||
if !ok || len(exts) != 1 {
|
if !ok || len(exts) != 1 {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
m, ok := exts[0].(map[string]interface{})
|
|
||||||
|
typeMeta, ok := toTypeMeta(exts[0])
|
||||||
if !ok {
|
if !ok {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
globalSchema.schemaByResourceType[typeMeta] = &d
|
||||||
// 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
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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
|
// Resolve resolves the reference against the global schema
|
||||||
func Resolve(ref *spec.Ref) (*spec.Schema, error) {
|
func Resolve(ref *spec.Ref) (*spec.Schema, error) {
|
||||||
return resolve(Schema(), ref)
|
return resolve(Schema(), ref)
|
||||||
@@ -231,6 +241,19 @@ func GetSchema(s string) (*ResourceSchema, error) {
|
|||||||
return &ResourceSchema{Schema: &sc}, nil
|
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
|
// SuppressBuiltInSchemaUse can be called to prevent using the built-in Kubernetes
|
||||||
// schema as part of the global schema.
|
// schema as part of the global schema.
|
||||||
// Must be called before the schema is used.
|
// Must be called before the schema is used.
|
||||||
@@ -368,12 +391,12 @@ func initSchema() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// parse the swagger, this should never fail
|
// 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
|
// this should never happen
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if _, err := parse(kustomizationapi.MustAsset(kustomizationAPIAssetName)); err != nil {
|
if err := parse(kustomizationapi.MustAsset(kustomizationAPIAssetName)); err != nil {
|
||||||
// this should never happen
|
// this should never happen
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
@@ -381,14 +404,55 @@ func initSchema() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// parse parses and indexes a single json schema
|
// parse parses and indexes a single json schema
|
||||||
func parse(b []byte) (*spec.Schema, error) {
|
func parse(b []byte) error {
|
||||||
var sc spec.Schema
|
var swagger spec.Swagger
|
||||||
|
|
||||||
if err := sc.UnmarshalJSON(b); err != nil {
|
if err := swagger.UnmarshalJSON(b); err != nil {
|
||||||
return nil, errors.Wrap(err)
|
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) {
|
func resolve(root interface{}, ref *spec.Ref) (*spec.Schema, error) {
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ func TestAddSchema(t *testing.T) {
|
|||||||
// reset package vars
|
// reset package vars
|
||||||
globalSchema = openapiData{}
|
globalSchema = openapiData{}
|
||||||
|
|
||||||
_, err := AddSchema(additionalSchema)
|
err := AddSchema(additionalSchema)
|
||||||
if !assert.NoError(t, err) {
|
if !assert.NoError(t, err) {
|
||||||
t.FailNow()
|
t.FailNow()
|
||||||
}
|
}
|
||||||
@@ -36,7 +36,7 @@ func TestNoUseBuiltInSchema_AddSchema(t *testing.T) {
|
|||||||
globalSchema = openapiData{}
|
globalSchema = openapiData{}
|
||||||
|
|
||||||
SuppressBuiltInSchemaUse()
|
SuppressBuiltInSchemaUse()
|
||||||
_, err := AddSchema(additionalSchema)
|
err := AddSchema(additionalSchema)
|
||||||
if !assert.NoError(t, err) {
|
if !assert.NoError(t, err) {
|
||||||
t.FailNow()
|
t.FailNow()
|
||||||
}
|
}
|
||||||
@@ -313,3 +313,100 @@ kind: Example
|
|||||||
t.FailNow()
|
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)
|
||||||
|
}
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ metadata:
|
|||||||
spec:
|
spec:
|
||||||
replicas: 3 # {"$ref": "#/definitions/io.k8s.cli.setters.replicas"}
|
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 {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
@@ -103,7 +103,7 @@ spec:
|
|||||||
image: nginx:1.7.9 # {"$ref": "#/definitions/io.k8s.cli.substitutions.image"}
|
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 {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -954,7 +954,7 @@ func initSchema(t *testing.T, s string) {
|
|||||||
openapi.ResetOpenAPI()
|
openapi.ResetOpenAPI()
|
||||||
|
|
||||||
// add the json schema to the global schema
|
// add the json schema to the global schema
|
||||||
_, err = openapi.AddSchema(j)
|
err = openapi.AddSchema(j)
|
||||||
if !assert.NoError(t, err) {
|
if !assert.NoError(t, err) {
|
||||||
t.FailNow()
|
t.FailNow()
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user