Skip to content

Commit

Permalink
Merge pull request #4467 from KnVerey/fn-cfg-openapi-validation
Browse files Browse the repository at this point in the history
fn framework: Enable validation using openAPI schema for functionConfig
  • Loading branch information
k8s-ci-robot committed Feb 24, 2022
2 parents 8dab949 + c90504a commit 6950a0d
Show file tree
Hide file tree
Showing 10 changed files with 580 additions and 43 deletions.
3 changes: 3 additions & 0 deletions cmd/pluginator/go.sum
Expand Up @@ -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=
Expand Down Expand Up @@ -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=
Expand Down Expand Up @@ -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=
Expand Down
2 changes: 1 addition & 1 deletion kyaml/fn/framework/example/main.go
Expand Up @@ -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}}
}
Expand Down
72 changes: 54 additions & 18 deletions kyaml/fn/framework/example_test.go
Expand Up @@ -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"
)

Expand Down Expand Up @@ -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
Expand Down
17 changes: 17 additions & 0 deletions kyaml/fn/framework/framework.go
Expand Up @@ -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"
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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 {
Expand Down
91 changes: 91 additions & 0 deletions kyaml/fn/framework/function_definition.go
@@ -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"`
}
47 changes: 43 additions & 4 deletions kyaml/fn/framework/processors.go
Expand Up @@ -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
Expand All @@ -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.
Expand Down Expand Up @@ -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)
}

Expand All @@ -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
}
Expand Down

0 comments on commit 6950a0d

Please sign in to comment.