Files
kubevela/pkg/cue/definition/template.go
5a44b5f10c Feat: auto remediate cue issues (#7199)
* feat(cue/upgrade): auto-remediate legacy CUE syntax at render time

Transparently rewrite CUE templates that use deprecated list arithmetic
(+, *) and conflicting field names (error) so that older definitions
continue to work with CUE ≥ v0.14 (KubeVela ≥ 1.11).

- CUEUpgradeFunc registry with ID, CUE/KubeVela version guards, precheck,
  and upgrade function fields
- upgradeListConcatenation: rewrites list1+list2 → list.Concat([list1,list2])
  and list*n → list.Repeat(list, n); adds "list" import as needed
- collectAddChain + extractListConcatArgs: flatten left-associative + chains
  and existing list.Concat([...]) leaves into a single flat call, so both
  fresh chains (a+b+c+d) and partially-upgraded chains produce one
  list.Concat([a,b,c,d]) with no nesting across repeated passes
- upgradeErrorFieldLabel: rewrites unquoted `error` field labels to "error"
  to avoid conflict with the CUE 0.14 built-in; precheck uses a tighter
  \berror\s*: regex to avoid false positives on identifiers like errorMessage
- EnsureCueVersionCompatibility: single entry point used at render time;
  LRU cache with TTL eviction, Prometheus metrics, feature flag
- ParseVersion: regex anchored to reject garbage suffixes (e.g. "1.11foo")
  while accepting pre-release+build metadata (e.g. "v1.13.0-alpha.1+dev")

- template.go: call EnsureCueVersionCompatibility for every template area
  (main, health, custom status, status detail) with correct DefinitionKind
  derived from which definition pointer is non-nil
- validate.go: upgrade policy templates before compiling in
  validateNoRequiredParameters

- `vela def upgrade FILE [-o OUTPUT]`: upgrades a single .cue file
- `vela def upgrade FILE --validate [--quiet]`: exit 1 if upgrade needed
- `vela def compat definitions` / `vela def compat applications`: scan
  cluster definitions/apps for compat issues; output as table or YAML
- Cyclomatic complexity kept below threshold by extracting scanDefinitions,
  scanDefRevisions, buildDefCompatReport, scanApplications, scanAppRevision
  as standalone functions with options structs
- revisionNum() helper for numeric vN comparison (avoids lexicographic bugs)
- mergeImports() dedup helper shared by ToCUEString and formatCUEString
- ANSI escape sequences replaced with fatih/color for portability
- goconst: "yaml" → outputFormatYAML named constant throughout

- Component, trait, and policy definition validating handlers: removed
  spurious obj.Name argument from fmt.Sprintf in warning messages

- FromCUEString: only prepend importString to the stored template when
  imports are non-empty; empty importString ("\n") was causing a leading
  newline that made yaml.v3 use |2 block scalar on every generated YAML

- gen_sdk testdata: removed unused imports (vela/op, encoding/base64) from
  one_of.cue that were exposed by our importString+templateString change
- e2e test: fix flaky trait-order assertion using ContainElements instead
  of index-based equality

Upgraded all built-in .cue files that used deprecated list arithmetic:
- vela-templates/definitions/internal/component/cron-task.cue
- vela-templates/definitions/internal/trait/command.cue
- vela-templates/definitions/internal/trait/container-ports.cue
- vela-templates/definitions/internal/trait/env.cue
- vela-templates/definitions/internal/trait/init-container.cue

Removed unused stdlib imports that caused `def gen-api` to fail:
- vela-templates/definitions/internal/workflowstep/apply-deployment.cue
- vela-templates/definitions/internal/workflowstep/apply-terraform-provider.cue
- vela-templates/definitions/internal/workflowstep/build-push-image.cue

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Brian Kane <briankane1@gmail.com>

* fix(cue/upgrade): address PR review comments

- sync.atomic.Pointer for compatCache to fix data race on reinit
- SummaryVec → HistogramVec for both duration metrics (aggregatable
  across HA replicas); buckets tuned to sub-millisecond upgrade path
  and millisecond render path respectively
- errorFieldLabelRe: extend to match optional (?) and required (!)
  field constraint markers before the colon
- cue-compatibility-cache-size: clamp negative values to 0 (disabled)
  with warning log; document 0=disabled in flag help; cache put is
  no-op when capacity <= 0
- webhook: replace RequiresUpgrade+EnsureCueVersionCompatibility double
  parse with single EnsureCueVersionCompatibility call; use string
  comparison to detect upgrade and emit warning
- def compat: log warning when ApplicationRevision fetch fails instead
  of silently skipping (partial results are preserved)
- e2e: only delete definitions in DeferCleanup if this test created
  them (avoid deleting pre-existing shared resources)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Brian Kane <briankane1@gmail.com>

* fix(cue/upgrade): address further PR review comments

- EnsureCueVersionCompatibility: return (string, bool) where bool
  indicates semantic upgrades were applied (len(applied)>0), not
  string inequality — prevents false-positive warnings from
  formatting-only normalisation; update all call sites
- webhook handlers (component, trait, policy): switch from
  RequiresUpgrade+EnsureCueVersionCompatibility double-call to single
  EnsureCueVersionCompatibility call using wasUpgraded bool; remove
  now-unused strings imports
- cache: skip eviction goroutine when capacity==0 (disabled); set
  compatCacheCancel=nil on disabled path to avoid stale cancel on
  next InitCompatibilityCache call
- e2e: replace boolean ownership tracking with createAndTrack helper
  that checks pre-existence via Get before Create, eliminating both
  the ambiguous-create leak and the boilerplate booleans

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Brian Kane <briankane1@gmail.com>

* fix: address reviewer comments — cache determinism, e2e ownership race

- cache: store normalised string in compatEntry.upgraded even when no
  semantic fixes were applied, so cache-hit and cache-miss paths return
  identical output (fixes non-deterministic behaviour flagged in review)
- upgrade: return entry.upgraded on the requiresUpgrade=false cache-hit
  path instead of the raw input cueStr
- e2e: replace GET-then-CREATE ownership inference with atomic CREATE-
  first pattern; err==nil means we created it (register DeferCleanup),
  IsAlreadyExists means it pre-existed (skip cleanup), eliminating the
  GET/CREATE race window that could misattribute ownership

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Brian Kane <briankane1@gmail.com>

---------

Signed-off-by: Brian Kane <briankane1@gmail.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-26 05:57:23 -07:00

654 lines
21 KiB
Go

/*
Copyright 2021 The KubeVela Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package definition
import (
"context"
"encoding/json"
"fmt"
"sort"
"strings"
"time"
"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"
velacuex "github.com/oam-dev/kubevela/pkg/cue/cuex"
"cuelang.org/go/cue"
cueerrors "cuelang.org/go/cue/errors"
"github.com/kubevela/pkg/multicluster"
"github.com/pkg/errors"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"sigs.k8s.io/controller-runtime/pkg/client"
"github.com/kubevela/workflow/pkg/cue/model"
"github.com/kubevela/workflow/pkg/cue/model/sets"
"github.com/kubevela/workflow/pkg/cue/model/value"
"github.com/kubevela/workflow/pkg/cue/process"
velaprocess "github.com/oam-dev/kubevela/pkg/cue/process"
"github.com/oam-dev/kubevela/pkg/cue/task"
"github.com/oam-dev/kubevela/pkg/cue/upgrade"
"github.com/oam-dev/kubevela/pkg/oam"
"github.com/oam-dev/kubevela/pkg/oam/util"
)
const (
// OutputFieldName is the name of the struct contains the CR data
OutputFieldName = velaprocess.OutputFieldName
// OutputsFieldName is the name of the struct contains the map[string]CR data
OutputsFieldName = velaprocess.OutputsFieldName
// PatchFieldName is the name of the struct contains the patch of CR data
PatchFieldName = "patch"
// PatchOutputsFieldName is the name of the struct contains the patch of outputs CR data
PatchOutputsFieldName = "patchOutputs"
// ErrsFieldName check if errors contained in the cue
ErrsFieldName = "errs"
// TemplateContextPrefix is the base prefix for storing templates in context
TemplateContextPrefix = "template-context-"
)
// GetWorkloadTemplateKey returns the context key for storing workload templates
func GetWorkloadTemplateKey(name string) string {
return TemplateContextPrefix + "workload-" + name
}
// GetTraitTemplateKey returns the context key for storing trait templates
func GetTraitTemplateKey(name string) string {
return TemplateContextPrefix + "trait-" + name
}
const (
// AuxiliaryWorkload defines the extra workload obj from a workloadDefinition,
// e.g. a workload composed by deployment and service, the service will be marked as AuxiliaryWorkload
AuxiliaryWorkload = "AuxiliaryWorkload"
)
// AbstractEngine defines Definition's Render interface
type AbstractEngine interface {
Complete(ctx process.Context, abstractTemplate string, params interface{}) error
Status(templateContext map[string]interface{}, request *health.StatusRequest) (*health.StatusResult, error)
GetTemplateContext(ctx process.Context, cli client.Client, accessor util.NamespaceAccessor) (map[string]interface{}, error)
}
type def struct {
name string
}
type workloadDef struct {
def
}
// NewWorkloadAbstractEngine create Workload Definition AbstractEngine
func NewWorkloadAbstractEngine(name string) AbstractEngine {
return &workloadDef{
def: def{
name: name,
},
}
}
// Complete do workload definition's rendering
func (wd *workloadDef) Complete(ctx process.Context, abstractTemplate string, params interface{}) (retErr error) {
start := time.Now()
defer func() {
status := "ok"
if retErr != nil {
status = "error"
}
CUERenderDuration.WithLabelValues(string(upgrade.ComponentKind), status).Observe(time.Since(start).Seconds())
}()
var paramFile = velaprocess.ParameterFieldName + ": {}"
if params != nil {
bt, err := json.Marshal(params)
if err != nil {
return errors.WithMessagef(err, "marshal parameter of workload %s", wd.name)
}
if string(bt) != "null" {
paramFile = fmt.Sprintf("%s: %s", velaprocess.ParameterFieldName, string(bt))
}
}
c, err := ctx.BaseContextFile()
if err != nil {
return err
}
abstractTemplate, _ = upgrade.EnsureCueVersionCompatibility(abstractTemplate, wd.name, upgrade.ComponentKind, upgrade.TemplateAreaMain)
val, err := velacuex.WorkloadCompiler.Get().CompileString(ctx.GetCtx(), strings.Join([]string{
renderTemplate(abstractTemplate), paramFile, c,
}, "\n"))
if err != nil {
return errors.WithMessagef(err, "failed to compile workload %s after merge parameter and context", wd.name)
}
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)
}
}
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))
base, err := model.NewBase(output)
if err != nil {
return errors.WithMessagef(err, "invalid output of workload %s", wd.name)
}
if err := ctx.SetBase(base); err != nil {
return err
}
// Store template for error context (use workload-specific key to avoid pollution)
ctx.PushData(GetWorkloadTemplateKey(wd.name), val)
// we will support outputs for workload composition, and it will become trait in AppConfig.
outputs := val.LookupPath(value.FieldPath(OutputsFieldName))
if !outputs.Exists() {
return nil
}
iter, err := outputs.Fields(cue.Definitions(true), cue.Hidden(true), cue.All())
if err != nil {
return errors.WithMessagef(err, "invalid outputs of workload %s", wd.name)
}
for iter.Next() {
if iter.Selector().IsDefinition() || iter.Selector().PkgPath() != "" || iter.IsOptional() {
continue
}
other, err := model.NewOther(iter.Value())
name := util.GetIteratorLabel(*iter)
if err != nil {
return errors.WithMessagef(err, "invalid outputs(%s) of workload %s", name, wd.name)
}
if err := ctx.AppendAuxiliaries(process.Auxiliary{Ins: other, Type: AuxiliaryWorkload, Name: name}); err != nil {
return err
}
}
return nil
}
func withCluster(ctx context.Context, o client.Object) context.Context {
if cluster := oam.GetCluster(o); cluster != "" {
return multicluster.WithCluster(ctx, cluster)
}
return ctx
}
func (wd *workloadDef) getTemplateContext(ctx process.Context, cli client.Reader, accessor util.NamespaceAccessor) (map[string]interface{}, error) {
baseLabels := GetBaseContextLabels(ctx)
var root = initRoot(baseLabels)
var commonLabels = GetCommonLabels(baseLabels)
base, assists := ctx.Output()
componentWorkload, err := base.Unstructured()
if err != nil {
return nil, err
}
// workload main resource will have a unique label("app.oam.dev/resourceType"="WORKLOAD") in per component/app level
_ctx := withCluster(ctx.GetCtx(), componentWorkload)
object, err := getResourceFromObj(_ctx, ctx, componentWorkload, cli, accessor.For(componentWorkload), util.MergeMapOverrideWithDst(map[string]string{
oam.LabelOAMResourceType: oam.ResourceTypeWorkload,
}, commonLabels), "")
if err != nil {
return nil, err
}
root[OutputFieldName] = object
outputs := make(map[string]interface{})
for _, assist := range assists {
if assist.Type != AuxiliaryWorkload {
continue
}
if assist.Name == "" {
return nil, errors.New("the auxiliary of workload must have a name with format 'outputs.<my-name>'")
}
traitRef, err := assist.Ins.Unstructured()
if err != nil {
return nil, err
}
// AuxiliaryWorkload will have a unique label("trait.oam.dev/resource"="name of outputs") in per component/app level
_ctx := withCluster(ctx.GetCtx(), traitRef)
object, err := getResourceFromObj(_ctx, ctx, traitRef, cli, accessor.For(traitRef), util.MergeMapOverrideWithDst(map[string]string{
oam.TraitTypeLabel: AuxiliaryWorkload,
}, commonLabels), assist.Name)
if err != nil {
return nil, err
}
outputs[assist.Name] = object
}
if len(outputs) > 0 {
root[OutputsFieldName] = outputs
}
return root, nil
}
// Status get workload status by customStatusTemplate
func (wd *workloadDef) Status(templateContext map[string]interface{}, request *health.StatusRequest) (*health.StatusResult, error) {
return health.GetStatus(templateContext, request)
}
func (wd *workloadDef) GetTemplateContext(ctx process.Context, cli client.Client, accessor util.NamespaceAccessor) (map[string]interface{}, error) {
return wd.getTemplateContext(ctx, cli, accessor)
}
type traitDef struct {
def
}
// NewTraitAbstractEngine create Trait Definition AbstractEngine
func NewTraitAbstractEngine(name string) AbstractEngine {
return &traitDef{
def: def{
name: name,
},
}
}
// Complete do trait definition's rendering
// nolint:gocyclo
func (td *traitDef) Complete(ctx process.Context, abstractTemplate string, params interface{}) (retErr error) {
start := time.Now()
defer func() {
status := "ok"
if retErr != nil {
status = "error"
}
CUERenderDuration.WithLabelValues(string(upgrade.TraitKind), status).Observe(time.Since(start).Seconds())
}()
abstractTemplate, _ = upgrade.EnsureCueVersionCompatibility(abstractTemplate, td.name, upgrade.TraitKind, upgrade.TemplateAreaMain)
buff := abstractTemplate + "\n"
if params != nil {
bt, err := json.Marshal(params)
if err != nil {
return errors.WithMessagef(err, "marshal parameter of trait %s", td.name)
}
if string(bt) != "null" {
buff += fmt.Sprintf("%s: %s\n", velaprocess.ParameterFieldName, string(bt))
}
}
multiStageEnabled := feature.DefaultMutableFeatureGate.Enabled(features.MultiStageComponentApply)
var statusBytes []byte
if multiStageEnabled {
statusBytes = outputStatusBytes(ctx)
}
c, err := ctx.BaseContextFile()
if err != nil {
return err
}
// When multi-stage is enabled, merge the existing output.status from ctx into the
// base context so downstream CUE can reference it deterministically.
if multiStageEnabled {
c = injectOutputStatusIntoBaseContext(ctx, c, statusBytes)
}
buff += c
val, err := velacuex.WorkloadCompiler.Get().CompileString(ctx.GetCtx(), buff)
if err != nil {
return errors.WithMessagef(err, "failed to compile trait %s after merge parameter and context", td.name)
}
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)
}
}
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"))
if processing.Exists() {
if val, err = task.Process(val); err != nil {
return errors.WithMessagef(err, "invalid process of trait %s", td.name)
}
}
outputs := val.LookupPath(value.FieldPath(OutputsFieldName))
if outputs.Exists() {
iter, err := outputs.Fields(cue.Definitions(true), cue.Hidden(true), cue.All())
if err != nil {
return errors.WithMessagef(err, "invalid outputs of trait %s", td.name)
}
for iter.Next() {
if iter.Selector().IsDefinition() || iter.Selector().PkgPath() != "" || iter.IsOptional() {
continue
}
other, err := model.NewOther(iter.Value())
name := util.GetIteratorLabel(*iter)
if err != nil {
return errors.WithMessagef(err, "invalid outputs(resource=%s) of trait %s", name, td.name)
}
if err := ctx.AppendAuxiliaries(process.Auxiliary{Ins: other, Type: td.name, Name: name}); err != nil {
return err
}
}
}
patcher := val.LookupPath(value.FieldPath(PatchFieldName))
base, auxiliaries := ctx.Output()
if patcher.Exists() {
if base == nil {
return fmt.Errorf("patch trait %s into an invalid workload", td.name)
}
if err := base.Unify(patcher, sets.CreateUnifyOptionsForPatcher(patcher)...); err != nil {
return errors.WithMessagef(err, "invalid patch trait %s into workload", td.name)
}
}
outputsPatcher := val.LookupPath(value.FieldPath(PatchOutputsFieldName))
if outputsPatcher.Exists() {
for _, auxiliary := range auxiliaries {
target := outputsPatcher.LookupPath(value.FieldPath(auxiliary.Name))
if !target.Exists() {
continue
}
if err = auxiliary.Ins.Unify(target); err != nil {
return errors.WithMessagef(err, "trait=%s, to=%s, invalid patch trait into auxiliary workload", td.name, auxiliary.Name)
}
}
}
return nil
}
func outputStatusBytes(ctx process.Context) []byte {
var statusBytes []byte
var outputMap map[string]interface{}
if output := ctx.GetData(OutputFieldName); output != nil {
if m, ok := output.(map[string]interface{}); ok {
outputMap = m
} else if ptr, ok := output.(*interface{}); ok && ptr != nil {
if m, ok := (*ptr).(map[string]interface{}); ok {
outputMap = m
}
}
if outputMap != nil {
if status, ok := outputMap["status"]; ok {
if b, err := json.Marshal(status); err == nil {
statusBytes = b
}
}
}
}
return statusBytes
}
func injectOutputStatusIntoBaseContext(ctx process.Context, c string, statusBytes []byte) string {
if len(statusBytes) > 0 {
// If output is an empty object, replace it with only the status field without trailing comma.
emptyOutputMarker := "\"output\":{}"
if strings.Contains(c, emptyOutputMarker) {
replacement := fmt.Sprintf("\"output\":{\"status\":%s}", string(statusBytes))
c = strings.Replace(c, emptyOutputMarker, replacement, 1)
} else {
// Otherwise, insert status as the first field and keep the comma to separate from existing fields.
replacement := fmt.Sprintf("\"output\":{\"status\":%s,", string(statusBytes))
c = strings.Replace(c, "\"output\":{", replacement, 1)
}
// Restore the status field to the current output in ctx.data
var status interface{}
if err := json.Unmarshal(statusBytes, &status); err == nil {
if currentOutput := ctx.GetData(OutputFieldName); currentOutput != nil {
if currentMap, ok := currentOutput.(map[string]interface{}); ok {
currentMap["status"] = status
ctx.PushData(OutputFieldName, currentMap)
}
}
}
}
return c
}
// GetCommonLabels will convert context based labels to OAM standard labels
func GetCommonLabels(contextLabels map[string]string) map[string]string {
var commonLabels = map[string]string{}
for k, v := range contextLabels {
switch k {
case velaprocess.ContextAppName:
commonLabels[oam.LabelAppName] = v
case velaprocess.ContextName:
commonLabels[oam.LabelAppComponent] = v
case velaprocess.ContextAppRevision:
commonLabels[oam.LabelAppRevision] = v
case velaprocess.ContextReplicaKey:
commonLabels[oam.LabelReplicaKey] = v
}
}
return commonLabels
}
// GetBaseContextLabels get base context labels
func GetBaseContextLabels(ctx process.Context) map[string]string {
baseLabels := ctx.BaseContextLabels()
baseLabels[velaprocess.ContextAppName] = ctx.GetData(velaprocess.ContextAppName).(string)
baseLabels[velaprocess.ContextAppRevision] = ctx.GetData(velaprocess.ContextAppRevision).(string)
return baseLabels
}
func initRoot(contextLabels map[string]string) map[string]interface{} {
var root = map[string]interface{}{}
for k, v := range contextLabels {
root[k] = v
}
return root
}
func renderTemplate(templ string) string {
return templ + `
context: _
parameter: _
`
}
func (td *traitDef) getTemplateContext(ctx process.Context, cli client.Reader, accessor util.NamespaceAccessor) (map[string]interface{}, error) {
baseLabels := GetBaseContextLabels(ctx)
var root = initRoot(baseLabels)
var commonLabels = GetCommonLabels(baseLabels)
_, assists := ctx.Output()
outputs := make(map[string]interface{})
for _, assist := range assists {
if assist.Type != td.name {
continue
}
traitRef, err := assist.Ins.Unstructured()
if err != nil {
return nil, err
}
_ctx := withCluster(ctx.GetCtx(), traitRef)
object, err := getResourceFromObj(_ctx, ctx, traitRef, cli, accessor.For(traitRef), util.MergeMapOverrideWithDst(map[string]string{
oam.TraitTypeLabel: assist.Type,
}, commonLabels), assist.Name)
if err != nil {
return nil, err
}
outputs[assist.Name] = object
}
if len(outputs) > 0 {
root[OutputsFieldName] = outputs
}
return root, nil
}
// Status get trait status by customStatusTemplate
func (td *traitDef) Status(templateContext map[string]interface{}, request *health.StatusRequest) (*health.StatusResult, error) {
return health.GetStatus(templateContext, request)
}
func (td *traitDef) GetTemplateContext(ctx process.Context, cli client.Client, accessor util.NamespaceAccessor) (map[string]interface{}, error) {
return td.getTemplateContext(ctx, cli, accessor)
}
func getResourceFromObj(ctx context.Context, pctx process.Context, obj *unstructured.Unstructured, client client.Reader, namespace string, labels map[string]string, outputsResource string) (map[string]interface{}, error) {
if outputsResource != "" {
labels[oam.TraitResource] = outputsResource
}
if obj.GetName() != "" {
u, err := util.GetObjectGivenGVKAndName(ctx, client, obj.GroupVersionKind(), namespace, obj.GetName())
if err != nil {
return nil, err
}
return u.Object, nil
}
if ctxName := pctx.GetData(model.ContextName).(string); ctxName != "" {
u, err := util.GetObjectGivenGVKAndName(ctx, client, obj.GroupVersionKind(), namespace, ctxName)
if err == nil {
return u.Object, nil
}
}
list, err := util.GetObjectsGivenGVKAndLabels(ctx, client, obj.GroupVersionKind(), namespace, labels)
if err != nil {
return nil, err
}
if len(list.Items) == 1 {
return list.Items[0].Object, nil
}
for _, v := range list.Items {
if v.GetLabels()[oam.TraitResource] == outputsResource {
return v.Object, nil
}
}
return nil, errors.Errorf("no resources found gvk(%v) labels(%v)", obj.GroupVersionKind(), labels)
}
// FormatCUEError formats CUE errors in a user-friendly grouped format
func FormatCUEError(err error, messagePrefix string, entityType, entityName string, val ...*cue.Value) error {
var allParamErrors = make(map[string]bool)
var allTemplateErrors = make(map[string]bool)
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
}
}
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(allParamErrors) == 0 && len(allTemplateErrors) == 0 {
return nil
}
var result strings.Builder
result.WriteString(fmt.Sprintf("%s %s %s:", messagePrefix, entityType, entityName))
if len(allParamErrors) > 0 {
result.WriteString("\n\nParameter errors:\n")
// Sort errors for deterministic output
paramErrs := make([]string, 0, len(allParamErrors))
for errMsg := range allParamErrors {
paramErrs = append(paramErrs, errMsg)
}
sort.Strings(paramErrs)
for _, errMsg := range paramErrs {
result.WriteString(" " + errMsg + "\n")
}
}
if len(allTemplateErrors) > 0 {
result.WriteString("\n\nTemplate errors:\n")
templateErrs := make([]string, 0, len(allTemplateErrors))
for errMsg := range allTemplateErrors {
templateErrs = append(templateErrs, errMsg)
}
sort.Strings(templateErrs)
for _, errMsg := range templateErrs {
result.WriteString(" " + errMsg + "\n")
}
}
return fmt.Errorf("%s", strings.TrimRight(result.String(), "\n"))
}