Skip to content

Commit

Permalink
feat: MultiError for invalid annotations
Browse files Browse the repository at this point in the history
- Move Validator to pkg/object/validation
- Replace ValidationError with validation.Error
- Replace MultiValidationError with generic MultiError
- Update Validator & SortObjs to use MultiError
- Add ResourceReferenceFromObjMetadata
- Rename NewResourceReference -> ResourceReferenceFromUnstructured
- Delete duplicate ResourceReference.ObjMetadata()
- Modify some error messages for consistency and clarity
- Use templating to generate some test artifacts

BREAKING CHANGE: apply-time-mutation namespace required for namespace-scoped resources
  • Loading branch information
karlkfi committed Jan 12, 2022
1 parent 0c9b214 commit f67aaa8
Show file tree
Hide file tree
Showing 22 changed files with 1,061 additions and 564 deletions.
6 changes: 3 additions & 3 deletions pkg/apply/applier.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import (
"sigs.k8s.io/cli-utils/pkg/kstatus/polling"
"sigs.k8s.io/cli-utils/pkg/kstatus/polling/engine"
"sigs.k8s.io/cli-utils/pkg/object"
"sigs.k8s.io/cli-utils/pkg/object/validation"
"sigs.k8s.io/cli-utils/pkg/ordering"
)

