mirror of
https://github.com/kubevela/kubevela.git
synced 2026-08-18 03:56:36 +00:00
* 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>
654 lines
21 KiB
Go
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"))
|
|
}
|