Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions cmd/pluginator/go.sum
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we want to support reading from a Catalog kind?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure, we can have another helper for that once Catalog is in here / somewhere. I'd like to keep that out of scope of this PR though.

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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
@@ -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"`
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: we usually have this field on the top.

// 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
Original file line number Diff line number Diff line change
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
Loading