Skip to content

Commit 6950a0d

Browse files
authored
Merge pull request #4467 from KnVerey/fn-cfg-openapi-validation
fn framework: Enable validation using openAPI schema for functionConfig
2 parents 8dab949 + c90504a commit 6950a0d

File tree

10 files changed

+580
-43
lines changed

10 files changed

+580
-43
lines changed

cmd/pluginator/go.sum

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kd
4848
github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o=
4949
github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY=
5050
github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
51+
github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a h1:idn718Q4B6AGu/h5Sxe66HYVdqdGu2l9Iebqhi/AEoA=
5152
github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY=
5253
github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs=
5354
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
156157
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4=
157158
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ=
158159
github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
160+
github.com/google/uuid v1.1.2 h1:EVhdT+1Kseyi1/pUmXKaFxYsDNy9RQYkMWRH68J/W7Y=
159161
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
160162
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
161163
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
218220
github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY=
219221
github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
220222
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
223+
github.com/mitchellh/mapstructure v1.4.1 h1:CpVNEelQCZBooIPDn+AR3NpivK/TIKU8bDxdASFVQag=
221224
github.com/mitchellh/mapstructure v1.4.1/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
222225
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
223226
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=

kyaml/fn/framework/example/main.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ func buildProcessor(value *string) framework.ResourceListProcessor {
3333
}},
3434
// This will be populated from the --value flag if provided,
3535
// or the config file's `value` field if provided, with the latter taking precedence.
36-
TemplateData: struct {
36+
TemplateData: &struct {
3737
Value *string `yaml:"value"`
3838
}{Value: value}}
3939
}

kyaml/fn/framework/example_test.go

Lines changed: 54 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,14 @@ import (
1010
"path/filepath"
1111
"strings"
1212

13+
validationErrors "k8s.io/kube-openapi/pkg/validation/errors"
14+
"k8s.io/kube-openapi/pkg/validation/spec"
1315
"sigs.k8s.io/kustomize/kyaml/errors"
1416
"sigs.k8s.io/kustomize/kyaml/fn/framework"
1517
"sigs.k8s.io/kustomize/kyaml/fn/framework/command"
1618
"sigs.k8s.io/kustomize/kyaml/fn/framework/parser"
1719
"sigs.k8s.io/kustomize/kyaml/kio"
20+
"sigs.k8s.io/kustomize/kyaml/resid"
1821
"sigs.k8s.io/kustomize/kyaml/yaml"
1922
)
2023

@@ -962,28 +965,61 @@ func (a *v1alpha1JavaSpringBoot) Default() error {
962965
return nil
963966
}
964967