Expand Down Expand Up @@ -143,9 +144,8 @@ func (a *Applier) Run(ctx context.Context, invInfo inventory.InventoryInfo, obje

// Validate the resources to make sure we catch those problems early
// before anything has been updated in the cluster.
if err := (&object.Validator{
Mapper: mapper,
}).Validate(objects); err != nil {
validator := &validation.Validator{Mapper: mapper}
if err := validator.Validate(objects); err != nil {
handleError(eventChannel, err)
return
}
Expand Down
6 changes: 3 additions & 3 deletions pkg/apply/mutator/apply_time_mutator.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ func (atm *ApplyTimeMutator) Mutate(ctx context.Context, obj *unstructured.Unstr
mutated := false
reason := ""

targetRef := mutation.NewResourceReference(obj)
targetRef := mutation.ResourceReferenceFromUnstructured(obj)

if !mutation.HasAnnotation(obj) {
return mutated, reason, nil
Expand Down Expand Up @@ -177,7 +177,7 @@ func (atm *ApplyTimeMutator) getObject(ctx context.Context, mapping *meta.RESTMa
if ref.Kind == "" {
return nil, fmt.Errorf("invalid source reference: empty kind")
}
id := ref.ObjMetadata()
id := ref.ToObjMetadata()

// get resource from cache
if atm.ResourceCache != nil {
Expand Down Expand Up @@ -225,7 +225,7 @@ func computeStatus(obj *unstructured.Unstructured) cache.ResourceStatus {
result, err := status.Compute(obj)
if err != nil {
if klog.V(3).Enabled() {
ref := mutation.NewResourceReference(obj)
ref := mutation.ResourceReferenceFromUnstructured(obj)
klog.Info("failed to compute resource status (%s): %d", ref, err)
}
return cache.ResourceStatus{
Expand Down
2 changes: 1 addition & 1 deletion pkg/apply/mutator/apply_time_mutator_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -410,7 +410,7 @@ func TestMutate(t *testing.T) {
reason: "",
// exact error message isn't very important. Feel free to update if the error text changes.
errMsg: `failed to read annotation in resource (v1/namespaces/map-namespace/ConfigMap/map3-name): ` +
`failed to parse apply-time-mutation annotation: "not a valid substitution list": ` +
`failed to parse apply-time-mutation annotation: ` +
`error unmarshaling JSON: ` +
`while decoding JSON: ` +
`json: cannot unmarshal string into Go value of type mutation.ApplyTimeMutation`,
Expand Down
10 changes: 5 additions & 5 deletions pkg/inventory/inventory-client.go
Original file line number Diff line number Diff line change
Expand Up @@ -156,15 +156,15 @@ func (cic *ClusterInventoryClient) Replace(localInv InventoryInfo, objs object.O
}
clusterObjs, err := cic.GetClusterObjs(localInv, dryRun)
if err != nil {
return err
return fmt.Errorf("failed to read inventory objects from cluster: %w", err)
}
if objs.Equal(clusterObjs) {
klog.V(4).Infof("applied objects same as cluster inventory: do nothing")
return nil
}
clusterInv, err := cic.GetClusterInventoryInfo(localInv, dryRun)
if err != nil {
return err
return fmt.Errorf("failed to read inventory from cluster: %w", err)
}
clusterInv, err = cic.replaceInventory(clusterInv, objs)
if err != nil {
Expand All @@ -173,7 +173,7 @@ func (cic *ClusterInventoryClient) Replace(localInv InventoryInfo, objs object.O
klog.V(4).Infof("replace cluster inventory: %s/%s", clusterInv.GetNamespace(), clusterInv.GetName())
klog.V(4).Infof("replace cluster inventory %d objects", len(objs))
if err := cic.applyInventoryObj(clusterInv, dryRun); err != nil {
return err
return fmt.Errorf("failed to write updated inventory to cluster: %w", err)
}
return nil
}
Expand Down Expand Up @@ -225,7 +225,7 @@ func (cic *ClusterInventoryClient) GetClusterObjs(localInv InventoryInfo, dryRun
var objs object.ObjMetadataSet
clusterInv, err := cic.GetClusterInventoryInfo(localInv, dryRun)
if err != nil {
return objs, err
return objs, fmt.Errorf("failed to read inventory from cluster: %w", err)
}
// First time; no inventory obj yet.
if clusterInv == nil {
Expand All @@ -247,7 +247,7 @@ func (cic *ClusterInventoryClient) GetClusterObjs(localInv InventoryInfo, dryRun
func (cic *ClusterInventoryClient) GetClusterInventoryInfo(inv InventoryInfo, dryRun common.DryRunStrategy) (*unstructured.Unstructured, error) {
clusterInvObjects, err := cic.GetClusterInventoryObjs(inv)
if err != nil {
return nil, err
return nil, fmt.Errorf("failed to read inventory objects from cluster: %w", err)
}

var clusterInv *unstructured.Unstructured
Expand Down
10 changes: 6 additions & 4 deletions pkg/jsonpath/jsonpath.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,10 @@ import (
"github.com/spyzhov/ajson"
)

// Get evaluates the yq expression to extract values from the input map.
// Get evaluates the JSONPath expression to extract values from the input map.
// Returns the node values that were found (zero or more), or an error.
// For details about the yq expression language, see: https://mikefarah.gitbook.io/yq/
// For details about the JSONPath expression language, see:
// https://goessner.net/articles/JsonPath/
func Get(obj map[string]interface{}, expression string) ([]interface{}, error) {
// format input object as json for input into jsonpath library
jsonBytes, err := json.Marshal(obj)
Expand Down Expand Up @@ -65,9 +66,10 @@ func Get(obj map[string]interface{}, expression string) ([]interface{}, error) {
return result, nil
}

// Set evaluates the yq expression to set a value in the input map.
// Set evaluates the JSONPath expression to set a value in the input map.
// Returns the number of matching nodes that were updated, or an error.
// For details about the yq expression language, see: https://mikefarah.gitbook.io/yq/
// For details about the JSONPath expression language, see:
// https://goessner.net/articles/JsonPath/
func Set(obj map[string]interface{}, expression string, value interface{}) (int, error) {
// format input object as json for input into jsonpath library
jsonBytes, err := json.Marshal(obj)
Expand Down
91 changes: 91 additions & 0 deletions pkg/multierror/multierror.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 multierror

import (
"fmt"
"strings"
)

const Prefix = "- "
const Indent = " "

type Interface interface {
Errors() []error
}

// New returns a new MultiError wrapping the specified error list.
func New(causes ...error) *MultiError {
return &MultiError{
Causes: causes,
}
}

// MultiError wraps multiple errors and formats them for multi-line output.
type MultiError struct {
Causes []error
}

func (mve *MultiError) Errors() []error {
return mve.Causes
}

func (mve *MultiError) Error() string {
if len(mve.Causes) == 1 {
return mve.Causes[0].Error()
}
var b strings.Builder
_, _ = fmt.Fprintf(&b, "%d errors:\n", len(mve.Causes))
for _, err := range mve.Causes {
_, _ = fmt.Fprintf(&b, "%s\n", formatError(err))
}
return b.String()
}

func formatError(err error) string {
lines := strings.Split(err.Error(), "\n")
return Prefix + strings.Join(lines, fmt.Sprintf("\n%s", Indent))
}

// Wrap merges zero or more errors and/or MultiErrors into one error.
// MultiErrors are recursively unwrapped to reduce depth.
// If only one error is received, that error is returned without a wrapper.
func Wrap(errs ...error) error {
if len(errs) == 0 {
return nil
}
errs = Unwrap(errs...)
var err error
switch {
case len(errs) == 0:
err = nil
case len(errs) == 1:
err = errs[0]
case len(errs) > 1:
err = &MultiError{
Causes: errs,
}
}
return err
}

// Unwrap flattens zero or more errors and/or MultiErrors into a list of errors.
// MultiErrors are recursively unwrapped to reduce depth.
func Unwrap(errs ...error) []error {
if len(errs) == 0 {
return nil
}
var errors []error
for _, err := range errs {
if mve, ok := err.(Interface); ok {
// Recursively unwrap MultiErrors
for _, cause := range mve.Errors() {
errors = append(errors, Unwrap(cause)...)
}
} else {
errors = append(errors, err)
}
}
return errors
}
4 changes: 2 additions & 2 deletions pkg/object/dependson/annotation.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ func ReadAnnotation(u *unstructured.Unstructured) (DependencySet, error) {

depSet, err := ParseDependencySet(depSetStr)
if err != nil {
return depSet, fmt.Errorf("failed to parse dependency set: %w", err)
return depSet, fmt.Errorf("failed to parse depends-on annotation: %w", err)
}
return depSet, nil
}
Expand All @@ -61,7 +61,7 @@ func WriteAnnotation(obj *unstructured.Unstructured, depSet DependencySet) error

depSetStr, err := FormatDependencySet(depSet)
if err != nil {
return fmt.Errorf("failed to format dependency set: %w", err)
return fmt.Errorf("failed to format depends-on annotation: %w", err)
}

a := obj.GetAnnotations()
Expand Down
4 changes: 2 additions & 2 deletions pkg/object/dependson/strings.go
Original file line number Diff line number Diff line change
Expand Up @@ -114,8 +114,8 @@ func ParseObjMetadata(objStr string) (object.ObjMetadata, error) {
fields := strings.Split(objStr, fieldSeparator)

if len(fields) != numFieldsClusterScoped && len(fields) != numFieldsNamespacedScoped {
return obj, fmt.Errorf("too many fields (expected %d or %d): %q",
numFieldsClusterScoped, numFieldsNamespacedScoped, objStr)
return obj, fmt.Errorf("expected %d or %d fields, found %d: %q",
numFieldsClusterScoped, numFieldsNamespacedScoped, len(fields), objStr)
}

group = fields[0]
Expand Down
Loading

0 comments on commit f67aaa8

Please sign in to comment.