update results field of ResourceList to implement function spec v1

This commit is contained in:
natasha41575
2021-10-19 15:20:00 -07:00
parent 71b978da1a
commit 3ea8b79925
4 changed files with 103 additions and 110 deletions

View File

@@ -296,7 +296,7 @@ functionConfig:
func ExampleBuild_validate() { func ExampleBuild_validate() {
fn := func(rl *framework.ResourceList) error { fn := func(rl *framework.ResourceList) error {
// validation results // validation results
var validationResults []framework.ResultItem var validationResults framework.Results
// validate that each Deployment resource has spec.replicas set // validate that each Deployment resource has spec.replicas set
for i := range rl.Items { for i := range rl.Items {
@@ -319,7 +319,7 @@ func ExampleBuild_validate() {
if r != nil { if r != nil {
continue continue
} }
validationResults = append(validationResults, framework.ResultItem{ validationResults = append(validationResults, &framework.Result{
Severity: framework.Error, Severity: framework.Error,
Message: "field is required", Message: "field is required",
ResourceRef: yaml.ResourceIdentifier{ ResourceRef: yaml.ResourceIdentifier{
@@ -327,20 +327,17 @@ func ExampleBuild_validate() {
NameMeta: meta.ObjectMeta.NameMeta, NameMeta: meta.ObjectMeta.NameMeta,
}, },
Field: framework.Field{ Field: framework.Field{
Path: "spec.replicas", Path: "spec.replicas",
SuggestedValue: "1", ProposedValue: "1",
}, },
}) })
} }
if len(validationResults) > 0 { if len(validationResults) > 0 {
rl.Result = &framework.Result{ rl.Results = validationResults
Name: "replicas-validator",
Items: validationResults,
}
} }
return rl.Result return rl.Results
} }
cmd := command.Build(framework.ResourceListProcessorFunc(fn), command.StandaloneDisabled, true) cmd := command.Build(framework.ResourceListProcessorFunc(fn), command.StandaloneDisabled, true)
@@ -370,15 +367,13 @@ items:
// metadata: // metadata:
// name: foo // name: foo
// results: // results:
// name: replicas-validator // - message: field is required
// items: // severity: error
// - message: field is required // resourceRef:
// severity: error // apiVersion: apps/v1
// resourceRef: // kind: Deployment
// apiVersion: apps/v1 // name: foo
// kind: Deployment // field:
// name: foo // path: spec.replicas
// field: // proposedValue: "1"
// path: spec.replicas
// suggestedValue: "1"
} }

View File

@@ -17,6 +17,22 @@ import (
// This framework facilitates building functions that receive and emit ResourceLists, // This framework facilitates building functions that receive and emit ResourceLists,
// as required by the specification. // as required by the specification.
type ResourceList struct { type ResourceList struct {
// Items is the ResourceList.items input and output value.
//
// e.g. given the function input:
//
// kind: ResourceList
// items:
// - kind: Deployment
// ...
// - kind: Service
// ...
//
// Items will be a slice containing the Deployment and Service resources
// Mutating functions will alter this field during processing.
// This field is required.
Items []*yaml.RNode `yaml:"items" json:"items"`
// FunctionConfig is the ResourceList.functionConfig input value. // FunctionConfig is the ResourceList.functionConfig input value.
// //
// e.g. given the input: // e.g. given the input:
@@ -33,25 +49,10 @@ type ResourceList struct {
// foo: var // foo: var
FunctionConfig *yaml.RNode `yaml:"functionConfig" json:"functionConfig"` FunctionConfig *yaml.RNode `yaml:"functionConfig" json:"functionConfig"`
// Items is the ResourceList.items input and output value. // Results is ResourceList.results output value.
//
// e.g. given the function input:
//
// kind: ResourceList
// items:
// - kind: Deployment
// ...
// - kind: Service
// ...
//
// Items will be a slice containing the Deployment and Service resources
// Mutating functions will alter this field during processing.
Items []*yaml.RNode `yaml:"items" json:"items"`
// Result is ResourceList.result output value.
// Validating functions can optionally use this field to communicate structured // Validating functions can optionally use this field to communicate structured
// validation error data to downstream functions. // validation error data to downstream functions.
Result *Result `yaml:"results" json:"results"` Results Results `yaml:"results" json:"results"`
} }
// ResourceListProcessor is implemented by configuration functions built with this framework // ResourceListProcessor is implemented by configuration functions built with this framework
@@ -119,8 +120,8 @@ func Execute(p ResourceListProcessor, rlSource *kio.ByteReadWriter) error {
// Write the results // Write the results
// Set the ResourceList.results for validating functions // Set the ResourceList.results for validating functions
if rl.Result != nil && len(rl.Result.Items) > 0 { if len(rl.Results) > 0 {
b, err := yaml.Marshal(rl.Result) b, err := yaml.Marshal(rl.Results)
if err != nil { if err != nil {
return errors.Wrap(err) return errors.Wrap(err)
} }

View File

@@ -16,33 +16,31 @@ import (
func TestExecute_Result(t *testing.T) { func TestExecute_Result(t *testing.T) {
p := framework.ResourceListProcessorFunc(func(rl *framework.ResourceList) error { p := framework.ResourceListProcessorFunc(func(rl *framework.ResourceList) error {
err := &framework.Result{ err := &framework.Results{
Name: "Incompatible config", {
Items: []framework.ResultItem{ Message: "bad value for replicas",
{ Severity: framework.Error,
Message: "bad value for replicas", ResourceRef: yaml.ResourceIdentifier{
Severity: framework.Error, TypeMeta: yaml.TypeMeta{APIVersion: "v1", Kind: "Deployment"},
ResourceRef: yaml.ResourceIdentifier{ NameMeta: yaml.NameMeta{Name: "tester", Namespace: "default"},
TypeMeta: yaml.TypeMeta{APIVersion: "v1", Kind: "Deployment"},
NameMeta: yaml.NameMeta{Name: "tester", Namespace: "default"},
},
Field: framework.Field{
Path: ".spec.Replicas",
CurrentValue: "0",
SuggestedValue: "3",
},
File: framework.File{
Path: "/path/to/deployment.yaml",
Index: 0,
},
}, },
{ Field: framework.Field{
Message: "some error", Path: ".spec.Replicas",
Severity: framework.Error, CurrentValue: "0",
ProposedValue: "3",
},
File: framework.File{
Path: "/path/to/deployment.yaml",
Index: 0,
}, },
}, },
{
Message: "some error",
Severity: framework.Error,
Tags: map[string]string{"foo": "bar"},
},
} }
rl.Result = err rl.Results = *err
return err return err
}) })
out := new(bytes.Buffer) out := new(bytes.Buffer)
@@ -62,7 +60,7 @@ items:
assert.EqualError(t, err, `[error] v1/Deployment/default/tester .spec.Replicas: bad value for replicas assert.EqualError(t, err, `[error] v1/Deployment/default/tester .spec.Replicas: bad value for replicas
[error] : some error`) [error] : some error`)
assert.Equal(t, 1, err.(*framework.Result).ExitCode()) assert.Equal(t, 1, err.(*framework.Results).ExitCode())
assert.Equal(t, `apiVersion: config.kubernetes.io/v1 assert.Equal(t, `apiVersion: config.kubernetes.io/v1
kind: ResourceList kind: ResourceList
items: items:
@@ -74,21 +72,21 @@ items:
spec: spec:
replicas: 0 replicas: 0
results: results:
name: Incompatible config - message: bad value for replicas
items: severity: error
- message: bad value for replicas resourceRef:
severity: error apiVersion: v1
resourceRef: kind: Deployment
apiVersion: v1 name: tester
kind: Deployment namespace: default
name: tester field:
namespace: default path: .spec.Replicas
field: currentValue: "0"
path: .spec.Replicas proposedValue: "3"
currentValue: "0" file:
suggestedValue: "3" path: /path/to/deployment.yaml
file: - message: some error
path: /path/to/deployment.yaml severity: error
- message: some error tags:
severity: error`, strings.TrimSpace(out.String())) foo: bar`, strings.TrimSpace(out.String()))
} }

View File

@@ -23,25 +23,30 @@ const (
) )
// ResultItem defines a validation result // ResultItem defines a validation result
type ResultItem struct { type Result struct {
// Message is a human readable message // Message is a human readable message. This field is required.
Message string `yaml:"message,omitempty"` Message string `yaml:"message,omitempty" json:"message,omitempty"`
// Severity is the severity of this result // Severity is the severity of this result
Severity Severity `yaml:"severity,omitempty"` Severity Severity `yaml:"severity,omitempty" json:"severity,omitempty"`
// ResourceRef is a reference to a resource // ResourceRef is a reference to a resource.
ResourceRef yaml.ResourceIdentifier `yaml:"resourceRef,omitempty"` // Required fields: apiVersion, kind, name.
ResourceRef yaml.ResourceIdentifier `yaml:"resourceRef,omitempty" json:"resourceRef,omitempty"`
// Field is a reference to the field in a resource this result refers to // Field is a reference to the field in a resource this result refers to
Field Field `yaml:"field,omitempty"` Field Field `yaml:"field,omitempty" json:"field,omitempty"`
// File references a file containing the resource this result refers to // File references a file containing the resource this result refers to
File File `yaml:"file,omitempty"` File File `yaml:"file,omitempty" json:"file,omitempty"`
// Tags is an unstructured key value map stored with a result that may be set
// by external tools to store and retrieve arbitrary metadata
Tags map[string]string `yaml:"tags,omitempty" json:"tags,omitempty"`
} }
// String provides a human-readable message for the result item // String provides a human-readable message for the result item
func (i ResultItem) String() string { func (i Result) String() string {
identifier := i.ResourceRef identifier := i.ResourceRef
var idStringList []string var idStringList []string
if identifier.APIVersion != "" { if identifier.APIVersion != "" {
@@ -65,47 +70,41 @@ func (i ResultItem) String() string {
// File references a file containing a resource // File references a file containing a resource
type File struct { type File struct {
// Path is relative path to the file containing the resource // Path is relative path to the file containing the resource.
Path string `yaml:"path,omitempty"` // This field is required.
Path string `yaml:"path,omitempty" json:"path,omitempty"`
// Index is the index into the file containing the resource // Index is the index into the file containing the resource
// (i.e. if there are multiple resources in a single file) // (i.e. if there are multiple resources in a single file)
Index int `yaml:"index,omitempty"` Index int `yaml:"index,omitempty" json:"index,omitempty"`
} }
// Field references a field in a resource // Field references a field in a resource
type Field struct { type Field struct {
// Path is the field path // Path is the field path. This field is required.
Path string `yaml:"path,omitempty"` Path string `yaml:"path,omitempty" json:"path,omitempty"`
// CurrentValue is the current field value // CurrentValue is the current field value
CurrentValue string `yaml:"currentValue,omitempty"` CurrentValue interface{} `yaml:"currentValue,omitempty" json:"currentValue,omitempty"`
// SuggestedValue is the suggested field value // ProposedValue is the proposed value of the field to fix an issue.
SuggestedValue string `yaml:"suggestedValue,omitempty"` ProposedValue interface{} `yaml:"proposedValue,omitempty" json:"proposedValue,omitempty"`
} }
// Result defines a function result which will be set on the emitted ResourceList type Results []*Result
type Result struct {
// Name is the name of the function creating the result
Name string `yaml:"name,omitempty"`
// Items are the individual results // Error enables Results to be returned as an error
Items []ResultItem `yaml:"items,omitempty"` func (e Results) Error() string {
}
// Error enables a Result to be returned as an error
func (e Result) Error() string {
var msgs []string var msgs []string
for _, i := range e.Items { for _, i := range e {
msgs = append(msgs, i.String()) msgs = append(msgs, i.String())
} }
return strings.Join(msgs, "\n\n") return strings.Join(msgs, "\n\n")
} }
// ExitCode provides the exit code based on the result's severity // ExitCode provides the exit code based on the result's severity
func (e Result) ExitCode() int { func (e Results) ExitCode() int {
for _, i := range e.Items { for _, i := range e {
if i.Severity == Error { if i.Severity == Error {
return 1 return 1
} }