mirror of
https://github.com/kubernetes-sigs/kustomize.git
synced 2026-05-17 18:25:26 +00:00
Merge pull request #2978 from mortent/NamespaceabilityFromSchema
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
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user