mirror of
https://github.com/kubernetes-sigs/kustomize.git
synced 2026-05-17 18:25:26 +00:00
Enable validation using function config schema from KRMFunctionDefinition
This commit is contained in:
@@ -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=
|
||||
|
||||
@@ -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}}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
91
kyaml/fn/framework/function_definition.go
Normal file
91
kyaml/fn/framework/function_definition.go
Normal 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"`
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
36
kyaml/fn/framework/validation.go
Normal file
36
kyaml/fn/framework/validation.go
Normal 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
|
||||
}
|
||||
153
kyaml/fn/framework/validation_test.go
Normal file
153
kyaml/fn/framework/validation_test.go
Normal 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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user