Files
kubevela/pkg/definition/defkit/trait.go
Asish Kumar 54197b4721 Fix: rename statusDetails CUE field to details to match Status struct (#7088)
The StatusDetails() function in defkit was rendering the CUE field as
"statusDetails:" instead of "details:", which is the actual JSON tag
defined on the Details field in the common.Status struct. This mismatch
caused CUE rendering failures at runtime because the controller expects
the field to be named "details" (matching the json:"details,omitempty"
tag in apis/core.oam.dev/common/types.go).

Fixed in all four definition types (component, trait, policy,
workflow step) and updated corresponding test assertions to verify
the correct field name.

Signed-off-by: Asish Kumar <officialasishkumar@gmail.com>
2026-04-06 07:24:15 -07:00

1662 lines
58 KiB
Go

/*
Copyright 2025 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 defkit
import (
"fmt"
"regexp"
"sort"
"strings"
"cuelang.org/go/cue/format"
"sigs.k8s.io/yaml"
"github.com/oam-dev/kubevela/pkg/definition/defkit/placement"
)
// topLevelTemplateRegex matches a top-level "template:" block in CUE.
// It looks for "template:" at the start of a line (with optional leading whitespace)
// that is NOT preceded by a colon (which would indicate it's part of a path like "spec: template:").
var topLevelTemplateRegex = regexp.MustCompile(`(?m)^[^\S\n]*template:`)
// TraitDefinition represents a KubeVela TraitDefinition.
// Traits modify or enhance components by either patching the workload resource
// or creating additional auxiliary resources.
//
// TraitDefinition embeds baseDefinition for common fields and methods shared
// with ComponentDefinition and other definition types.
type TraitDefinition struct {
baseDefinition // embedded common fields (name, description, params, template, etc.)
appliesToWorkloads []string // e.g., ["deployments.apps", "statefulsets.apps"]
conflictsWith []string // traits that conflict with this one
conflictsWithSet bool // true when ConflictsWith() was explicitly called
podDisruptive bool // whether applying this trait causes pod restart
stage string // "PreDispatch" or "PostDispatch" (default: "")
templateBlock string // raw CUE for template: block only (uses fluent API for header)
labels map[string]string // metadata labels for the trait definition
workloadRefPath *string // workloadRefPath attribute (nil = omit, pointer to distinguish from empty)
manageWorkload bool
controlPlaneOnly bool
revisionEnabled bool
}
// NewTrait creates a new TraitDefinition builder.
func NewTrait(name string) *TraitDefinition {
return &TraitDefinition{
baseDefinition: baseDefinition{
name: name,
params: make([]Param, 0),
},
appliesToWorkloads: make([]string, 0),
conflictsWith: make([]string, 0),
}
}
// Description sets the trait description.
func (t *TraitDefinition) Description(desc string) *TraitDefinition {
t.setDescription(desc)
return t
}
// AppliesTo specifies which workload types this trait can be applied to.
// Common values: "deployments.apps", "statefulsets.apps", "daemonsets.apps"
func (t *TraitDefinition) AppliesTo(workloads ...string) *TraitDefinition {
t.appliesToWorkloads = append(t.appliesToWorkloads, workloads...)
return t
}
// ConflictsWith specifies traits that cannot be used together with this trait.
func (t *TraitDefinition) ConflictsWith(traits ...string) *TraitDefinition {
t.conflictsWithSet = true
t.conflictsWith = append(t.conflictsWith, traits...)
return t
}
// PodDisruptive marks whether applying this trait causes pod restarts.
func (t *TraitDefinition) PodDisruptive(disruptive bool) *TraitDefinition {
t.podDisruptive = disruptive
return t
}
// WorkloadRefPath sets the workloadRefPath attribute for the trait.
func (t *TraitDefinition) WorkloadRefPath(path string) *TraitDefinition {
t.workloadRefPath = &path
return t
}
// ManageWorkload marks this trait as managing the workload.
func (t *TraitDefinition) ManageWorkload() *TraitDefinition {
t.manageWorkload = true
return t
}
// ControlPlaneOnly marks this trait as running on the control plane only.
func (t *TraitDefinition) ControlPlaneOnly() *TraitDefinition {
t.controlPlaneOnly = true
return t
}
// RevisionEnabled marks this trait as revision-enabled.
func (t *TraitDefinition) RevisionEnabled() *TraitDefinition {
t.revisionEnabled = true
return t
}
// Stage sets the trait application stage.
// Use "PreDispatch" for traits that must run before dispatch,
// "PostDispatch" for traits that run after (e.g., creating Services).
func (t *TraitDefinition) Stage(stage string) *TraitDefinition {
t.stage = stage
return t
}
// Params adds parameter definitions to the trait.
func (t *TraitDefinition) Params(params ...Param) *TraitDefinition {
t.addParams(params...)
return t
}
// Param adds a single parameter definition to the trait.
func (t *TraitDefinition) Param(param Param) *TraitDefinition {
t.addParams(param)
return t
}
// Template sets the unified template function for the trait.
// This is the preferred way to define trait behavior.
// Use tpl.Patch() for modifying the workload, and tpl.Outputs() for creating auxiliary resources.
//
// Example (patch-only):
//
// defkit.NewTrait("scaler").
// Params(defkit.Int("replicas").Default(1)).
// Template(func(tpl *defkit.Template) {
// replicas := defkit.Int("replicas")
// tpl.PatchStrategy("retainKeys").
// Patch().Set("spec.replicas", replicas)
// })
//
// Example (outputs-only):
//
// defkit.NewTrait("expose").
// Params(defkit.Array("ports")...).
// Template(func(tpl *defkit.Template) {
// service := defkit.NewResource("v1", "Service").
// Set("spec.type", "ClusterIP")
// tpl.Outputs("service", service)
// })
//
// Example (patch and outputs):
//
// defkit.NewTrait("ingress").
// Template(func(tpl *defkit.Template) {
// // Patch the workload
// tpl.Patch().Set("metadata.annotations[ingress]", "enabled")
// // Create auxiliary resource
// ingress := defkit.NewResource("networking.k8s.io/v1", "Ingress")
// tpl.Outputs("ingress", ingress)
// })
func (t *TraitDefinition) Template(fn func(tpl *Template)) *TraitDefinition {
t.setTemplate(fn)
return t
}
// CustomStatus sets the custom status CUE expression for the trait.
func (t *TraitDefinition) CustomStatus(expr string) *TraitDefinition {
t.setCustomStatus(expr)
return t
}
// HealthPolicy sets the health policy CUE expression for the trait.
func (t *TraitDefinition) HealthPolicy(expr string) *TraitDefinition {
t.setHealthPolicy(expr)
return t
}
// HealthPolicyExpr sets the health policy using a composable HealthExpression.
func (t *TraitDefinition) HealthPolicyExpr(expr HealthExpression) *TraitDefinition {
t.setHealthPolicyExpr(expr)
return t
}
// StatusDetails sets the status details CUE expression for the trait.
func (t *TraitDefinition) StatusDetails(details string) *TraitDefinition {
t.setStatusDetails(details)
return t
}
// Annotations sets metadata annotations on the trait definition.
func (t *TraitDefinition) Annotations(annotations map[string]string) *TraitDefinition {
t.setAnnotations(annotations)
return t
}
// Version sets the version string for the trait definition.
func (t *TraitDefinition) Version(v string) *TraitDefinition {
t.setVersion(v)
return t
}
// RawCUE sets raw CUE for complex trait definitions that don't fit the builder pattern.
// When set, this bypasses all other template settings and outputs the raw CUE directly.
func (t *TraitDefinition) RawCUE(cue string) *TraitDefinition {
t.setRawCUE(cue)
return t
}
// TemplateBlock sets raw CUE for the template: block only.
// Unlike RawCUE which bypasses everything, TemplateBlock uses the fluent API for:
// - Trait header (name, type, description, annotations, labels)
// - Attributes (podDisruptive, appliesToWorkloads, conflictsWith, stage, status)
//
// But uses raw CUE for the template: block content (patch, outputs, parameter, helpers).
// This is useful when the template logic is too complex for the fluent API but you
// still want type-safe metadata and attributes.
//
// Example:
//
// defkit.NewTrait("command").
// Description("Add command on K8s pod for your workload").
// AppliesTo("deployments.apps", "statefulsets.apps").
// TemplateBlock(`
// #PatchParams: {
// containerName: *"" | string
// command: *null | [...string]
// }
// PatchContainer: { ... }
// patch: spec: template: spec: { ... }
// parameter: #PatchParams
// `)
func (t *TraitDefinition) TemplateBlock(cue string) *TraitDefinition {
t.templateBlock = cue
return t
}
// GetTemplateBlock returns the raw CUE template block if set.
func (t *TraitDefinition) GetTemplateBlock() string { return t.templateBlock }
// HasTemplateBlock returns true if the trait has a raw CUE template block.
func (t *TraitDefinition) HasTemplateBlock() bool { return t.templateBlock != "" }
// WithImports adds CUE imports to the trait definition.
// Usage: trait.WithImports("strconv", "strings")
func (t *TraitDefinition) WithImports(imports ...string) *TraitDefinition {
t.addImports(imports...)
return t
}
// Helper adds a helper type definition like #HealthProbe or #labelSelector.
// The param can be a StructParam, MapParam, or ArrayParam that defines the schema.
// Usage: trait.Helper("HealthProbe", defkit.Struct("probe").Fields(...))
func (t *TraitDefinition) Helper(name string, param Param) *TraitDefinition {
t.addHelper(name, param)
return t
}
// Labels sets metadata labels for the trait definition.
// These labels appear in the definition's labels block.
// Usage: trait.Labels(map[string]string{"ui-hidden": "true"})
func (t *TraitDefinition) Labels(labels map[string]string) *TraitDefinition {
t.labels = labels
return t
}
// GetLabels returns the trait's metadata labels.
func (t *TraitDefinition) GetLabels() map[string]string { return t.labels }
// RunOn adds placement conditions specifying which clusters this trait should run on.
// Use the placement package's fluent API to build conditions.
//
// Example:
//
// defkit.NewTrait("eks-scaler").
// RunOn(placement.Label("provider").Eq("aws"))
//
// Multiple RunOn calls are combined with AND semantics (all conditions must match).
func (t *TraitDefinition) RunOn(conditions ...placement.Condition) *TraitDefinition {
t.addRunOn(conditions...)
return t
}
// NotRunOn adds placement conditions specifying which clusters this trait should NOT run on.
// Use the placement package's fluent API to build conditions.
//
// Example:
//
// defkit.NewTrait("no-vclusters").
// NotRunOn(placement.Label("cluster-type").Eq("vcluster"))
//
// If any NotRunOn condition matches, the trait is ineligible for that cluster.
func (t *TraitDefinition) NotRunOn(conditions ...placement.Condition) *TraitDefinition {
t.addNotRunOn(conditions...)
return t
}
// DefName implements Definition.DefName.
func (t *TraitDefinition) DefName() string { return t.name }
// DefType implements Definition.DefType.
func (t *TraitDefinition) DefType() DefinitionType { return DefinitionTypeTrait }
// GetAppliesToWorkloads returns the workload types this trait applies to.
func (t *TraitDefinition) GetAppliesToWorkloads() []string { return t.appliesToWorkloads }
// GetConflictsWith returns traits that conflict with this one.
func (t *TraitDefinition) GetConflictsWith() []string { return t.conflictsWith }
// IsPodDisruptive returns whether this trait is pod-disruptive.
func (t *TraitDefinition) IsPodDisruptive() bool { return t.podDisruptive }
// IsManageWorkload returns whether this trait manages the workload.
func (t *TraitDefinition) IsManageWorkload() bool { return t.manageWorkload }
// IsControlPlaneOnly returns whether this trait runs on the control plane only.
func (t *TraitDefinition) IsControlPlaneOnly() bool { return t.controlPlaneOnly }
// IsRevisionEnabled returns whether this trait has revision enabled.
func (t *TraitDefinition) IsRevisionEnabled() bool { return t.revisionEnabled }
// GetStage returns the trait stage.
func (t *TraitDefinition) GetStage() string { return t.stage }
// Note: The following methods are inherited from baseDefinition:
// - GetDescription() string
// - GetParams() []Param
// - GetCustomStatus() string
// - GetHealthPolicy() string
// - GetHelperDefinitions() []HelperDefinition
// - GetTemplate() func(tpl *Template)
// - HasTemplate() bool
// ToCue generates the complete CUE definition string for this trait.
func (t *TraitDefinition) ToCue() string {
gen := NewTraitCUEGenerator()
if len(t.imports) > 0 {
gen.WithImports(t.imports...)
}
var result string
// If raw CUE is set, determine how to handle it
if t.rawCUE != "" {
// If raw CUE contains a complete definition (has a top-level "template:" block),
// return it with the name rewritten to match the name set via NewTrait().
// This ensures the fluent API name takes precedence over any name in the raw CUE.
// We use regex to check for "template:" at the start of a line to avoid false
// positives from paths like "patch: spec: template: spec:" which contain "template:"
// but not as a top-level block.
if hasTopLevelTemplateBlock(t.rawCUE) {
result = t.GetRawCUEWithName()
} else {
// Otherwise, it's template content only - generate header with fluent API metadata
result = gen.GenerateDefinitionWithRawTemplate(t, t.rawCUE)
}
} else {
result = gen.GenerateFullDefinition(t)
}
// Format the CUE output for consistency
return formatCUE(result)
}
// formatCUE formats a CUE string using the standard CUE formatter.
// If formatting fails, it returns the original string unchanged.
func formatCUE(cue string) string {
formatted, err := format.Source([]byte(cue), format.Simplify())
if err != nil {
// If formatting fails, return the original string
return cue
}
return string(formatted)
}
// hasTopLevelTemplateBlock checks if the CUE content has a top-level "template:" block.
// It returns true if "template:" appears at the start of a line (with optional leading whitespace),
// which indicates a complete definition. This distinguishes from cases like "patch: spec: template: spec:"
// where "template:" is part of a path, not a top-level block.
func hasTopLevelTemplateBlock(cue string) bool {
return topLevelTemplateRegex.MatchString(cue)
}
// ToYAML generates the Kubernetes YAML representation of the TraitDefinition.
func (t *TraitDefinition) ToYAML() ([]byte, error) {
cueStr := t.ToCue()
// Build the TraitDefinition CR structure
cr := map[string]any{
"apiVersion": "core.oam.dev/v1beta1",
"kind": "TraitDefinition",
"metadata": map[string]any{
"name": t.name,
"annotations": func() map[string]any {
a := map[string]any{}
for k, v := range t.GetAnnotations() {
a[k] = v
}
a["definition.oam.dev/description"] = t.description
return a
}(),
},
"spec": map[string]any{
"appliesToWorkloads": t.appliesToWorkloads,
"podDisruptive": t.podDisruptive,
"schematic": map[string]any{
"cue": map[string]any{
"template": cueStr,
},
},
},
}
// Add conflictsWith if present
if len(t.conflictsWith) > 0 {
cr["spec"].(map[string]any)["conflictsWith"] = t.conflictsWith
}
// Add stage if present
if t.stage != "" {
cr["spec"].(map[string]any)["stage"] = t.stage
}
if t.GetVersion() != "" {
cr["spec"].(map[string]any)["version"] = t.GetVersion()
}
if t.manageWorkload {
cr["spec"].(map[string]any)["manageWorkload"] = true
}
if t.controlPlaneOnly {
cr["spec"].(map[string]any)["controlPlaneOnly"] = true
}
if t.revisionEnabled {
cr["spec"].(map[string]any)["revisionEnabled"] = true
}
return yaml.Marshal(cr)
}
// --- TraitCUEGenerator ---
// TraitCUEGenerator generates CUE definitions for traits.
type TraitCUEGenerator struct {
indent string
imports []string
}
// NewTraitCUEGenerator creates a new trait CUE generator.
func NewTraitCUEGenerator() *TraitCUEGenerator {
return &TraitCUEGenerator{
indent: "\t",
imports: []string{},
}
}
// WithImports adds CUE imports.
func (g *TraitCUEGenerator) WithImports(imports ...string) *TraitCUEGenerator {
g.imports = append(g.imports, imports...)
return g
}
// GenerateFullDefinition generates the complete CUE definition for a trait.
func (g *TraitCUEGenerator) GenerateFullDefinition(t *TraitDefinition) string {
var sb strings.Builder
// Write imports if any
if len(g.imports) > 0 {
sb.WriteString("import (\n")
for _, imp := range g.imports {
sb.WriteString(fmt.Sprintf("\t%q\n", imp))
}
sb.WriteString(")\n\n")
}
// Write trait header
sb.WriteString(fmt.Sprintf("%s: {\n", cueLabel(t.GetName())))
sb.WriteString(fmt.Sprintf("%stype: \"trait\"\n", g.indent))
if t.GetAnnotations() != nil && len(t.GetAnnotations()) > 0 {
annKeys := make([]string, 0, len(t.GetAnnotations()))
for k := range t.GetAnnotations() {
annKeys = append(annKeys, k)
}
sort.Strings(annKeys)
sb.WriteString(fmt.Sprintf("%sannotations: {\n", g.indent))
for _, k := range annKeys {
sb.WriteString(fmt.Sprintf("%s\t%q: %q\n", g.indent, k, t.GetAnnotations()[k]))
}
sb.WriteString(fmt.Sprintf("%s}\n", g.indent))
} else {
sb.WriteString(fmt.Sprintf("%sannotations: {}\n", g.indent))
}
if len(t.labels) > 0 {
labelKeys := sortedKeys(t.labels)
sb.WriteString(fmt.Sprintf("%slabels: {\n", g.indent))
for _, k := range labelKeys {
sb.WriteString(fmt.Sprintf("%s\t%q: %q\n", g.indent, k, t.labels[k]))
}
sb.WriteString(fmt.Sprintf("%s}\n", g.indent))
} else {
sb.WriteString(fmt.Sprintf("%slabels: {}\n", g.indent))
}
sb.WriteString(fmt.Sprintf("%sdescription: %q\n", g.indent, t.GetDescription()))
if t.GetVersion() != "" {
sb.WriteString(fmt.Sprintf("%sversion: %q\n", g.indent, t.GetVersion()))
}
// Write attributes
sb.WriteString(fmt.Sprintf("%sattributes: {\n", g.indent))
g.writeAttributes(&sb, t, 2)
sb.WriteString(fmt.Sprintf("%s}\n", g.indent))
sb.WriteString("}\n")
// Write template section
sb.WriteString(g.GenerateTemplate(t))
return sb.String()
}
// writeAttributes writes the trait attributes block.
func (g *TraitCUEGenerator) writeAttributes(sb *strings.Builder, t *TraitDefinition, depth int) {
indent := strings.Repeat(g.indent, depth)
// podDisruptive (always emit — vela CUE always includes this attribute)
sb.WriteString(fmt.Sprintf("%spodDisruptive: %v\n", indent, t.IsPodDisruptive()))
if t.IsManageWorkload() {
sb.WriteString(fmt.Sprintf("%smanageWorkload: true\n", indent))
}
if t.IsControlPlaneOnly() {
sb.WriteString(fmt.Sprintf("%scontrolPlaneOnly: true\n", indent))
}
if t.IsRevisionEnabled() {
sb.WriteString(fmt.Sprintf("%srevisionEnabled: true\n", indent))
}
// stage (if set)
if t.GetStage() != "" {
sb.WriteString(fmt.Sprintf("%sstage: %q\n", indent, t.GetStage()))
}
// appliesToWorkloads
if len(t.GetAppliesToWorkloads()) > 0 {
workloads := make([]string, len(t.GetAppliesToWorkloads()))
for i, w := range t.GetAppliesToWorkloads() {
workloads[i] = fmt.Sprintf("%q", w)
}
sb.WriteString(fmt.Sprintf("%sappliesToWorkloads: [%s]\n", indent, strings.Join(workloads, ", ")))
}
// conflictsWith (emit when explicitly set, even if empty)
if t.conflictsWithSet {
if len(t.GetConflictsWith()) > 0 {
conflicts := make([]string, len(t.GetConflictsWith()))
for i, c := range t.GetConflictsWith() {
conflicts[i] = fmt.Sprintf("%q", c)
}
sb.WriteString(fmt.Sprintf("%sconflictsWith: [%s]\n", indent, strings.Join(conflicts, ", ")))
} else {
sb.WriteString(fmt.Sprintf("%sconflictsWith: []\n", indent))
}
}
// status (customStatus, healthPolicy, and statusDetails)
if t.GetCustomStatus() != "" || t.GetHealthPolicy() != "" || t.GetStatusDetails() != "" {
sb.WriteString(fmt.Sprintf("%sstatus: {\n", indent))
innerIndent := strings.Repeat(g.indent, depth+1)
if t.GetCustomStatus() != "" {
sb.WriteString(fmt.Sprintf("%scustomStatus: #\"\"\"\n", innerIndent))
for _, line := range strings.Split(t.GetCustomStatus(), "\n") {
sb.WriteString(fmt.Sprintf("%s\t%s\n", innerIndent, line))
}
sb.WriteString(fmt.Sprintf("%s\t\"\"\"#\n", innerIndent))
}
if t.GetHealthPolicy() != "" {
sb.WriteString(fmt.Sprintf("%shealthPolicy: #\"\"\"\n", innerIndent))
for _, line := range strings.Split(t.GetHealthPolicy(), "\n") {
sb.WriteString(fmt.Sprintf("%s\t%s\n", innerIndent, line))
}
sb.WriteString(fmt.Sprintf("%s\t\"\"\"#\n", innerIndent))
}
if t.GetStatusDetails() != "" {
sb.WriteString(fmt.Sprintf("%sdetails: #\"\"\"\n", innerIndent))
for _, line := range strings.Split(t.GetStatusDetails(), "\n") {
sb.WriteString(fmt.Sprintf("%s\t%s\n", innerIndent, line))
}
sb.WriteString(fmt.Sprintf("%s\t\"\"\"#\n", innerIndent))
}
sb.WriteString(fmt.Sprintf("%s}\n", indent))
}
// workloadRefPath (if explicitly set)
if t.workloadRefPath != nil {
sb.WriteString(fmt.Sprintf("%sworkloadRefPath: %q\n", indent, *t.workloadRefPath))
}
}
// GenerateTemplate generates the template block for a trait.
func (g *TraitCUEGenerator) GenerateTemplate(t *TraitDefinition) string {
var sb strings.Builder
// If TemplateBlock is set, use it directly (raw CUE for template section)
if t.HasTemplateBlock() {
sb.WriteString("template: {\n")
// Indent each line of the template block
for _, line := range strings.Split(strings.TrimSpace(t.GetTemplateBlock()), "\n") {
sb.WriteString(fmt.Sprintf("%s%s\n", g.indent, line))
}
sb.WriteString("}\n")
return sb.String()
}
sb.WriteString("template: {\n")
// Check if using Template API
usedPatchContainer := false
if t.HasTemplate() {
// Check if PatchContainer is configured (it generates its own parameter block)
tpl := NewTemplate()
t.GetTemplate()(tpl)
usedPatchContainer = tpl.GetPatchContainerConfig() != nil
g.writeUnifiedTemplate(&sb, t, 1)
}
// Generate parameter section (skip if PatchContainer already generated it)
if !usedPatchContainer {
sb.WriteString(g.generateParameterBlock(t, 1))
}
// Generate helper type definitions (like #HealthProbe, #labelSelector)
gen := NewCUEGenerator()
for _, helperDef := range t.GetHelperDefinitions() {
gen.WriteHelperDefinition(&sb, helperDef, 1)
}
sb.WriteString("}\n")
return sb.String()
}
// writeUnifiedTemplate writes the template block using the new unified Template API.
// This handles both patch and outputs in a single template function.
func (g *TraitCUEGenerator) writeUnifiedTemplate(sb *strings.Builder, t *TraitDefinition, depth int) {
// Execute the unified template function to capture all operations
tpl := NewTemplate()
if t.template != nil {
t.template(tpl)
}
indent := strings.Repeat(g.indent, depth)
gen := NewCUEGenerator()
// Generate PatchContainer pattern if configured
if config := tpl.GetPatchContainerConfig(); config != nil {
g.writePatchContainerPattern(sb, config, depth)
return
}
// Handle raw blocks (for init-container, expose, hpa and similar traits)
// Check if any raw block is set
hasRawBlocks := tpl.GetRawPatchBlock() != "" || tpl.GetRawOutputsBlock() != "" ||
tpl.GetRawParameterBlock() != "" || tpl.GetRawHeaderBlock() != ""
if hasRawBlocks {
g.writeRawBlocks(sb, tpl, depth)
return
}
// Render let bindings before patch/outputs
for _, lb := range tpl.GetLetBindings() {
exprStr := gen.valueToCUE(lb.Expr())
sb.WriteString(fmt.Sprintf("%slet %s = %s\n", indent, lb.Name(), exprStr))
}
// Generate patch block if present
if tpl.HasPatch() {
// Write patch strategy comment if set
if tpl.GetPatchStrategy() != "" {
sb.WriteString(fmt.Sprintf("%s// +patchStrategy=%s\n", indent, tpl.GetPatchStrategy()))
}
sb.WriteString(fmt.Sprintf("%spatch: ", indent))
g.writePatchResourceOps(sb, gen, tpl.GetPatch().Ops(), depth)
sb.WriteString("\n")
}
// Generate outputs block if present
if outputs := tpl.GetOutputs(); len(outputs) > 0 {
outputNames := sortedKeys(outputs)
sb.WriteString(fmt.Sprintf("%soutputs: {\n", indent))
for _, name := range outputNames {
res := outputs[name]
g.writeTraitResourceOutput(sb, gen, name, res, depth+1)
}
// Render output groups (multiple outputs under one condition)
for _, group := range tpl.GetOutputGroups() {
condStr := gen.conditionToCUE(group.cond)
sb.WriteString(fmt.Sprintf("%s\tif %s {\n", indent, condStr))
for _, gName := range sortedKeys(group.outputs) {
gRes := group.outputs[gName]
g.writeTraitResourceOutput(sb, gen, gName, gRes, depth+2)
}
sb.WriteString(fmt.Sprintf("%s\t}\n", indent))
}
sb.WriteString(fmt.Sprintf("%s}\n", indent))
}
}
// writeRawBlocks writes raw CUE blocks for header, patch, outputs, and parameter.
// This is used for traits with complex structures that cannot be
// expressed using the fluent API (like init-container, expose, hpa, etc.).
// Order: header (let bindings) -> patch -> outputs -> parameter
func (g *TraitCUEGenerator) writeRawBlocks(sb *strings.Builder, tpl *Template, depth int) {
indent := strings.Repeat(g.indent, depth)
// Write raw header block (let bindings and pre-output declarations)
if rawHeader := tpl.GetRawHeaderBlock(); rawHeader != "" {
for _, line := range strings.Split(strings.TrimSpace(rawHeader), "\n") {
sb.WriteString(fmt.Sprintf("%s%s\n", indent, line))
}
}
// Write raw patch block
if rawPatch := tpl.GetRawPatchBlock(); rawPatch != "" {
for _, line := range strings.Split(strings.TrimSpace(rawPatch), "\n") {
sb.WriteString(fmt.Sprintf("%s%s\n", indent, line))
}
}
// Write raw outputs block
if rawOutputs := tpl.GetRawOutputsBlock(); rawOutputs != "" {
for _, line := range strings.Split(strings.TrimSpace(rawOutputs), "\n") {
sb.WriteString(fmt.Sprintf("%s%s\n", indent, line))
}
}
// Write raw parameter block
if rawParam := tpl.GetRawParameterBlock(); rawParam != "" {
for _, line := range strings.Split(strings.TrimSpace(rawParam), "\n") {
sb.WriteString(fmt.Sprintf("%s%s\n", indent, line))
}
}
}
// writePatchResourceOps writes PatchResource operations as CUE.
func (g *TraitCUEGenerator) writePatchResourceOps(sb *strings.Builder, gen *CUEGenerator, ops []ResourceOp, depth int) {
if len(ops) == 0 {
sb.WriteString("{}")
return
}
// Check for passthrough operation (patch: parameter)
for _, op := range ops {
if _, isPassthrough := op.(*PassthroughOp); isPassthrough {
sb.WriteString("parameter")
return
}
}
// Separate IfBlocks from other ops
var bareOps []ResourceOp
var ifBlocks []*IfBlock
for _, op := range ops {
if ib, ok := op.(*IfBlock); ok {
ifBlocks = append(ifBlocks, ib)
} else {
bareOps = append(bareOps, op)
}
}
// If 0 or 1 IfBlock, use existing merged approach (no path conflicts possible)
if len(ifBlocks) <= 1 {
tree := gen.buildFieldTree(ops)
gen.liftChildConditions(tree)
g.writePatchFieldTree(sb, gen, tree, depth)
return
}
// Multiple IfBlocks: process each separately to avoid path conflicts
// when different IfBlocks target the same field paths with different conditions.
prefix := g.findIfBlockCommonPrefix(ifBlocks, bareOps)
var prefixParts []string
if prefix != "" {
prefixParts = strings.Split(prefix, ".")
}
// Write the common prefix inline (e.g., "spec: ")
for _, p := range prefixParts {
sb.WriteString(fmt.Sprintf("%s: ", p))
}
innerDepth := depth + len(prefixParts)
// Open block for the children
sb.WriteString("{\n")
indent := strings.Repeat(g.indent, innerDepth+1)
first := true
// Render bare ops first (if any)
if len(bareOps) > 0 {
bareTree := gen.buildFieldTree(bareOps)
// Navigate to subtree below common prefix
subtree := bareTree
for _, p := range prefixParts {
if child, ok := subtree.children[p]; ok {
subtree = child
}
}
gen.liftChildConditions(subtree)
for _, key := range subtree.childOrder {
node := subtree.children[key]
sb.WriteString(indent)
g.writePatchFieldNode(sb, gen, key, node, innerDepth+1)
sb.WriteString("\n")
}
first = false
}
// Render each IfBlock as a separate subtree
for _, ib := range ifBlocks {
if !first {
sb.WriteString("\n")
}
// Build tree from inner ops only (without the outer If condition)
innerTree := gen.buildFieldTree(ib.Ops())
// Navigate to subtree below common prefix
subtree := innerTree
for _, p := range prefixParts {
if child, ok := subtree.children[p]; ok {
subtree = child
}
}
// Normalize conditional nodes in the subtree
gen.liftChildConditions(subtree)
// Emit: if condition { ... }
condStr := gen.conditionToCUE(ib.Cond())
sb.WriteString(fmt.Sprintf("%sif %s {\n", indent, condStr))
// Render subtree children inside the if block
innerBlockIndent := strings.Repeat(g.indent, innerDepth+2)
for _, key := range subtree.childOrder {
node := subtree.children[key]
sb.WriteString(innerBlockIndent)
g.writePatchFieldNode(sb, gen, key, node, innerDepth+2)
sb.WriteString("\n")
}
sb.WriteString(fmt.Sprintf("%s}", indent))
first = false
}
// Close outer block
sb.WriteString(fmt.Sprintf("\n%s}", strings.Repeat(g.indent, innerDepth)))
}
// findIfBlockCommonPrefix finds the longest common path prefix across all IfBlocks and bare ops.
// For example, if all paths start with "spec.", the prefix is "spec".
func (g *TraitCUEGenerator) findIfBlockCommonPrefix(ifBlocks []*IfBlock, bareOps []ResourceOp) string {
var allPaths []string
for _, ib := range ifBlocks {
for _, op := range ib.Ops() {
switch o := op.(type) {
case *SetOp:
allPaths = append(allPaths, o.Path())
case *SetIfOp:
allPaths = append(allPaths, o.Path())
case *PatchStrategyAnnotationOp:
allPaths = append(allPaths, o.Path())
}
}
}
for _, op := range bareOps {
switch o := op.(type) {
case *SetOp:
allPaths = append(allPaths, o.Path())
case *SetIfOp:
allPaths = append(allPaths, o.Path())
}
}
if len(allPaths) == 0 {
return ""
}
firstParts := strings.Split(allPaths[0], ".")
commonLen := len(firstParts)
for _, p := range allPaths[1:] {
parts := strings.Split(p, ".")
minLen := commonLen
if len(parts) < minLen {
minLen = len(parts)
}
matched := 0
for i := 0; i < minLen; i++ {
if parts[i] != firstParts[i] {
break
}
matched++
}
commonLen = matched
}
if commonLen == 0 {
return ""
}
// Don't include leaf-level parts — only include the common ancestor path
// (at least one level must remain below the prefix for each IfBlock)
return strings.Join(firstParts[:commonLen], ".")
}
// writePatchFieldTree writes a field tree as CUE patch syntax.
// This method reuses the CUEGenerator's writeFieldTree with patch-specific handling.
func (g *TraitCUEGenerator) writePatchFieldTree(sb *strings.Builder, gen *CUEGenerator, tree *fieldNode, depth int) {
// For a single-path case with one child, generate inline nested syntax
if len(tree.childOrder) == 1 {
key := tree.childOrder[0]
node := tree.children[key]
g.writePatchFieldNode(sb, gen, key, node, depth)
return
}
// Multiple keys - write as block
sb.WriteString("{\n")
indent := strings.Repeat(g.indent, depth+1)
for _, key := range tree.childOrder {
node := tree.children[key]
sb.WriteString(indent)
g.writePatchFieldNode(sb, gen, key, node, depth+1)
sb.WriteString("\n")
}
sb.WriteString(fmt.Sprintf("%s}", strings.Repeat(g.indent, depth)))
}
// writePatchFieldNode writes a single field node for patches.
func (g *TraitCUEGenerator) writePatchFieldNode(sb *strings.Builder, gen *CUEGenerator, key string, node *fieldNode, depth int) {
// Handle ForEach operations specially - pass condition if present
if node.forEach != nil {
g.writeForEachOp(sb, gen, key, node.forEach, node.cond, depth)
return
}
// Handle PatchKey operations specially (array patches with merge key) - pass condition if present
if node.patchKey != nil {
g.writePatchKeyOp(sb, gen, key, node.patchKey, node.cond, depth)
return
}
// Handle SpreadAll operations (array constraint patches)
if node.spreadAll != nil {
g.writeSpreadAllOp(sb, gen, key, node.spreadAll, node.cond, depth)
return
}
// Handle conditional fields
if node.cond != nil {
condStr := gen.conditionToCUE(node.cond)
sb.WriteString(fmt.Sprintf("if %s {\n", condStr))
innerIndent := strings.Repeat(g.indent, depth+1)
// Emit patchStrategy annotation inside the if block
if node.patchStrategy != "" {
sb.WriteString(fmt.Sprintf("%s// +patchStrategy=%s\n", innerIndent, node.patchStrategy))
}
sb.WriteString(fmt.Sprintf("%s%s: ", innerIndent, key))
if len(node.children) > 0 {
g.writePatchFieldTreeFromChildren(sb, gen, node, depth+1)
} else if node.value != nil {
sb.WriteString(gen.valueToCUE(node.value))
}
sb.WriteString("\n")
sb.WriteString(fmt.Sprintf("%s}", strings.Repeat(g.indent, depth)))
return
}
// Regular field
if node.patchStrategy != "" {
sb.WriteString(fmt.Sprintf("// +patchStrategy=%s\n%s", node.patchStrategy, strings.Repeat(g.indent, depth)))
}
sb.WriteString(fmt.Sprintf("%s: ", key))
if len(node.children) > 0 {
g.writePatchFieldTreeFromChildren(sb, gen, node, depth)
} else if node.value != nil {
sb.WriteString(gen.valueToCUE(node.value))
}
}
// writePatchFieldTreeFromChildren writes children of a field node.
func (g *TraitCUEGenerator) writePatchFieldTreeFromChildren(sb *strings.Builder, gen *CUEGenerator, parent *fieldNode, depth int) {
// For a single child without condition, generate inline nested syntax
// If child has condition, we must use block format to allow proper if-wrapping
if len(parent.childOrder) == 1 {
key := parent.childOrder[0]
node := parent.children[key]
// Check if this node or any descendant has a condition - if so, use block format
if !g.hasConditionalDescendant(node) {
g.writePatchFieldNode(sb, gen, key, node, depth)
return
}
}
// Multiple children or conditional child - write as block
sb.WriteString("{\n")
indent := strings.Repeat(g.indent, depth+1)
for _, key := range parent.childOrder {
node := parent.children[key]
sb.WriteString(indent)
g.writePatchFieldNode(sb, gen, key, node, depth+1)
sb.WriteString("\n")
}
sb.WriteString(fmt.Sprintf("%s}", strings.Repeat(g.indent, depth)))
}
// hasConditionalDescendant checks if a node or any of its descendants has a condition,
// patchKey, or forEach operation that requires block format output.
func (g *TraitCUEGenerator) hasConditionalDescendant(node *fieldNode) bool {
if node.cond != nil {
return true
}
// PatchKey, ForEach, and patchStrategy annotations require block format
// because they need to write comments or special syntax that can't appear inline
if node.patchKey != nil || node.forEach != nil || node.patchStrategy != "" {
return true
}
for _, child := range node.children {
if g.hasConditionalDescendant(child) {
return true
}
}
return false
}
// writeForEachOp writes a ForEach operation as CUE.
// Generates: for k, v in source { (k): v }
// If cond is provided, wraps in: if cond { ... }
func (g *TraitCUEGenerator) writeForEachOp(sb *strings.Builder, gen *CUEGenerator, key string, op *ForEachOp, cond Condition, depth int) {
sourceStr := gen.valueToCUE(op.Source())
indent := strings.Repeat(g.indent, depth)
// Wrap in condition if present
if cond != nil {
condStr := gen.conditionToCUE(cond)
sb.WriteString(fmt.Sprintf("if %s {\n", condStr))
sb.WriteString(fmt.Sprintf("%s\t%s: {\n", indent, key))
sb.WriteString(fmt.Sprintf("%s\t\tfor k, v in %s {\n", indent, sourceStr))
sb.WriteString(fmt.Sprintf("%s\t\t\t(k): v\n", indent))
sb.WriteString(fmt.Sprintf("%s\t\t}\n", indent))
sb.WriteString(fmt.Sprintf("%s\t}\n", indent))
sb.WriteString(fmt.Sprintf("%s}", indent))
} else {
sb.WriteString(fmt.Sprintf("%s: {\n", key))
innerIndent := strings.Repeat(g.indent, depth+1)
sb.WriteString(fmt.Sprintf("%sfor k, v in %s {\n", innerIndent, sourceStr))
sb.WriteString(fmt.Sprintf("%s\t(k): v\n", innerIndent))
sb.WriteString(fmt.Sprintf("%s}\n", innerIndent))
sb.WriteString(fmt.Sprintf("%s}", indent))
}
}
// writePatchKeyOp writes a PatchKey operation as CUE.
// Generates: // +patchKey=key
//
// path: [...]
//
// If cond is provided, wraps in: if cond { ... }
func (g *TraitCUEGenerator) writePatchKeyOp(sb *strings.Builder, gen *CUEGenerator, key string, op *PatchKeyOp, cond Condition, depth int) {
indent := strings.Repeat(g.indent, depth)
elements := op.Elements()
useArrayValue := len(elements) == 1
if useArrayValue {
_, useArrayValue = elements[0].(*ArrayParam)
}
// Wrap in condition if present
if cond != nil {
condStr := gen.conditionToCUE(cond)
sb.WriteString(fmt.Sprintf("if %s {\n", condStr))
sb.WriteString(fmt.Sprintf("%s\t// +patchKey=%s\n", indent, op.Key()))
if useArrayValue {
valStr := gen.valueToCUEAtDepth(elements[0], depth+1)
sb.WriteString(fmt.Sprintf("%s\t%s: %s\n", indent, key, valStr))
} else {
sb.WriteString(fmt.Sprintf("%s\t%s: [", indent, key))
for i, elem := range elements {
if i > 0 {
sb.WriteString(", ")
}
// Use depth-aware formatting for ArrayElement
if arrElem, ok := elem.(*ArrayElement); ok {
sb.WriteString(gen.arrayElementToCUEWithDepth(arrElem, depth+1))
} else {
sb.WriteString(gen.valueToCUE(elem))
}
}
sb.WriteString("]\n")
}
sb.WriteString(fmt.Sprintf("%s}", indent))
} else {
// Write the patchKey annotation comment
sb.WriteString(fmt.Sprintf("// +patchKey=%s\n", op.Key()))
if useArrayValue {
valStr := gen.valueToCUEAtDepth(elements[0], depth)
sb.WriteString(fmt.Sprintf("%s%s: %s", indent, key, valStr))
} else {
sb.WriteString(fmt.Sprintf("%s%s: [", indent, key))
// Write elements
for i, elem := range elements {
if i > 0 {
sb.WriteString(", ")
}
// Use depth-aware formatting for ArrayElement
if arrElem, ok := elem.(*ArrayElement); ok {
sb.WriteString(gen.arrayElementToCUEWithDepth(arrElem, depth))
} else {
sb.WriteString(gen.valueToCUE(elem))
}
}
sb.WriteString("]")
}
}
}
// writeSpreadAllOp writes a SpreadAll operation as CUE.
// Generates: path: [...{element}]
// If cond is provided, wraps in: if cond { ... }
func (g *TraitCUEGenerator) writeSpreadAllOp(sb *strings.Builder, gen *CUEGenerator, key string, op *SpreadAllOp, cond Condition, depth int) {
indent := strings.Repeat(g.indent, depth)
writeElements := func(sb *strings.Builder, elemDepth int) {
for i, elem := range op.Elements() {
if i > 0 {
sb.WriteString(", ")
}
if arrElem, ok := elem.(*ArrayElement); ok {
// Build a field tree from the element's ops for proper nesting
tree := gen.buildFieldTree(arrElem.Ops())
gen.liftChildConditions(tree)
// Also add direct field assignments
for _, fk := range sortedKeys(arrElem.Fields()) {
fv := arrElem.Fields()[fk]
gen.insertIntoTree(tree, fk, fv, nil)
}
sb.WriteString("...{\n")
// Write tree children with explicit indentation
// (avoid writePatchFieldTree's single-child inline optimization)
innerIndent := strings.Repeat(g.indent, elemDepth+1)
for _, tk := range tree.childOrder {
tn := tree.children[tk]
sb.WriteString(innerIndent)
g.writePatchFieldNode(sb, gen, tk, tn, elemDepth+1)
sb.WriteString("\n")
}
sb.WriteString(fmt.Sprintf("%s}", strings.Repeat(g.indent, elemDepth)))
} else {
sb.WriteString("...")
sb.WriteString(gen.valueToCUE(elem))
}
}
}
if cond != nil {
condStr := gen.conditionToCUE(cond)
sb.WriteString(fmt.Sprintf("if %s {\n", condStr))
sb.WriteString(fmt.Sprintf("%s\t%s: [", indent, key))
writeElements(sb, depth+1)
sb.WriteString("]\n")
sb.WriteString(fmt.Sprintf("%s}", indent))
} else {
sb.WriteString(fmt.Sprintf("%s: [", key))
writeElements(sb, depth)
sb.WriteString("]")
}
}
// writeTraitResourceOutput writes a resource as CUE for trait outputs.
// This handles OutputsIf conditions and VersionConditionals, which the old
// writeResourceOutput method did not support.
func (g *TraitCUEGenerator) writeTraitResourceOutput(sb *strings.Builder, gen *CUEGenerator, name string, res *Resource, depth int) {
gen.writeResourceOutput(sb, name, res, res.outputCondition, depth)
}
// generateParameterBlock generates the parameter schema for the trait.
func (g *TraitCUEGenerator) generateParameterBlock(t *TraitDefinition, depth int) string {
var sb strings.Builder
indent := strings.Repeat(g.indent, depth)
// Check for special parameter types that change the entire parameter structure
for _, param := range t.GetParams() {
// OpenStructParam: parameter: {...}
if _, ok := param.(*OpenStructParam); ok {
sb.WriteString(fmt.Sprintf("%sparameter: {...}\n", indent))
return sb.String()
}
// DynamicMapParam: parameter: [string]: T
if dynMap, ok := param.(*DynamicMapParam); ok {
var typeStr string
if dynMap.GetValueTypeUnion() != "" {
typeStr = dynMap.GetValueTypeUnion()
} else {
typeStr = cueTypeStr(dynMap.GetValueType())
}
sb.WriteString(fmt.Sprintf("%sparameter: [string]: %s\n", indent, typeStr))
return sb.String()
}
}
// Standard parameter block
sb.WriteString(fmt.Sprintf("%sparameter: {\n", indent))
gen := NewCUEGenerator()
for _, param := range t.GetParams() {
// Handle OpenArrayParam specially
if openArr, ok := param.(*OpenArrayParam); ok {
innerIndent := strings.Repeat(g.indent, depth+1)
sb.WriteString(fmt.Sprintf("%s%s: [...{...}]\n", innerIndent, openArr.Name()))
continue
}
gen.writeParam(&sb, param, depth+1)
}
sb.WriteString(fmt.Sprintf("%s}\n", indent))
return sb.String()
}
// writePatchContainerPattern generates the complete PatchContainer pattern in CUE.
// This generates the #PatchParams helper, PatchContainer definition, patch block,
// parameter schema, and errs aggregation.
func (g *TraitCUEGenerator) writePatchContainerPattern(sb *strings.Builder, config *PatchContainerConfig, depth int) {
indent := strings.Repeat(g.indent, depth)
innerIndent := strings.Repeat(g.indent, depth+1)
deepIndent := strings.Repeat(g.indent, depth+2)
// Determine the helper type name (default: "PatchParams")
paramsTypeName := config.ParamsTypeName
if paramsTypeName == "" {
paramsTypeName = "PatchParams"
}
// Generate helper definition
if config.CustomParamsBlock != "" {
// Use custom params block (for complex schemas like startup-probe)
sb.WriteString(fmt.Sprintf("%s#%s: {\n", indent, paramsTypeName))
sb.WriteString(fmt.Sprintf("%s// +usage=Specify the name of the target container, if not set, use the component name\n", innerIndent))
sb.WriteString(fmt.Sprintf("%scontainerName: *\"\" | string\n", innerIndent))
// Write each line of custom params block with proper indentation
for _, line := range strings.Split(strings.TrimSpace(config.CustomParamsBlock), "\n") {
sb.WriteString(fmt.Sprintf("%s%s\n", innerIndent, strings.TrimSpace(line)))
}
sb.WriteString(fmt.Sprintf("%s}\n", indent))
} else {
sb.WriteString(fmt.Sprintf("%s#%s: {\n", indent, paramsTypeName))
sb.WriteString(fmt.Sprintf("%s// +usage=Specify the name of the target container, if not set, use the component name\n", innerIndent))
sb.WriteString(fmt.Sprintf("%scontainerName: *\"\" | string\n", innerIndent))
// Generate patch field parameters
for _, field := range config.PatchFields {
g.writePatchParamField(sb, field, innerIndent)
}
// Generate group field parameters
for _, group := range config.Groups {
g.writePatchParamGroup(sb, group, innerIndent)
}
sb.WriteString(fmt.Sprintf("%s}\n", indent))
}
// Generate PatchContainer definition
if config.CustomPatchContainerBlock != "" {
// Use custom PatchContainer block for complex merge logic
sb.WriteString(fmt.Sprintf("%sPatchContainer: {\n", indent))
// Write each line of custom block with proper indentation
for _, line := range strings.Split(strings.TrimSpace(config.CustomPatchContainerBlock), "\n") {
sb.WriteString(fmt.Sprintf("%s%s\n", innerIndent, line))
}
sb.WriteString(fmt.Sprintf("%s}\n", indent))
} else {
sb.WriteString(fmt.Sprintf("%sPatchContainer: {\n", indent))
sb.WriteString(fmt.Sprintf("%s_params: #%s\n", innerIndent, paramsTypeName))
sb.WriteString(fmt.Sprintf("%sname: _params.containerName\n", innerIndent))
sb.WriteString(fmt.Sprintf("%s_baseContainers: context.output.spec.template.spec.containers\n", innerIndent))
sb.WriteString(fmt.Sprintf("%s_matchContainers_: [for _container_ in _baseContainers if _container_.name == name {_container_}]\n", innerIndent))
sb.WriteString(fmt.Sprintf("%s_baseContainer: *_|_ | {...}\n", innerIndent))
// Container not found error
sb.WriteString(fmt.Sprintf("%sif len(_matchContainers_) == 0 {\n", innerIndent))
sb.WriteString(fmt.Sprintf("%s\terr: \"container \\(name) not found\"\n", innerIndent))
sb.WriteString(fmt.Sprintf("%s}\n", innerIndent))
// Container found - apply patches
sb.WriteString(fmt.Sprintf("%sif len(_matchContainers_) > 0 {\n", innerIndent))
// Write flat fields
for _, field := range config.PatchFields {
g.writePatchContainerField(sb, field, deepIndent)
}
// Write grouped fields (e.g., startupProbe: { ... })
for _, group := range config.Groups {
g.writePatchContainerGroup(sb, group, deepIndent)
}
sb.WriteString(fmt.Sprintf("%s}\n", innerIndent))
sb.WriteString(fmt.Sprintf("%s}\n", indent))
}
// Generate patch block
if config.CustomPatchBlock != "" {
// Use custom patch block for complex patch structures
sb.WriteString(fmt.Sprintf("%s// +patchStrategy=open\n", indent))
sb.WriteString(fmt.Sprintf("%spatch: spec: template: spec: {\n", indent))
for _, line := range strings.Split(strings.TrimSpace(config.CustomPatchBlock), "\n") {
sb.WriteString(fmt.Sprintf("%s%s\n", innerIndent, line))
}
sb.WriteString(fmt.Sprintf("%s}\n", indent))
} else {
if config.PatchStrategy != "" {
sb.WriteString(fmt.Sprintf("%s// +patchStrategy=%s\n", indent, config.PatchStrategy))
}
sb.WriteString(fmt.Sprintf("%spatch: spec: template: spec: {\n", indent))
// Determine the multi-container parameter name
multiParam := config.ContainersParam
if multiParam == "" && config.MultiContainerParam != "" {
multiParam = config.MultiContainerParam
}
if config.AllowMultiple && multiParam != "" {
// Multi-container mode
sb.WriteString(fmt.Sprintf("%sif parameter.%s == _|_ {\n", innerIndent, multiParam))
sb.WriteString(fmt.Sprintf("%s\t// +patchKey=name\n", innerIndent))
sb.WriteString(fmt.Sprintf("%s\tcontainers: [{\n", innerIndent))
sb.WriteString(fmt.Sprintf("%s\t\tPatchContainer & {_params: {\n", innerIndent))
// Default containerName to context.name
if config.DefaultToContextName {
sb.WriteString(fmt.Sprintf("%s\t\t\tif parameter.%s == \"\" {\n", innerIndent, config.ContainerNameParam))
sb.WriteString(fmt.Sprintf("%s\t\t\t\tcontainerName: context.name\n", innerIndent))
sb.WriteString(fmt.Sprintf("%s\t\t\t}\n", innerIndent))
sb.WriteString(fmt.Sprintf("%s\t\t\tif parameter.%s != \"\" {\n", innerIndent, config.ContainerNameParam))
sb.WriteString(fmt.Sprintf("%s\t\t\t\tcontainerName: parameter.%s\n", innerIndent, config.ContainerNameParam))
sb.WriteString(fmt.Sprintf("%s\t\t\t}\n", innerIndent))
}
// Map flat parameters
for _, field := range config.PatchFields {
g.writePatchParamMapping(sb, field, innerIndent+"\t\t\t", "parameter.")
}
// Map grouped parameters
for _, group := range config.Groups {
g.writePatchGroupMapping(sb, group, innerIndent+"\t\t\t", "parameter.")
}
sb.WriteString(fmt.Sprintf("%s\t\t}}\n", innerIndent))
sb.WriteString(fmt.Sprintf("%s\t}]\n", innerIndent))
sb.WriteString(fmt.Sprintf("%s}\n", innerIndent))
// Multiple containers mode
sb.WriteString(fmt.Sprintf("%sif parameter.%s != _|_ {\n", innerIndent, multiParam))
sb.WriteString(fmt.Sprintf("%s\t// +patchKey=name\n", innerIndent))
sb.WriteString(fmt.Sprintf("%s\tcontainers: [for c in parameter.%s {\n", innerIndent, multiParam))
checkField := "containerName"
if config.MultiContainerCheckField != "" {
checkField = config.MultiContainerCheckField
}
errMsg := fmt.Sprintf("containerName must be set for %s", multiParam)
if config.MultiContainerErrMsg != "" {
errMsg = config.MultiContainerErrMsg
}
sb.WriteString(fmt.Sprintf("%s\t\tif c.%s == \"\" {\n", innerIndent, checkField))
sb.WriteString(fmt.Sprintf("%s\t\t\terr: \"%s\"\n", innerIndent, errMsg))
sb.WriteString(fmt.Sprintf("%s\t\t}\n", innerIndent))
sb.WriteString(fmt.Sprintf("%s\t\tif c.%s != \"\" {\n", innerIndent, checkField))
sb.WriteString(fmt.Sprintf("%s\t\t\tPatchContainer & {_params: c}\n", innerIndent))
sb.WriteString(fmt.Sprintf("%s\t\t}\n", innerIndent))
sb.WriteString(fmt.Sprintf("%s\t}]\n", innerIndent))
sb.WriteString(fmt.Sprintf("%s}\n", innerIndent))
} else {
// Single container mode
sb.WriteString(fmt.Sprintf("%s// +patchKey=name\n", innerIndent))
sb.WriteString(fmt.Sprintf("%scontainers: [{\n", innerIndent))
sb.WriteString(fmt.Sprintf("%s\tPatchContainer & {_params: {\n", innerIndent))
// Default containerName to context.name
if config.DefaultToContextName {
sb.WriteString(fmt.Sprintf("%s\t\tif parameter.%s == \"\" {\n", innerIndent, config.ContainerNameParam))
sb.WriteString(fmt.Sprintf("%s\t\t\tcontainerName: context.name\n", innerIndent))
sb.WriteString(fmt.Sprintf("%s\t\t}\n", innerIndent))
sb.WriteString(fmt.Sprintf("%s\t\tif parameter.%s != \"\" {\n", innerIndent, config.ContainerNameParam))
sb.WriteString(fmt.Sprintf("%s\t\t\tcontainerName: parameter.%s\n", innerIndent, config.ContainerNameParam))
sb.WriteString(fmt.Sprintf("%s\t\t}\n", innerIndent))
}
// Map flat parameters
for _, field := range config.PatchFields {
g.writePatchParamMapping(sb, field, innerIndent+"\t\t", "parameter.")
}
// Map grouped parameters
for _, group := range config.Groups {
g.writePatchGroupMapping(sb, group, innerIndent+"\t\t", "parameter.")
}
sb.WriteString(fmt.Sprintf("%s\t}}\n", innerIndent))
sb.WriteString(fmt.Sprintf("%s}]\n", innerIndent))
}
sb.WriteString(fmt.Sprintf("%s}\n", indent))
}
// Determine the multi-container parameter name (for parameter block)
multiParam := config.ContainersParam
if multiParam == "" && config.MultiContainerParam != "" {
multiParam = config.MultiContainerParam
}
// Generate parameter block
switch {
case config.CustomParameterBlock != "":
// Use custom parameter block
sb.WriteString(fmt.Sprintf("%sparameter: ", indent))
for i, line := range strings.Split(strings.TrimSpace(config.CustomParameterBlock), "\n") {
if i == 0 {
sb.WriteString(fmt.Sprintf("%s\n", line))
} else {
sb.WriteString(fmt.Sprintf("%s%s\n", indent, line))
}
}
case config.AllowMultiple && multiParam != "":
defaultMarker := "*"
if config.NoDefaultDisjunction {
defaultMarker = ""
}
sb.WriteString(fmt.Sprintf("%sparameter: %s#%s | close({\n", indent, defaultMarker, paramsTypeName))
containersDesc := config.ContainersDescription
if containersDesc == "" {
containersDesc = "Specify the settings for multiple containers"
}
sb.WriteString(fmt.Sprintf("%s// +usage=%s\n", innerIndent, containersDesc))
sb.WriteString(fmt.Sprintf("%s%s: [...#%s]\n", innerIndent, multiParam, paramsTypeName))
sb.WriteString(fmt.Sprintf("%s})\n", indent))
default:
sb.WriteString(fmt.Sprintf("%sparameter: #%s\n", indent, paramsTypeName))
}
// Generate errs aggregation
sb.WriteString(fmt.Sprintf("%serrs: [for c in patch.spec.template.spec.containers if c.err != _|_ {c.err}]\n", indent))
}
// writePatchParamField writes a single field in the #PatchParams schema.
func (g *TraitCUEGenerator) writePatchParamField(sb *strings.Builder, field PatchContainerField, indent string) {
desc := field.Description
if desc == "" {
desc = fmt.Sprintf("Specify the %s of the container", field.ParamName)
}
sb.WriteString(fmt.Sprintf("%s// +usage=%s\n", indent, desc))
// Determine the type string
typeStr := field.ParamType
if typeStr == "" {
// Infer type from field name/target
switch field.TargetField {
case "image":
typeStr = string(ParamTypeString)
case "imagePullPolicy":
typeStr = "\"IfNotPresent\" | \"Always\" | \"Never\""
case "command", "args":
typeStr = "[...string]"
default:
typeStr = string(ParamTypeString)
}
}
// Determine default value and optionality
defaultVal := field.ParamDefault
optional := ""
if defaultVal == "" && field.Condition != "" {
// Has condition, likely optional - choose appropriate default
if field.Condition == "!= \"\"" {
// String-equality condition: default to empty string
defaultVal = "\"\""
} else {
// Non-string condition (e.g. != _|_): make field optional
optional = "?"
}
}
if defaultVal != "" {
sb.WriteString(fmt.Sprintf("%s%s: *%s | %s\n", indent, field.ParamName, defaultVal, typeStr))
} else {
sb.WriteString(fmt.Sprintf("%s%s%s: %s\n", indent, field.ParamName, optional, typeStr))
}
}
// writePatchParamGroup writes a group of fields in the #PatchParams schema.
func (g *TraitCUEGenerator) writePatchParamGroup(sb *strings.Builder, group PatchContainerGroup, indent string) {
for _, field := range group.Fields {
g.writePatchParamField(sb, field, indent)
}
for _, subGroup := range group.SubGroups {
g.writePatchParamGroup(sb, subGroup, indent)
}
}
// writePatchContainerField writes a field assignment in the PatchContainer body.
func (g *TraitCUEGenerator) writePatchContainerField(sb *strings.Builder, field PatchContainerField, indent string) {
if field.Condition != "" {
// Conditional field
sb.WriteString(fmt.Sprintf("%sif _params.%s %s {\n", indent, field.ParamName, field.Condition))
if field.PatchStrategy != "" {
sb.WriteString(fmt.Sprintf("%s\t// +patchStrategy=%s\n", indent, field.PatchStrategy))
}
sb.WriteString(fmt.Sprintf("%s\t%s: _params.%s\n", indent, field.TargetField, field.ParamName))
sb.WriteString(fmt.Sprintf("%s}\n", indent))
} else {
// Always apply
if field.PatchStrategy != "" {
sb.WriteString(fmt.Sprintf("%s// +patchStrategy=%s\n", indent, field.PatchStrategy))
}
sb.WriteString(fmt.Sprintf("%s%s: _params.%s\n", indent, field.TargetField, field.ParamName))
}
}
// writePatchContainerGroup writes a grouped field block in the PatchContainer body.
// Generates: targetField: { field1: _params.field1, if _params.field2 != _|_ { field2: _params.field2 } }
func (g *TraitCUEGenerator) writePatchContainerGroup(sb *strings.Builder, group PatchContainerGroup, indent string) {
sb.WriteString(fmt.Sprintf("%s%s: {\n", indent, group.TargetField))
innerIndent := indent + "\t"
// Write fields in this group
for _, field := range group.Fields {
if field.Condition != "" {
sb.WriteString(fmt.Sprintf("%sif _params.%s %s {\n", innerIndent, field.ParamName, field.Condition))
sb.WriteString(fmt.Sprintf("%s\t%s: _params.%s\n", innerIndent, field.TargetField, field.ParamName))
sb.WriteString(fmt.Sprintf("%s}\n", innerIndent))
} else {
sb.WriteString(fmt.Sprintf("%s%s: _params.%s\n", innerIndent, field.TargetField, field.ParamName))
}
}
// Write nested subgroups
for _, subGroup := range group.SubGroups {
g.writePatchContainerGroup(sb, subGroup, innerIndent)
}
sb.WriteString(fmt.Sprintf("%s}\n", indent))
}
// writePatchParamMapping writes a parameter mapping in the patch block.
// Fields with a "!= _|_" existence condition and no ParamDefault are truly optional
// (defined as field?: type in the schema) and need conditional guards to avoid
// propagating _|_ when the parameter is unset. Fields with defaults or value-based
// conditions (like '!= ""') always have a value and can be mapped unconditionally.
func (g *TraitCUEGenerator) writePatchParamMapping(sb *strings.Builder, field PatchContainerField, indent string, prefix string) {
if field.Condition == "!= _|_" && field.ParamDefault == "" && field.ParamType == "" {
sb.WriteString(fmt.Sprintf("%sif %s%s %s {\n", indent, prefix, field.ParamName, field.Condition))
sb.WriteString(fmt.Sprintf("%s\t%s: %s%s\n", indent, field.ParamName, prefix, field.ParamName))
sb.WriteString(fmt.Sprintf("%s}\n", indent))
} else {
sb.WriteString(fmt.Sprintf("%s%s: %s%s\n", indent, field.ParamName, prefix, field.ParamName))
}
}
// writePatchGroupMapping writes a group parameter mapping in the patch block.
func (g *TraitCUEGenerator) writePatchGroupMapping(sb *strings.Builder, group PatchContainerGroup, indent string, prefix string) {
for _, field := range group.Fields {
g.writePatchParamMapping(sb, field, indent, prefix)
}
for _, subGroup := range group.SubGroups {
g.writePatchGroupMapping(sb, subGroup, indent, prefix)
}
}
// GenerateDefinitionWithRawTemplate generates a trait definition with fluent API header and raw CUE template.
// This combines the best of both worlds:
// - Header (name, type, description, labels, attributes) from fluent API
// - Template content (patch, outputs, parameter) from raw CUE string
func (g *TraitCUEGenerator) GenerateDefinitionWithRawTemplate(t *TraitDefinition, rawTemplate string) string {
var sb strings.Builder
// Write imports if any
if len(g.imports) > 0 {
sb.WriteString("import (\n")
for _, imp := range g.imports {
sb.WriteString(fmt.Sprintf("\t%q\n", imp))
}
sb.WriteString(")\n\n")
}
// Write trait header
sb.WriteString(fmt.Sprintf("%s: {\n", cueLabel(t.GetName())))
sb.WriteString(fmt.Sprintf("%stype: \"trait\"\n", g.indent))
if t.GetAnnotations() != nil && len(t.GetAnnotations()) > 0 {
annKeys := make([]string, 0, len(t.GetAnnotations()))
for k := range t.GetAnnotations() {
annKeys = append(annKeys, k)
}
sort.Strings(annKeys)
sb.WriteString(fmt.Sprintf("%sannotations: {\n", g.indent))
for _, k := range annKeys {
sb.WriteString(fmt.Sprintf("%s\t%q: %q\n", g.indent, k, t.GetAnnotations()[k]))
}
sb.WriteString(fmt.Sprintf("%s}\n", g.indent))
} else {
sb.WriteString(fmt.Sprintf("%sannotations: {}\n", g.indent))
}
if len(t.labels) > 0 {
labelKeys := sortedKeys(t.labels)
sb.WriteString(fmt.Sprintf("%slabels: {\n", g.indent))
for _, k := range labelKeys {
sb.WriteString(fmt.Sprintf("%s\t%q: %q\n", g.indent, k, t.labels[k]))
}
sb.WriteString(fmt.Sprintf("%s}\n", g.indent))
} else {
sb.WriteString(fmt.Sprintf("%slabels: {}\n", g.indent))
}
sb.WriteString(fmt.Sprintf("%sdescription: %q\n", g.indent, t.GetDescription()))
// Write attributes
sb.WriteString(fmt.Sprintf("%sattributes: {\n", g.indent))
g.writeAttributes(&sb, t, 2)
sb.WriteString(fmt.Sprintf("%s}\n", g.indent))
sb.WriteString("}\n")
// Write template section with raw CUE content
sb.WriteString("template: {\n")
// Indent each line of the raw template
for _, line := range strings.Split(strings.TrimSpace(rawTemplate), "\n") {
sb.WriteString(fmt.Sprintf("%s%s\n", g.indent, line))
}
sb.WriteString("}\n")
return sb.String()
}