968+
var javaSpringBootDefinition = `
969+
apiVersion: config.kubernetes.io/v1alpha1
970+
kind: KRMFunctionDefinition
971+
metadata:
972+
name: javaspringboot.example.com
973+
spec:
974+
group: example.com
975+
names:
976+
kind: JavaSpringBoot
977+
versions:
978+
- name: v1alpha1
979+
schema:
980+
openAPIV3Schema:
981+
properties:
982+
apiVersion:
983+
type: string
984+
kind:
985+
type: string
986+
metadata:
987+
type: object
988+
properties:
989+
name:
990+
type: string
991+
minLength: 1
992+
required:
993+
- name
994+
spec:
995+
properties:
996+
domain:
997+
pattern: example\.com$
998+
type: string
999+
image:
1000+
type: string
1001+
replicas:
1002+
maximum: 9
1003+
minimum: 0
1004+
type: integer
1005+
type: object
1006+
type: object
1007+
`
1008+
1009+
func (a v1alpha1JavaSpringBoot) Schema() (*spec.Schema, error) {
1010+
schema, err := framework.SchemaFromFunctionDefinition(resid.NewGvk("example.com", "v1alpha1", "JavaSpringBoot"), javaSpringBootDefinition)
1011+
return schema, errors.WrapPrefixf(err, "parsing JavaSpringBoot schema")
1012+
}
1013+
9651014
func (a *v1alpha1JavaSpringBoot) Validate() error {
966-
var messages []string
967-
if a.Metadata.Name == "" {
968-
messages = append(messages, "name is required")
969-
}
970-
if a.Spec.Replicas > 10 {
971-
messages = append(messages, "replicas must be less than 10")
972-
}
973-
if !strings.HasSuffix(a.Spec.Domain, "example.com") {
974-
messages = append(messages, "domain must be a subdomain of example.com")
975-
}
1015+
var errs []error
9761016
if strings.HasSuffix(a.Spec.Image, ":latest") {
977-
messages = append(messages, "image should not have latest tag")
1017+
errs = append(errs, errors.Errorf("spec.image should not have latest tag"))
9781018
}
979-
if len(messages) == 0 {
980-
return nil
1019+
if len(errs) > 0 {
1020+
return validationErrors.CompositeValidationError(errs...)
9811021
}
982-
errMsg := fmt.Sprintf("JavaSpringBoot had %d errors:\n", len(messages))
983-
for i, msg := range messages {
984-
errMsg += fmt.Sprintf(" [%d] %s\n", i+1, msg)
985-
}
986-
return errors.Errorf(errMsg)
1022+
return nil
9871023
}
9881024

9891025
// ExampleVersionedAPIProcessor shows how to use the VersionedAPIProcessor and TemplateProcessor to

kyaml/fn/framework/framework.go

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77
goerrors "errors"
88
"os"
99

10+
"k8s.io/kube-openapi/pkg/validation/spec"
1011
"sigs.k8s.io/kustomize/kyaml/errors"
1112
"sigs.k8s.io/kustomize/kyaml/kio"
1213
"sigs.k8s.io/kustomize/kyaml/yaml"
@@ -92,6 +93,19 @@ type Validator interface {
9293
Validate() error
9394
}
9495

96+
// ValidationSchemaProvider is implemented by APIs to have the openapi schema provided by Schema()
97+
// used to validate the input functionConfig before it is parsed into the API's struct.
98+
// Use this with framework.SchemaFromFunctionDefinition to load the schema out of a KRMFunctionDefinition
99+
// or CRD (e.g. one generated with KubeBuilder).
100+
//
101+
// func (t MyType) Schema() (*spec.Schema, error) {
102+
// schema, err := framework.SchemaFromFunctionDefinition(resid.NewGvk("example.com", "v1", "MyType"), MyTypeDef)
103+
// return schema, errors.WrapPrefixf(err, "parsing MyType schema")
104+
// }
105+
type ValidationSchemaProvider interface {
106+
Schema() (*spec.Schema, error)
107+
}
108+
95109
// Execute is the entrypoint for invoking configuration functions built with this framework
96110
// from code. See framework/command#Build for a Cobra-based command-line equivalent.
97111
// 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 {
158172
// Filters that return a Result as error will store the result in the ResourceList
159173
// and continue processing instead of erroring out.
160174
func (rl *ResourceList) Filter(api kio.Filter) error {
175+
if api == nil {
176+
return errors.Errorf("ResourceList cannot run apply nil filter")
177+
}
161178
var err error
162179
rl.Items, err = api.Filter(rl.Items)
163180
if err != nil {
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
// Copyright 2022 The Kubernetes Authors.
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
package framework
5+
6+
import (
7+
"k8s.io/kube-openapi/pkg/validation/spec"
8+
"sigs.k8s.io/kustomize/kyaml/fn/runtime/runtimeutil"
9+
"sigs.k8s.io/kustomize/kyaml/yaml"
10+
)
11+
12+
const FunctionDefinitionKind = "KRMFunctionDefinition"
13+
const FunctionDefinitionGroupVersion = "config.kubernetes.io/v1alpha1"
14+
15+
// KRMFunctionDefinition is metadata that defines a KRM function the same way a CRD defines a custom resource.
16+
// https://github.com/kubernetes/enhancements/tree/master/keps/sig-cli/2906-kustomize-function-catalog#function-metadata-schema
17+
type KRMFunctionDefinition struct {
18+
// APIVersion and Kind of the object. Must be config.kubernetes.io/v1alpha1 and KRMFunctionDefinition respectively.
19+
yaml.TypeMeta `yaml:",inline" json:",inline"`
20+
// Standard KRM object metadata
21+
yaml.ObjectMeta `yaml:"metadata,omitempty" json:"metadata,omitempty"`
22+
// Spec contains the properties of the KRM function this object defines.
23+
Spec KrmFunctionDefinitionSpec `yaml:"spec" json:"spec"`
24+
}
25+
26+
type KrmFunctionDefinitionSpec struct {
27+
//
28+
// The following fields are shared with CustomResourceDefinition.
29+
//
30+
// Group is the API group of the defined KRM function.
31+
Group string `yaml:"group" json:"group"`
32+
// Names specify the resource and kind names for the KRM function.
33+
Names KRMFunctionNames `yaml:"names" json:"names"`
34+
// Versions is the list of all API versions of the defined KRM function.
35+
Versions []KRMFunctionVersion `yaml:"versions" json:"versions"`
36+
37+
//
38+
// The following fields are custom to KRMFunctionDefinition
39+
//
40+
// Description briefly describes the KRM function.
41+
Description string `yaml:"description,omitempty" json:"description,omitempty"`
42+
// Publisher is the entity (e.g. organization) that produced and owns this KRM function.
43+
Publisher string `yaml:"publisher,omitempty" json:"publisher,omitempty"`
44+
// Home is a URI pointing the home page of the KRM function.
45+
Home string `yaml:"home,omitempty" json:"home,omitempty"`
46+
// Maintainers lists the individual maintainers of the KRM function.
47+
Maintainers []string `yaml:"maintainers,omitempty" json:"maintainers,omitempty"`
48+
// Tags are keywords describing the function. e.g. mutator, validator, generator, prefix, GCP.
49+
Tags []string `yaml:"tags,omitempty" json:"tags,omitempty"`
50+
}
51+
52+
type KRMFunctionVersion struct {
53+
//
54+
// The following fields are shared with CustomResourceDefinition.
55+
//
56+
// Name is the version name, e.g. “v1”, “v2beta1”, etc.
57+
Name string `yaml:"name" json:"name"`
58+
// Schema describes the schema of this version of the KRM function.
59+
// This can be used for validation, pruning, and/or defaulting.
60+
Schema *KRMFunctionValidation `yaml:"schema,omitempty" json:"schema,omitempty"`
61+
62+
//
63+
// The following fields are custom to KRMFunctionDefinition
64+
//
65+
// Idempotent indicates whether the function can be re-run multiple times without changing the result.
66+
Idempotent bool `yaml:"idempotent,omitempty" json:"idempotent,omitempty"`
67+
// Usage is URI pointing to a README.md that describe the details of how to use the KRM function.
68+
// It should at least cover what the function does and should give a detailed explanation about each
69+
// field used to configure it.
70+
Usage string `yaml:"usage,omitempty" json:"usage,omitempty"`
71+
// A list of URIs that point to README.md files. Each README.md should cover an example.
72+
// It should at least cover how to get input resources, how to run it and what is the expected
73+
// output.
74+
Examples []string `yaml:"examples,omitempty" json:"examples,omitempty"`
75+
// License is the name of the license covering the function.
76+
License string `yaml:"license,omitempty" json:"license,omitempty"`
77+
// The maintainers for this version of the function, if different from the primary maintainers.
78+
Maintainers []string `yaml:"maintainers,omitempty" json:"maintainers,omitempty"`
79+
// The runtime information describing how to execute this function.
80+
Runtime runtimeutil.FunctionSpec `yaml:"runtime" json:"runtime"`
81+
}
82+
83+
type KRMFunctionValidation struct {
84+
// OpenAPIV3Schema is the OpenAPI v3 schema for an instance of the KRM function.
85+
OpenAPIV3Schema *spec.Schema `yaml:"openAPIV3Schema,omitempty" json:"openAPIV3Schema,omitempty"`
86+
}
87+
88+
type KRMFunctionNames struct {
89+
// Kind is the kind of the defined KRM Function. It is normally CamelCase and singular.
90+
Kind string `yaml:"kind" json:"kind"`
91+
}

kyaml/fn/framework/processors.go

Lines changed: 43 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,16 @@ package framework
66
import (
77
"strings"
88

9+
validationErrors "k8s.io/kube-openapi/pkg/validation/errors"
910
"k8s.io/kube-openapi/pkg/validation/spec"
11+
"k8s.io/kube-openapi/pkg/validation/strfmt"
12+
"k8s.io/kube-openapi/pkg/validation/validate"
1013
"sigs.k8s.io/kustomize/kyaml/errors"
1114
"sigs.k8s.io/kustomize/kyaml/kio"
1215
"sigs.k8s.io/kustomize/kyaml/kio/filters"
1316
"sigs.k8s.io/kustomize/kyaml/openapi"
1417
"sigs.k8s.io/kustomize/kyaml/yaml"
18+
k8syaml "sigs.k8s.io/yaml"
1519
)
1620

1721
// SimpleProcessor processes a ResourceList by loading the FunctionConfig into
@@ -35,9 +39,9 @@ type SimpleProcessor struct {
3539
// defaulting and validation if supported by Config. It then executes the processor's filter.
3640
func (p SimpleProcessor) Process(rl *ResourceList) error {
3741
if err := LoadFunctionConfig(rl.FunctionConfig, p.Config); err != nil {
38-
return errors.Wrap(err)
42+
return errors.WrapPrefixf(err, "loading function config")
3943
}
40-
return errors.Wrap(rl.Filter(p.Filter))
44+
return errors.WrapPrefixf(rl.Filter(p.Filter), "processing filter")
4145
}
4246

4347
// 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 {
139143
if api == nil {
140144
return nil
141145
}
142-
if err := yaml.Unmarshal([]byte(src.MustString()), api); err != nil {
146+
// Run this before unmarshalling to avoid nasty unmarshal failure error messages
147+
var schemaValidationError error
148+
if s, ok := api.(ValidationSchemaProvider); ok {
149+
schema, err := s.Schema()
150+
if err != nil {
151+
return errors.WrapPrefixf(err, "loading provided schema")
152+
}
153+
schemaValidationError = validate.AgainstSchema(schema, src, strfmt.Default)
154+
// don't return it yet--try to make it to custom validation stage to combine errors
155+
}
156+
157+
// using sigs.k8s.io/yaml here lets the custom types embed core types
158+
// that only have json tags, notably types from k8s.io/apimachinery/pkg/apis/meta/v1
159+
if err := k8syaml.Unmarshal([]byte(src.MustString()), api); err != nil {
160+
if schemaValidationError != nil {
161+
// if we got a validation error, report it instead as it is likely a nicer version of the same message
162+
return schemaValidationError
163+
}
143164
return errors.Wrap(err)
144165
}
145166

@@ -150,7 +171,25 @@ func LoadFunctionConfig(src *yaml.RNode, api interface{}) error {
150171
}
151172

152173
if v, ok := api.(Validator); ok {
153-
return v.Validate()
174+
return combineErrors(schemaValidationError, v.Validate())
175+
}
176+
return nil
177+
}
178+
179+
func combineErrors(schemaErr, customErr error) error {
180+
combined := validationErrors.CompositeValidationError()
181+
if compositeSchemaErr, ok := schemaErr.(*validationErrors.CompositeError); ok {
182+
combined.Errors = append(combined.Errors, compositeSchemaErr.Errors...)
183+
} else if schemaErr != nil {
184+
combined.Errors = append(combined.Errors, schemaErr)
185+
}
186+
if compositeCustomErr, ok := customErr.(*validationErrors.CompositeError); ok {
187+
combined.Errors = append(combined.Errors, compositeCustomErr.Errors...)
188+
} else if customErr != nil {
189+
combined.Errors = append(combined.Errors, customErr)
190+
}
191+
if len(combined.Errors) > 0 {
192+
return combined
154193
}
155194
return nil
156195
}

0 commit comments

Comments
 (0)