From 37fb2a6f496a4875f1487713a56ff22bbbec85aa Mon Sep 17 00:00:00 2001 From: Brian Kane Date: Wed, 21 Jan 2026 10:37:07 +0000 Subject: [PATCH] Feat: 7024 Enable custom errors in components similar to traits (#7028) Signed-off-by: Brian Kane --- pkg/cue/definition/template.go | 124 +++++++++++++++++----------- pkg/cue/definition/template_test.go | 33 ++++++++ 2 files changed, 109 insertions(+), 48 deletions(-) diff --git a/pkg/cue/definition/template.go b/pkg/cue/definition/template.go index 4cbe17535..3374a4df4 100644 --- a/pkg/cue/definition/template.go +++ b/pkg/cue/definition/template.go @@ -24,6 +24,7 @@ import ( "strings" "k8s.io/apiserver/pkg/util/feature" + "k8s.io/klog/v2" "github.com/oam-dev/kubevela/pkg/cue/definition/health" "github.com/oam-dev/kubevela/pkg/features" @@ -130,11 +131,35 @@ func (wd *workloadDef) Complete(ctx process.Context, abstractTemplate string, pa return errors.WithMessagef(err, "failed to compile workload %s after merge parameter and context", wd.name) } - if err := val.Validate(); err != nil { - if fmtErr := FormatCUEError(err, "validation failed for", "workload", wd.name, &val); fmtErr != nil { - return fmtErr + var userErrors []string + if errs := val.LookupPath(value.FieldPath(ErrsFieldName)); errs.Exists() { + if err := errs.Decode(&userErrors); err != nil { + klog.Warningf("Workload definition '%s' has malformed 'errs' field (expected []string): %v. Custom error reporting will be skipped.", wd.name, err) } - return fmt.Errorf("validation failed for workload %s: %w", wd.name, err) + } + + validationErr := val.Validate() + + if validationErr != nil || len(userErrors) > 0 { + var result strings.Builder + result.WriteString(fmt.Sprintf("validation failed for workload %s:", wd.name)) + + if len(userErrors) > 0 { + result.WriteString("\n\nUser Errors:\n") + for _, e := range userErrors { + result.WriteString(fmt.Sprintf(" %s\n", e)) + } + } + + if validationErr != nil { + if fmtErr := FormatCUEError(validationErr, "validation failed for", "workload", wd.name, &val); fmtErr != nil { + errMsg := fmtErr.Error() + errMsg = strings.TrimPrefix(errMsg, fmt.Sprintf("validation failed for workload %s:", wd.name)) + result.WriteString(errMsg) + } + } + + return errors.New(strings.TrimRight(result.String(), "\n")) } output := val.LookupPath(value.FieldPath(OutputFieldName)) @@ -290,11 +315,35 @@ func (td *traitDef) Complete(ctx process.Context, abstractTemplate string, param return errors.WithMessagef(err, "failed to compile trait %s after merge parameter and context", td.name) } - if err := val.Validate(); err != nil { - if fmtErr := FormatCUEError(err, "validation failed for", "trait", td.name, &val); fmtErr != nil { - return fmtErr + var userErrors []string + if errs := val.LookupPath(value.FieldPath(ErrsFieldName)); errs.Exists() { + if err := errs.Decode(&userErrors); err != nil { + klog.Warningf("Trait definition '%s' has malformed 'errs' field (expected []string): %v. Custom error reporting will be skipped.", td.name, err) } - return fmt.Errorf("validation failed for trait %s: %w", td.name, err) + } + + validationErr := val.Validate() + + if validationErr != nil || len(userErrors) > 0 { + var result strings.Builder + result.WriteString(fmt.Sprintf("validation failed for trait %s:", td.name)) + + if len(userErrors) > 0 { + result.WriteString("\n\nUser Errors:\n") + for _, e := range userErrors { + result.WriteString(fmt.Sprintf(" %s\n", e)) + } + } + + if validationErr != nil { + if fmtErr := FormatCUEError(validationErr, "validation failed for", "trait", td.name, &val); fmtErr != nil { + errMsg := fmtErr.Error() + errMsg = strings.TrimPrefix(errMsg, fmt.Sprintf("validation failed for trait %s:", td.name)) + result.WriteString(errMsg) + } + } + + return errors.New(strings.TrimRight(result.String(), "\n")) } processing := val.LookupPath(value.FieldPath("processing")) @@ -348,13 +397,6 @@ func (td *traitDef) Complete(ctx process.Context, abstractTemplate string, param } } - errs := val.LookupPath(value.FieldPath(ErrsFieldName)) - if errs.Exists() { - if err := parseErrors(errs); err != nil { - return err - } - } - return nil } @@ -408,17 +450,6 @@ func injectOutputStatusIntoBaseContext(ctx process.Context, c string, statusByte return c } -func parseErrors(errs cue.Value) error { - if it, e := errs.List(); e == nil { - for it.Next() { - if s, err := it.Value().String(); err == nil && s != "" { - return errors.Errorf("%s", s) - } - } - } - return nil -} - // GetCommonLabels will convert context based labels to OAM standard labels func GetCommonLabels(contextLabels map[string]string) map[string]string { var commonLabels = map[string]string{} @@ -535,33 +566,30 @@ func getResourceFromObj(ctx context.Context, pctx process.Context, obj *unstruct // FormatCUEError formats CUE errors in a user-friendly grouped format func FormatCUEError(err error, messagePrefix string, entityType, entityName string, val ...*cue.Value) error { - if err == nil { - return nil - } - var allParamErrors = make(map[string]bool) var allTemplateErrors = make(map[string]bool) - errList := cueerrors.Errors(err) - for _, e := range errList { - errMsg := e.Error() - if strings.HasPrefix(errMsg, "parameter.") { - allParamErrors[errMsg] = true - } else { - allTemplateErrors[errMsg] = true + if err != nil { + errList := cueerrors.Errors(err) + for _, e := range errList { + errMsg := e.Error() + if strings.HasPrefix(errMsg, "parameter.") { + allParamErrors[errMsg] = true + } else { + allTemplateErrors[errMsg] = true + } } - } - // Run concrete validation if val provided to catch missing parameters - if len(val) > 0 && val[0] != nil { - if concreteErr := val[0].Validate(cue.Concrete(true)); concreteErr != nil { - concreteErrList := cueerrors.Errors(concreteErr) - for _, e := range concreteErrList { - errMsg := e.Error() - if strings.HasPrefix(errMsg, "parameter.") { - allParamErrors[errMsg] = true - } else { - allTemplateErrors[errMsg] = true + if len(val) > 0 && val[0] != nil { + if concreteErr := val[0].Validate(cue.Concrete(true)); concreteErr != nil { + concreteErrList := cueerrors.Errors(concreteErr) + for _, e := range concreteErrList { + errMsg := e.Error() + if strings.HasPrefix(errMsg, "parameter.") { + allParamErrors[errMsg] = true + } else { + allTemplateErrors[errMsg] = true + } } } } diff --git a/pkg/cue/definition/template_test.go b/pkg/cue/definition/template_test.go index 19ced6e35..9e908e8a7 100644 --- a/pkg/cue/definition/template_test.go +++ b/pkg/cue/definition/template_test.go @@ -240,6 +240,39 @@ output:{ "kind": "Ingress", }}, }, + "using errs field in workload": { + workloadTemplate: ` +output: { + apiVersion: "apps/v1" + kind: "Deployment" + metadata: name: context.name +} +errs: parameter.errs +parameter: { errs: [...string] }`, + params: map[string]interface{}{ + "errs": []string{"custom workload error"}, + }, + hasCompileErr: true, + }, + "user errors and validation errors together": { + workloadTemplate: ` +output: { + apiVersion: "apps/v1" + kind: "Deployment" + metadata: name: context.name + spec: replicas: parameter.replicas +} +errs: (if parameter.replicas < 1 {["replicas must be at least 1"]} else {[]}) +parameter: { + replicas: int + required: string // This will cause a validation error +}`, + params: map[string]interface{}{ + "replicas": 0, + // missing "required" field + }, + hasCompileErr: true, + }, } for _, v := range testCases {