Enable validation using function config schema from KRMFunctionDefinition

This commit is contained in:
Katrina Verey
2022-02-09 16:46:56 -05:00
parent a5df6f7fd9
commit c90504a19d
10 changed files with 580 additions and 43 deletions

View File

@@ -48,6 +48,7 @@ github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kd
github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o=
github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY=
github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a h1:idn718Q4B6AGu/h5Sxe66HYVdqdGu2l9Iebqhi/AEoA=
github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY=
github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs=
github.com/bketelsen/crypt v0.0.4/go.mod h1:aI6NrJ0pMGgvZKL1iVgXLnfIFJtfV+bKCoqOes/6LfM=
@@ -156,6 +157,7 @@ github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm4
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4=
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ=
github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.1.2 h1:EVhdT+1Kseyi1/pUmXKaFxYsDNy9RQYkMWRH68J/W7Y=
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
@@ -218,6 +220,7 @@ github.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS4
github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY=
github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
github.com/mitchellh/mapstructure v1.4.1 h1:CpVNEelQCZBooIPDn+AR3NpivK/TIKU8bDxdASFVQag=
github.com/mitchellh/mapstructure v1.4.1/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=

View File

@@ -33,7 +33,7 @@ func buildProcessor(value *string) framework.ResourceListProcessor {
}},
// This will be populated from the --value flag if provided,
// or the config file's `value` field if provided, with the latter taking precedence.
TemplateData: struct {
TemplateData: &struct {
Value *string `yaml:"value"`
}{Value: value}}
}

View File

@@ -10,11 +10,14 @@ import (
"path/filepath"
"strings"
validationErrors "k8s.io/kube-openapi/pkg/validation/errors"
"k8s.io/kube-openapi/pkg/validation/spec"
"sigs.k8s.io/kustomize/kyaml/errors"
"sigs.k8s.io/kustomize/kyaml/fn/framework"
"sigs.k8s.io/kustomize/kyaml/fn/framework/command"
"sigs.k8s.io/kustomize/kyaml/fn/framework/parser"
"sigs.k8s.io/kustomize/kyaml/kio"
"sigs.k8s.io/kustomize/kyaml/resid"
"sigs.k8s.io/kustomize/kyaml/yaml"
)
@@ -962,28 +965,61 @@ func (a *v1alpha1JavaSpringBoot) Default() error {
return nil
}
var javaSpringBootDefinition = `
apiVersion: config.kubernetes.io/v1alpha1
kind: KRMFunctionDefinition
metadata:
name: javaspringboot.example.com
spec:
group: example.com
names:
kind: JavaSpringBoot
versions:
- name: v1alpha1
schema:
openAPIV3Schema:
properties:
apiVersion:
type: string
kind:
type: string
metadata:
type: object
properties:
name:
type: string
minLength: 1
required:
- name
spec:
properties:
domain:
pattern: example\.com$
type: string
image:
type: string
replicas:
maximum: 9
minimum: 0
type: integer
type: object
type: object
`
func (a v1alpha1JavaSpringBoot) Schema() (*spec.Schema, error) {
schema, err := framework.SchemaFromFunctionDefinition(resid.NewGvk("example.com", "v1alpha1", "JavaSpringBoot"), javaSpringBootDefinition)
return schema, errors.WrapPrefixf(err, "parsing JavaSpringBoot schema")
}
func (a *v1alpha1JavaSpringBoot) Validate() error {
var messages []string
if a.Metadata.Name == "" {
messages = append(messages, "name is required")
}
if a.Spec.Replicas > 10 {
messages = append(messages, "replicas must be less than 10")
}
if !strings.HasSuffix(a.Spec.Domain, "example.com") {
messages = append(messages, "domain must be a subdomain of example.com")
}
var errs []error
if strings.HasSuffix(a.Spec.Image, ":latest") {
messages = append(messages, "image should not have latest tag")
errs = append(errs, errors.Errorf("spec.image should not have latest tag"))
}
if len(messages) == 0 {
return nil
if len(errs) > 0 {
return validationErrors.CompositeValidationError(errs...)
}
errMsg := fmt.Sprintf("JavaSpringBoot had %d errors:\n", len(messages))
for i, msg := range messages {
errMsg += fmt.Sprintf(" [%d] %s\n", i+1, msg)
}
return errors.Errorf(errMsg)
return nil
}
// ExampleVersionedAPIProcessor shows how to use the VersionedAPIProcessor and TemplateProcessor to

View File

@@ -7,6 +7,7 @@ import (
goerrors "errors"
"os"
"k8s.io/kube-openapi/pkg/validation/spec"
"sigs.k8s.io/kustomize/kyaml/errors"
"sigs.k8s.io/kustomize/kyaml/kio"
"sigs.k8s.io/kustomize/kyaml/yaml"
@@ -92,6 +93,19 @@ type Validator interface {
Validate() error
}
// ValidationSchemaProvider is implemented by APIs to have the openapi schema provided by Schema()
// used to validate the input functionConfig before it is parsed into the API's struct.
// Use this with framework.SchemaFromFunctionDefinition to load the schema out of a KRMFunctionDefinition
// or CRD (e.g. one generated with KubeBuilder).
//
// func (t MyType) Schema() (*spec.Schema, error) {
// schema, err := framework.SchemaFromFunctionDefinition(resid.NewGvk("example.com", "v1", "MyType"), MyTypeDef)
// return schema, errors.WrapPrefixf(err, "parsing MyType schema")
// }
type ValidationSchemaProvider interface {
Schema() (*spec.Schema, error)
}
// Execute is the entrypoint for invoking configuration functions built with this framework
// from code. See framework/command#Build for a Cobra-based command-line equivalent.
// Execute reads a ResourceList from the given source, passes it to a ResourceListProcessor,
@@ -158,6 +172,9 @@ func Execute(p ResourceListProcessor, rlSource *kio.ByteReadWriter) error {
// Filters that return a Result as error will store the result in the ResourceList
// and continue processing instead of erroring out.
func (rl *ResourceList) Filter(api kio.Filter) error {
if api == nil {
return errors.Errorf("ResourceList cannot run apply nil filter")
}
var err error
rl.Items, err = api.Filter(rl.Items)
if err != nil {

View File

@@ -0,0 +1,91 @@
// Copyright 2022 The Kubernetes Authors.
// SPDX-License-Identifier: Apache-2.0
package framework
import (
"k8s.io/kube-openapi/pkg/validation/spec"
"sigs.k8s.io/kustomize/kyaml/fn/runtime/runtimeutil"
"sigs.k8s.io/kustomize/kyaml/yaml"
)
const FunctionDefinitionKind = "KRMFunctionDefinition"
const FunctionDefinitionGroupVersion = "config.kubernetes.io/v1alpha1"
// KRMFunctionDefinition is metadata that defines a KRM function the same way a CRD defines a custom resource.
// https://github.com/kubernetes/enhancements/tree/master/keps/sig-cli/2906-kustomize-function-catalog#function-metadata-schema
type KRMFunctionDefinition struct {
// APIVersion and Kind of the object. Must be config.kubernetes.io/v1alpha1 and KRMFunctionDefinition respectively.
yaml.TypeMeta `yaml:",inline" json:",inline"`
// Standard KRM object metadata
yaml.ObjectMeta `yaml:"metadata,omitempty" json:"metadata,omitempty"`
// Spec contains the properties of the KRM function this object defines.
Spec KrmFunctionDefinitionSpec `yaml:"spec" json:"spec"`
}
type KrmFunctionDefinitionSpec struct {
//
// The following fields are shared with CustomResourceDefinition.
//
// Group is the API group of the defined KRM function.
Group string `yaml:"group" json:"group"`
// Names specify the resource and kind names for the KRM function.
Names KRMFunctionNames `yaml:"names" json:"names"`
// Versions is the list of all API versions of the defined KRM function.
Versions []KRMFunctionVersion `yaml:"versions" json:"versions"`
//
// The following fields are custom to KRMFunctionDefinition
//
// Description briefly describes the KRM function.
Description string `yaml:"description,omitempty" json:"description,omitempty"`
// Publisher is the entity (e.g. organization) that produced and owns this KRM function.
Publisher string `yaml:"publisher,omitempty" json:"publisher,omitempty"`
// Home is a URI pointing the home page of the KRM function.
Home string `yaml:"home,omitempty" json:"home,omitempty"`
// Maintainers lists the individual maintainers of the KRM function.
Maintainers []string `yaml:"maintainers,omitempty" json:"maintainers,omitempty"`
// Tags are keywords describing the function. e.g. mutator, validator, generator, prefix, GCP.
Tags []string `yaml:"tags,omitempty" json:"tags,omitempty"`
}
type KRMFunctionVersion struct {
//
// The following fields are shared with CustomResourceDefinition.
//
// Name is the version name, e.g. “v1”, “v2beta1”, etc.
Name string `yaml:"name" json:"name"`
// Schema describes the schema of this version of the KRM function.
// This can be used for validation, pruning, and/or defaulting.
Schema *KRMFunctionValidation `yaml:"schema,omitempty" json:"schema,omitempty"`
//
// The following fields are custom to KRMFunctionDefinition
//
// Idempotent indicates whether the function can be re-run multiple times without changing the result.
Idempotent bool `yaml:"idempotent,omitempty" json:"idempotent,omitempty"`
// Usage is URI pointing to a README.md that describe the details of how to use the KRM function.
// It should at least cover what the function does and should give a detailed explanation about each
// field used to configure it.
Usage string `yaml:"usage,omitempty" json:"usage,omitempty"`
// A list of URIs that point to README.md files. Each README.md should cover an example.
// It should at least cover how to get input resources, how to run it and what is the expected
// output.
Examples []string `yaml:"examples,omitempty" json:"examples,omitempty"`
// License is the name of the license covering the function.
License string `yaml:"license,omitempty" json:"license,omitempty"`
// The maintainers for this version of the function, if different from the primary maintainers.
Maintainers []string `yaml:"maintainers,omitempty" json:"maintainers,omitempty"`
// The runtime information describing how to execute this function.
Runtime runtimeutil.FunctionSpec `yaml:"runtime" json:"runtime"`
}
type KRMFunctionValidation struct {
// OpenAPIV3Schema is the OpenAPI v3 schema for an instance of the KRM function.
OpenAPIV3Schema *spec.Schema `yaml:"openAPIV3Schema,omitempty" json:"openAPIV3Schema,omitempty"`
}
type KRMFunctionNames struct {
// Kind is the kind of the defined KRM Function. It is normally CamelCase and singular.
Kind string `yaml:"kind" json:"kind"`
}

View File

@@ -6,12 +6,16 @@ package framework
import (
"strings"
validationErrors "k8s.io/kube-openapi/pkg/validation/errors"
"k8s.io/kube-openapi/pkg/validation/spec"
"k8s.io/kube-openapi/pkg/validation/strfmt"
"k8s.io/kube-openapi/pkg/validation/validate"
"sigs.k8s.io/kustomize/kyaml/errors"
"sigs.k8s.io/kustomize/kyaml/kio"
"sigs.k8s.io/kustomize/kyaml/kio/filters"
"sigs.k8s.io/kustomize/kyaml/openapi"
"sigs.k8s.io/kustomize/kyaml/yaml"
k8syaml "sigs.k8s.io/yaml"
)
// SimpleProcessor processes a ResourceList by loading the FunctionConfig into
@@ -35,9 +39,9 @@ type SimpleProcessor struct {
// defaulting and validation if supported by Config. It then executes the processor's filter.
func (p SimpleProcessor) Process(rl *ResourceList) error {
if err := LoadFunctionConfig(rl.FunctionConfig, p.Config); err != nil {
return errors.Wrap(err)
return errors.WrapPrefixf(err, "loading function config")
}
return errors.Wrap(rl.Filter(p.Filter))
return errors.WrapPrefixf(rl.Filter(p.Filter), "processing filter")
}
// GVKFilterMap is a FilterProvider that resolves Filters through a simple lookup in a map.
@@ -139,7 +143,24 @@ func LoadFunctionConfig(src *yaml.RNode, api interface{}) error {
if api == nil {
return nil
}
if err := yaml.Unmarshal([]byte(src.MustString()), api); err != nil {
// Run this before unmarshalling to avoid nasty unmarshal failure error messages
var schemaValidationError error
if s, ok := api.(ValidationSchemaProvider); ok {
schema, err := s.Schema()
if err != nil {
return errors.WrapPrefixf(err, "loading provided schema")
}
schemaValidationError = validate.AgainstSchema(schema, src, strfmt.Default)
// don't return it yet--try to make it to custom validation stage to combine errors
}
// using sigs.k8s.io/yaml here lets the custom types embed core types
// that only have json tags, notably types from k8s.io/apimachinery/pkg/apis/meta/v1
if err := k8syaml.Unmarshal([]byte(src.MustString()), api); err != nil {
if schemaValidationError != nil {
// if we got a validation error, report it instead as it is likely a nicer version of the same message
return schemaValidationError
}
return errors.Wrap(err)
}
@@ -150,7 +171,25 @@ func LoadFunctionConfig(src *yaml.RNode, api interface{}) error {
}
if v, ok := api.(Validator); ok {
return v.Validate()
return combineErrors(schemaValidationError, v.Validate())
}
return nil
}
func combineErrors(schemaErr, customErr error) error {
combined := validationErrors.CompositeValidationError()
if compositeSchemaErr, ok := schemaErr.(*validationErrors.CompositeError); ok {
combined.Errors = append(combined.Errors, compositeSchemaErr.Errors...)
} else if schemaErr != nil {
combined.Errors = append(combined.Errors, schemaErr)
}
if compositeCustomErr, ok := customErr.(*validationErrors.CompositeError); ok {
combined.Errors = append(combined.Errors, compositeCustomErr.Errors...)
} else if customErr != nil {
combined.Errors = append(combined.Errors, customErr)
}
if len(combined.Errors) > 0 {
return combined
}
return nil
}

View File

@@ -11,6 +11,8 @@ import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
validationErrors "k8s.io/kube-openapi/pkg/validation/errors"
"k8s.io/kube-openapi/pkg/validation/spec"
"sigs.k8s.io/kustomize/kyaml/errors"
"sigs.k8s.io/kustomize/kyaml/fn/framework/frameworktestutil"
"sigs.k8s.io/kustomize/kyaml/fn/framework/parser"
@@ -357,13 +359,21 @@ func TestSimpleProcessor_Process_Error(t *testing.T) {
wantErr string
}{
{
name: "error when given func as Config",
config: func() {},
wantErr: "cannot unmarshal !!map into func()",
name: "error when filter is nil",
config: map[string]string{},
filter: nil,
wantErr: "processing filter: ResourceList cannot run apply nil filter",
}, {
name: "no error when config is nil",
config: nil,
filter: kio.FilterFunc(func(items []*yaml.RNode) ([]*yaml.RNode, error) {
return items, nil
}),
wantErr: "",
},
{
name: "error in filter",
wantErr: "err from filter",
wantErr: "processing filter: err from filter",
filter: kio.FilterFunc(func(_ []*yaml.RNode) ([]*yaml.RNode, error) {
return nil, errors.Errorf("err from filter")
}),
@@ -382,8 +392,11 @@ func TestSimpleProcessor_Process_Error(t *testing.T) {
}),
}
err := p.Process(&rl)
require.Error(t, err)
assert.Contains(t, err.Error(), tt.wantErr)
if tt.wantErr == "" {
require.NoError(t, err)
} else {
assert.EqualError(t, err, tt.wantErr)
}
})
}
}
@@ -396,15 +409,6 @@ func TestVersionedAPIProcessor_Process_Error(t *testing.T) {
kind string
wantErr string
}{
{
name: "error when given FilterFunc as Filter",
filterProvider: framework.FilterProviderFunc(func(_, _ string) (kio.Filter, error) {
return kio.FilterFunc(func(items []*yaml.RNode) ([]*yaml.RNode, error) {
return items, nil
}), nil
}),
wantErr: "cannot unmarshal !!map into kio.FilterFunc",
},
{
name: "error in filter",
filterProvider: framework.FilterProviderFunc(func(_, _ string) (kio.Filter, error) {
@@ -658,3 +662,161 @@ func TestTemplateProcessor_Validator(t *testing.T) {
}
c.Assert(t)
}
type jsonTagTest struct {
Name string `json:"name"`
Test bool `json:"test"`
}
type yamlTagTest struct {
Name string `yaml:"name"`
Test bool `yaml:"test"`
}
type customErrorTest struct {
v1alpha1JavaSpringBoot
}
func (e customErrorTest) Schema() (*spec.Schema, error) {
return e.v1alpha1JavaSpringBoot.Schema()
}
func (e customErrorTest) Validate() error {
return errors.Errorf("Custom errors:\n- first error\n- second error")
}
type errorMergeTest struct {
v1alpha1JavaSpringBoot
}
func (e errorMergeTest) Schema() (*spec.Schema, error) {
return e.v1alpha1JavaSpringBoot.Schema()
}
func (e errorMergeTest) Validate() error {
if strings.HasSuffix(e.Spec.Image, "latest") {
return validationErrors.CompositeValidationError(errors.Errorf("spec.image cannot be tagged :latest"))
}
return nil
}
func TestLoadFunctionConfig(t *testing.T) {
tests := []struct {
name string
src *yaml.RNode
api interface{}
want interface{}
wantErrMsgs []string
}{
{
name: "combines schema-based and non-composite custom errors",
src: yaml.MustParse(`
apiVersion: example.com/v1alpha1
kind: JavaSpringBoot
spec:
replicas: 11
domain: foo.myco.io
image: nginx:latest
`),
api: &customErrorTest{},
wantErrMsgs: []string{
"validation failure list:",
"spec.replicas in body should be less than or equal to 9",
"spec.domain in body should match 'example\\.com$'",
`Custom errors:
- first error
- second error`,
},
},
{
name: "merges schema-based errors with custom composite errors",
src: yaml.MustParse(`
apiVersion: example.com/v1alpha1
kind: JavaSpringBoot
spec:
replicas: 11
domain: foo.myco.io
image: nginx:latest
`),
api: &errorMergeTest{},
wantErrMsgs: []string{"validation failure list:",
"spec.replicas in body should be less than or equal to 9",
"spec.domain in body should match 'example\\.com$'",
"spec.image cannot be tagged :latest"},
},
{
name: "schema errors only",
src: yaml.MustParse(`
apiVersion: example.com/v1alpha1
kind: JavaSpringBoot
spec:
replicas: 11
`),
api: &errorMergeTest{},
wantErrMsgs: []string{
`validation failure list:
spec.replicas in body should be less than or equal to 9`,
},
},
{
name: "custom errors only",
src: yaml.MustParse(`
apiVersion: example.com/v1alpha1
kind: JavaSpringBoot
spec:
image: nginx:latest
`),
api: &errorMergeTest{},
wantErrMsgs: []string{
`validation failure list:
spec.image cannot be tagged :latest`},
},
{
name: "both custom and schema error hooks defined, but no errors produced",
src: yaml.MustParse(`
apiVersion: example.com/v1alpha1
kind: JavaSpringBoot
spec:
image: nginx:1.0
replicas: 3
domain: bar.example.com
`),
api: &errorMergeTest{},
want: &errorMergeTest{v1alpha1JavaSpringBoot: v1alpha1JavaSpringBoot{
Spec: v1alpha1JavaSpringBootSpec{Replicas: 3, Domain: "bar.example.com", Image: "nginx:1.0"}},
},
},
{
name: "successfully loads types that include fields only tagged with json markers",
src: yaml.MustParse(`
name: tester
test: true
`),
api: &jsonTagTest{},
want: &jsonTagTest{Name: "tester", Test: true},
},
{
name: "successfully loads types that include fields only tagged with yaml markers",
src: yaml.MustParse(`
name: tester
test: true
`),
api: &yamlTagTest{},
want: &yamlTagTest{Name: "tester", Test: true},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := framework.LoadFunctionConfig(tt.src, tt.api)
if len(tt.wantErrMsgs) == 0 {
require.NoError(t, err)
require.Equal(t, tt.want, tt.api)
} else {
for _, msg := range tt.wantErrMsgs {
require.Contains(t, err.Error(), msg)
}
}
})
}
}

View File

@@ -1,5 +1,5 @@
JavaSpringBoot had 4 errors:
\[\d\] replicas must be less than 10
\[\d\] name is required
\[\d\] image should not have latest tag
\[\d\] domain must be a subdomain of example.com
validation failure list:
spec.domain in body should match 'example\\.com\$'
spec.image should not have latest tag
metadata.name in body should be at least 1 chars long
spec.replicas in body should be less than or equal to 9

View File

@@ -0,0 +1,36 @@
// Copyright 2022 The Kubernetes Authors.
// SPDX-License-Identifier: Apache-2.0
package framework
import (
"k8s.io/kube-openapi/pkg/validation/spec"
"sigs.k8s.io/kustomize/kyaml/errors"
"sigs.k8s.io/kustomize/kyaml/resid"
k8syaml "sigs.k8s.io/yaml"
)
// SchemaFromFunctionDefinition extracts the schema for a particular GVK from the provided KRMFunctionDefinition
// Since the relevant fields of KRMFunctionDefinition exactly match the ones in CustomResourceDefinition,
// this helper can also load CRDs (e.g. produced by KubeBuilder) transparently.
func SchemaFromFunctionDefinition(gvk resid.Gvk, data string) (*spec.Schema, error) {
var def KRMFunctionDefinition
// need to use sigs yaml because spec.Schema type only has json tags
if err := k8syaml.Unmarshal([]byte(data), &def); err != nil {
return nil, errors.WrapPrefixf(err, "unmarshalling %s", FunctionDefinitionKind)
}
var foundGVKs []*resid.Gvk
var schema *spec.Schema
for i, version := range def.Spec.Versions {
versionGVK := resid.Gvk{Group: def.Spec.Group, Kind: def.Spec.Names.Kind, Version: version.Name}
if gvk.Equals(versionGVK) {
schema = def.Spec.Versions[i].Schema.OpenAPIV3Schema
break
}
foundGVKs = append(foundGVKs, &versionGVK)
}
if schema == nil {
return nil, errors.Errorf("%s does not define %s (defines: %s)", FunctionDefinitionKind, gvk, foundGVKs)
}
return schema, nil
}

View File

@@ -0,0 +1,153 @@
// Copyright 2022 The Kubernetes Authors.
// SPDX-License-Identifier: Apache-2.0
package framework
import (
"sort"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"sigs.k8s.io/kustomize/kyaml/resid"
)
var demoFunctionDefinition = `
apiVersion: config.kubernetes.io/v1alpha1
kind: KRMFunctionDefinition
metadata:
name: demos.example.io
spec:
group: example.io
names:
kind: Demo
versions:
- name: v1alpha1
schema:
openAPIV3Schema:
properties:
apiVersion:
type: string
color:
type: string
kind:
type: string
metadata:
type: object
required:
- color
type: object
- name: v1alpha2
schema:
openAPIV3Schema:
properties:
apiVersion:
type: string
flavor:
type: string
kind:
type: string
metadata:
type: object
required:
- flavor
type: object
`
var demoCRD = `
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
name: demos.example.io
spec:
group: example.io
names:
kind: Demo
listKind: DemoList
plural: demos
singular: demo
scope: Namespaced
versions:
- name: v1alpha1
schema:
openAPIV3Schema:
properties:
apiVersion:
description: 'APIVersion defines the versioned schema of this representation
of an object. Servers should convert recognized schemas to the latest
internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources'
type: string
color:
type: string
kind:
description: 'Kind is a string value representing the REST resource this
object represents. Servers may infer this from the endpoint the client
submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds'
type: string
metadata:
type: object
required:
- color
type: object
served: true
storage: true
`
func TestSchemaFromFunctionDefinition(t *testing.T) {
tests := []struct {
name string
gvk resid.Gvk
data string
wantProps []string
wantErr string
}{
{
name: "demo KRMFunctionDefinition extract v1alpha1",
gvk: resid.NewGvk("example.io", "v1alpha1", "Demo"),
data: demoFunctionDefinition,
wantProps: []string{"apiVersion", "kind", "metadata", "color"},
}, {
name: "demo KRMFunctionDefinition extract v1alpha2",
gvk: resid.NewGvk("example.io", "v1alpha2", "Demo"),
data: demoFunctionDefinition,
wantProps: []string{"apiVersion", "kind", "metadata", "flavor"},
}, {
name: "works with CustomResourceDefinition",
gvk: resid.NewGvk("example.io", "v1alpha1", "Demo"),
data: demoCRD,
wantProps: []string{"apiVersion", "kind", "metadata", "color"},
}, {
name: "group mismatch",
gvk: resid.NewGvk("example.com", "v1alpha2", "Demo"),
data: demoFunctionDefinition,
wantErr: "KRMFunctionDefinition does not define Demo.v1alpha2.example.com (defines: [Demo.v1alpha1.example.io Demo.v1alpha2.example.io])",
}, {
name: "version mismatch",
gvk: resid.NewGvk("example.io", "v1alpha3", "Demo"),
data: demoFunctionDefinition,
wantErr: "KRMFunctionDefinition does not define Demo.v1alpha3.example.io (defines: [Demo.v1alpha1.example.io Demo.v1alpha2.example.io])",
}, {
name: "kind mismatch",
gvk: resid.NewGvk("example.io", "v1alpha2", "Demonstration"),
data: demoFunctionDefinition,
wantErr: "KRMFunctionDefinition does not define Demonstration.v1alpha2.example.io (defines: [Demo.v1alpha1.example.io Demo.v1alpha2.example.io])",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := SchemaFromFunctionDefinition(tt.gvk, tt.data)
if tt.wantErr != "" {
require.EqualError(t, err, tt.wantErr)
} else {
require.NoError(t, err)
var gotProps []string
for prop, _ := range got.Properties {
gotProps = append(gotProps, prop)
}
sort.Strings(tt.wantProps)
sort.Strings(gotProps)
assert.Equal(t, gotProps, tt.wantProps)
}
})
}
}